本帖最后由 tomcar 于 2021-8-6 07:52 编辑
14、原理4 管道
通常方式:写->读->写->读
管道方式:写->写->读->读
这两种方式相比,管道方式只进行了一个网络通信,节省了IO时间,效率提高了,但是返回结果是一样的。管道中指令越多,效果越好。
14.1、管道压力测试
Redis 自带了一个压力测试工具 redis-benchmark,使用这个工具就可以进行管道测试。
首先我们对一个普通的 set 指令进行压测,QPS 大约 5w/s。
[Bash shell] 纯文本查看 复制代码 > redis-benchmark -t set -q
SET: 51975.05 requests per second
我们加入管道选项-P 参数,它表示单个管道内并行的请求数量,看下面 P=2,QPS 达到
了 9w/s。
[Bash shell] 纯文本查看 复制代码 > redis-benchmark -t set -P 2 -q
SET: 91240.88 requests per second
再看看 P=3,QPS 达到了 10w/s。
SET: 102354.15 requests per second
但如果再继续提升 P 参数,发现 QPS 已经上不去了。因为这里 CPU 处理能力已经达到了瓶颈,Redis 的单线程 CPU 已经飙到了 100%,所
以无法再继续提升了。
14.2、管道原理
我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作 IO 操作的真正耗时。
我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作 IO 操作的真正耗时。所以对于 value = redis.get(key)这样一个简单的请求来说,write 操作几乎没有耗时,直接写到发送缓冲就返回,而 read 就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销。
其实真正的时间开销是在网络上,所以减少网络来回,是好的增加效率的方法。而对于管道来说,连续的 write 操作根本就没有耗时,之后第一个 read 操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作直接就可以从缓冲拿到结果,瞬间就返回了。
15、原理5 事务
15.1、事务使用
[Bash shell] 纯文本查看 复制代码 > multi
OK
> incr books
QUEUED
> incr books
QUEUED
> exec
(integer) 1
(integer) 2
15.2、Watch
Redis 提供了 watch 的机制,它是一种乐观锁。watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 回复告知客户端事务执行失败,这个时候客户端一般会选择重试。
[Bash shell] 纯文本查看 复制代码 > watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败(nil)
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。
15.3、其他
1、Redis事务不是原子的,仅仅满足了事务的隔离性。
2、discard(丢弃)。可以在exec之前丢弃事务缓存队列中的所有指令。
3、一般使用中,事务会结合管道一起使用。
16、原理6 PubSub(消息多播)
16.1、代码实现:python
消费者
[Python] 纯文本查看 复制代码 # -*- coding: utf-8 -*-
import time
import redis
client = redis.StrictRedis()
p = client.pubsub()
p.subscribe("codehole")
while True:
msg = p.get_message()
if not msg:
time.sleep(1)
continue
print msg
生产者
[Python] 纯文本查看 复制代码 # -*- coding: utf-8 -*-
import redis
client = redis.StrictRedis()
client.publish("codehole", "python comes")
client.publish("codehole", "java comes")
client.publish("codehole", "golang comes")
结果:
[Python] 纯文本查看 复制代码 {'pattern': None, 'type': 'subscribe', 'channel': 'codehole', 'data': 1L}
{'pattern': None, 'type': 'message', 'channel': 'codehole', 'data': 'python comes'}
{'pattern': None, 'type': 'message', 'channel': 'codehole', 'data': 'java comes'}
{'pattern': None, 'type': 'message', 'channel': 'codehole', 'data': 'golang comes'}
我们从消费者的控制台窗口可以看到上面的输出,每个消费者窗口都是同样的输出。第一行是订阅成功消息,它很快就会输出,后面的三行会在生产者进程执行的时候立即输出。
阻塞消费者
[Python] 纯文本查看 复制代码 # -*- coding: utf-8 -*-
import time
import redis
client = redis.StrictRedis()
p = client.pubsub()
p.subscribe("codehole")
for msg in p.listen():
print msg
代码简短了很多,不需要再休眠了,消息处理也及时了。
16.2、PubSub 缺点
PubSub 的生产者传递过来一个消息,Redis 会直接找到相应的消费者传递过去。如果一个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息直接被丢弃。
17、原理7 小对象存储
Redis是一个非常耗内存的数据库,它所有的数据都放在内存里。如果不节约使用内存,Redis可能会内存不足而崩溃。
17.1、小对象压缩存储 (ziplist)
如果 Redis 内部管理的集合数据结构很小,它会使用紧凑存储形式压缩存储。
Redis 的 ziplist 是一个紧凑的字节数组结构,每个元素之间都是紧挨着的。Redis 的 intset 是一个紧凑的整数数组结构,它用于存放元素都是整数的并且元素个数较少的 set 集合。
存储界限 当集合对象的元素不断增加,或者某个 value 值过大,这种小对象存储也会被升级为标准结构。
17.2、内存回收机制
Redis并不总是可以将空闲内存立即归还给操作系统。
如果当前Redis内存有10G,当删除了1GB的key后,再去观察内存,会发现内存变化不会太大。原因是操作系统内存回收是以页为单位回收的,如果这个页上还有一个key在使用,那么这个页就不会被回收。但是如果执行flushdb,会发现内存确实被回收了。原因是所有的key都干掉了,大部分之前使用的页面都完全干净了,会立即被操作系统回收。
Redis虽然无法保证立即回收已经删除的key的内存,但是他会重用那些尚未回收的空闲内存。
17.3内存分配算法
Redis 为了保持自身结构的简单性,将内存分配的细节丢给了第三方内存分配库去实现。目前 Redis 可以使用jemalloc(facebook) 库来管理内存,也可以切换到 tcmalloc(google)。因为 jemalloc 相比 tcmalloc 的性能要稍好一些,所以Redis 默认使用了 jemalloc。
[Bash shell] 纯文本查看 复制代码 > info memory
# Memory
used_memory:809608
used_memory_human:790.63K
used_memory_rss:8232960
used_memory_peak:566296608
used_memory_peak_human:540.06M
used_memory_lua:36864
mem_fragmentation_ratio:10.17
mem_allocator:jemalloc-3.6.0
18、原理8 主从同步
18.1、CAP- C - Consistent ,一致性
- A - Availability ,可用性
- P - Partition tolerance ,分区容忍性
分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」。网络分区发生时,一致性和可用性两难全。
Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。
18.2、同步
1、主从同步。Redis 同步支持主从同步和从从同步,从从同步功能是 Redis 后续版本增加的功能,为了减轻主库的同步负担。
2、增量同步。Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。
因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。
3、快照同步。快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次bgsave将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。
4、无盘复制。无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。
5、Wait指令。Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一致性 (不严格)。
[Bash shell] 纯文本查看 复制代码 > set key value
OK
> wait 1 0
(integer) 1
wait 提供两个参数,第一个参数是从库的数量 N,第二个参数是时间 t,以毫秒为单位。它表示等待 wait 指令之前的所有写操作同步到 N 个从库 (也就是确保 N 个从库的同步没有滞后),最多等待时间 t。如果时间 t=0,表示无限等待直到 N 个从库同步完成达成一致。
假设此时出现了网络分区,wait 指令第二个参数时间 t=0,主从同步无法继续进行,wait 指令会永远阻塞,Redis 服务器将丧失可用性。 |