Professional Documents
Culture Documents
《阿里开发者手册 Redis专题》
《阿里开发者手册 Redis专题》
《阿里开发者手册 Redis专题》
(PDF 内更新)
阿里云开发者“藏经阁”
海量电子手册免费下载
卷首语
《阿里开发者手册》每期将聚焦一个当下热门的技术领域,收纳阿里技术大神
的实战精华,手册共分「乘云上」、「正当时」和「创新汇」三个栏目。「乘云上」
涵盖开发者上云时遇到的难题解决方案,为开发者提供相应的技术指导,并提供体
系化、集成式的实操案例;「正当时」主打纯技术分享,帮助开发者突破技术难题;
「创新汇」聚焦技术发展的未来,带领开发者学习新技术,为开发者分享新产品、
解读新趋势。
「乘云上」 ............................................................................. 5
「正当时」 ........................................................................... 20
「创新汇」 ........................................................................... 92
「乘云上」
(间隔页,PDF 中更新)
基于 Redis 实现特殊的消息队列 6
基于 Redis 实现特殊的消息队列
作者:默达
• 消息重复概率比较高时,需要对重复消息进行合并处理避免浪费有限的资源,
减少消费延迟;
• 需要根据业务自定义优先级进行消息处理,高优先级的消息比低优先级的消息
先处理;
• 消息需要定时消费的场景,消息只有在设定的消费时间到了之后立马被消费。
一、 功能介绍
RMQ 消息队列目前支持三种类型的消息,分别是:RangeMergeMessage(区间重
复合并消息)、PriorityMessage(优先级消息)、FixedTimeMessage(任意定时消
息)。
1. 区间重复合并消息
RangeMergeMessage 支持区间重复消息合并,发送消息时需要设置时间区间,消
息延迟该时间区间长度后被消费,在该时间区间内如果发送重复的消息,重复消息
基于 Redis 实现特殊的消息队列 7
2. 优先级消息
PriorityMessage 支持给消息设置任意等级的优先级,优先级高的消息会被优先消费,
相同优先级的消息被随机消费。如果消息在 Redis 服务端发生堆积,重复的消息将
被合并处理,合并后消息的优先级等于最后存储的消息的优先级。该类型消息适用
于希望重复消息合并处理且需要设置优先级的场景,下游消费者资源有限时,合并
重复消息且优先处理优先级高的消息将可以合理利用有限的资源。
3. 任意定时消息
FixedTimeMessage 支持给消息设置任意消费时间,只有消费时间到了之后消息才
被消费,消费时间可精确到秒。消息到期后没有及时被消费时,消费者将按照时间
由远及近进行消费。如果消息在 Redis 服务端发生堆积,重复的消息将被合并处理,
合并后消息的消费时间等于最后存储的消息的消费时间。该类型消息适用于希望重
复消息合并处理且需要定时消费的场景,定时消息应用场景非常丰富,比如定时打
标去标、活动结束后清理动作、订单超时关闭等。
4. 并发消费控制
使用传统消息中间件进行集群消费的时候,为了避免并发处理同一元数据导致不一
致问题,通常需要对元数据加分布式锁,频繁的锁冲突会导致消费效率低下。加分
布式锁的最终目的其实就是保障属于同一元数据的消息被串行消费。加分布式锁并
不是最好的方案,最好的方案应该是从根上解决并发问题,让属于同一元数据的消
息串行消费。
基于 Redis 实现特殊的消息队列 8
RMQ 消息队列具有并发消费控制能力,属于同一元数据的消息只会被分配给全局唯
一一个线程进行消费,因此属于同一元数据的消息将被串行消费。使用方如果需要
该能力,除了需要提供 Redis,还需要提供 ZooKeeper。
5. 重试次数控制
二、 技术原理
1. 总体框架
基于 Redis 实现特殊的消息队列 9
2. 消息存储
基于 Redis 实现特殊的消息队列 10
1) Topic 的设计
2) StoreQueue 的设计
• 优先级消息将优先级作为分数,消费时每次弹出分数最大的消息;
• 任意定时消息将时间戳作为分数,消费时每次弹出分数大于当前时间戳的一个
消息;
• 区间重复合并消息将时间戳作为分数,添加消息时将(当前时间戳+时间区间)
作为分数,消费时每次弹出分数大于当前时间戳的一个消息。
基于 Redis 实现特殊的消息队列 11
3) PrepareQueue 的设计
在后面将会详细介绍二阶段消费的实现思路,这里重点介绍下 PrepareQueue 的存
储设计。
4) DeadQueue 的设计
3. 生产者
1) 区间重复合并消息
2) 优先级消息
3) 任意定时消息
4. 消费者
1) 二阶段消费方式
基于 Redis 实现特殊的消息队列 13
a) 三种消费模式
一般消息队列存在三种消费模式,分别是:最多消费一次、至少消费一次、只消费
一次。
• 最多消费一次模式消息可能丢失,一般不怎么使用;
• 至少消费一次模式消息不会丢失,但是可能存在重复消费,比较常用;
• 只消费一次模式消息被精确只消费一次,实现较困难,一般需要业务记录幂等
ID 来实现。
RMQ 实现了至少消费一次的模式,那么如何保证消息至少被消费一次呢?
b) 至少消费一次模式实现的难点
从最简单的消费模式——最多消费一次说起,消费者端只需要从消息队列服务中取
出消息就行,即执行 Redis 的 zpopmax 命令,不伦消费者是否接收到该消息并成
功消费,消息队列服务都认为消息消费成功。
最多一次消费模式导致消息丢失的因素可能有:
• 网络丢包导致消费者没有接收到消息;
• 消费者接收到消息但在消费的时候宕机了;
• 消费者接收到消息但消费失败。
针对消费失败导致消息丢失的情况比较好解决,只需要把消费失败的消息重新放入
消息队列服务就行,但是网络丢包和消费系统异常导致的消息丢失问题不好解决。
可能有人会想到,我们不把元素从有序集合中 pop 出来,我们先查询优先级最高的
元素,然后消费,再删除消费成功的元素,但是这样消息服务队列就变成了同步阻
塞队列,性能会很差。
基于 Redis 实现特殊的消息队列 14
c) 至少消费一次模式的实现
二阶段消费的流程图如下所示:
基于 Redis 实现特殊的消息队列 15
d) 实现方案的异常情况分析
我们来分析下采用二阶段消费方案可能存在的异常情况,从以下分析来看二阶段消
费方案可以保障消息至少被消费一次。
• 网络丢包导致消费者没有接收到消息,这时消息已经记录到 PrepareQueue,
如果到了超时时间,消息被回滚放回 StoreQueue,等待下次被消费,消息不丢
失。
• 消费者接收到了消息,但是消费者还没来得及消费完成系统就宕机了,消息消
费超时到了后,消息会被重新放入 StoreQueue,等待下次被消费,消息不丢失。
基于 Redis 实现特殊的消息队列 16
• 消费者接收到了消息并消费成功,消费者端在协调事务提交的时候宕机了,消
息消费超时到了后,消息会被重新放入 StoreQueue,等待下次被消费,消息被
重复消费。
• 消费者接收到了消息但消费失败,消费者端在协调事务提交的时候宕机了,消
息消费超时到了后,消息会被重新放入 StoreQueue,等待下次被消费,消息不
丢失。
• 消费者接收到了消息并消费成功,但是由于 fullgc 等原因使消费时间太长,
PrepareQueue 中的消息由于超时已经回滚到 StoreQueue,等待下次被消费,
消息被重复消费。
2) 重试次数控制的实现
PrepareQueue 的分数需要与时间相关,正常情况下,消费者不管消费失败还是消
费成功,都会从 PrepareQueue 删除消息,当消费者系统发生异常或者宕机的时候,
消息就无法从 PrepareQueue 中删除,我们也不知道消费者是否消费成功,为保障
消息至少被消费一次,我们需要做到超时回滚,因此分数需要与消费时间相关。
3) Pop 消息
a) 区间重复合并消息
该消息存储的分数设计为消费时间戳,当前时间大于消息的消费时间戳时,该消息
应该被消费。因此采用 Redis 命令 ZRANGEBYSCORE 弹出分数小于当前时间戳的一
条消息。
基于 Redis 实现特殊的消息队列 18
b) 优先级消息
该消息存储的分数设计为优先级,优先级越高分数越大,因此采用 Redis 命令
ZPOPMAX 弹出分数最大的一条消息。
任意定时消息该消息存储的分数设计为消费时间戳,当前时间大于消息的消费时间
戳时,该消息应该被消费。因此采用 Redis 命令 ZRANGEBYSCORE 弹出分数小于当
前时间戳的一条消息。
三、 相关应用
1. 主图价格表达项目
在主图价格表达中需要实现一个功能,商品价格发生变化时将商品价格打印在商品
主图上面,那么需要在价格发生变动的时候触发合成一张带价格的图片,每一次触
发合图时计算价格都是获取当前最新的价格。
2. 在线数据圈选引擎
在线数据圈选引擎需要处理各种来源的大量动态数据,需要将一段时间区间内的消
息合并处理,减少处理压力,并且在对同一元数据进行并发处理需要加分布式锁,
锁冲突导致消费效率下降。RMQ 的区间重复合并消息和并发消费控制能力可以帮助
解决这些问题。目前,在线数据圈选引擎已经采用了 RMQ 消息队列作为核心组件,
RMQ 消息队列发挥了很大的作用。
四、 总结
「正当时」
(间隔页,PDF 中更新)
Redis 性能优化 21
Redis 性能优化
作者:春潭
一、 Redis 为什么变慢了
1. Redis 真的变慢了吗?
对 Redis 进行基准性能测试
服务器端
# iperf -s -p 12345 -i 1 -M
iperf: option requires an argument -- M
------------------------------------------------------------
Server listening on TCP port 12345
TCP window size: 4.00 MByte (default)
------------------------------------------------------------
[ 4] local 172.20.0.113 port 12345 connected with 172.20.0.114 port 56796
[ ID] Interval Transfer Bandwidth
[ 4] 0.0- 1.0 sec 614 MBytes 5.15 Gbits/sec
[ 4] 1.0- 2.0 sec 622 MBytes 5.21 Gbits/sec
[ 4] 2.0- 3.0 sec 647 MBytes 5.42 Gbits/sec
[ 4] 3.0- 4.0 sec 644 MBytes 5.40 Gbits/sec
[ 4] 4.0- 5.0 sec 651 MBytes 5.46 Gbits/sec
[ 4] 5.0- 6.0 sec 652 MBytes 5.47 Gbits/sec
[ 4] 6.0- 7.0 sec 669 MBytes 5.61 Gbits/sec
[ 4] 7.0- 8.0 sec 670 MBytes 5.62 Gbits/sec
[ 4] 8.0- 9.0 sec 667 MBytes 5.59 Gbits/sec
[ 4] 9.0-10.0 sec 667 MBytes 5.60 Gbits/sec
[ 4] 0.0-10.0 sec 6.35 GBytes 5.45 Gbits/sec
客户端
2. 使用复杂度过高的命令
Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。
查看 Redis 慢日志之前,你需要设置慢日志的阈值。例如,设置慢日志的阈值为 5
毫秒,并且保留最近 500 条慢日志记录:
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要
花费更多的 CPU 资源。
第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多
时间花费在数据协议的组装和网络传输过程中。
3. 操作 bigkey
Redis 性能优化 24
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
这里我需要提醒你的是,当执行这个命令时,要注意 2 个问题:
4. 集中过期
如果是出现这种情况,那么你需要排查一下,业务代码中是否存在设置大量 key 集
中过期的情况。如果有大量的 key 在某个固定时间点集中过期,在这个时间点访问
Redis 时,就有可能导致延时变大。
Redis 性能优化 25
Redis 的过期数据采用被动过期+主动过期两种策略:
注意
这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。
如果此时需要过期删除的是一个 bigkey,那么这个耗时会更久。而且,这个操作延
迟的命令并不会记录在慢日志中。因为慢日志中只记录一个命令真正操作内存数据
的耗时,而 Redis 主动删除过期 key 的逻辑,是在命令真正执行之前执行的。
5. 实例内存达到上限
当 我 们 把 Redis 当 做 纯 缓 存 使 用 时 , 通 常 会 给 这 个 实 例 设 置 一 个 内 存 上 限
maxmemory,然后设置一个数据淘汰策略。
这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置
的淘汰策略:
Redis 性能优化 26
6. fork 耗时严重
7. 开启内存大页
什么是内存大页?
我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的
内存页大小是 4KB。Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许
应用程序以 2MB 大小为单位,向操作系统申请内存。应用程序每次向操作系统申请
的内存单位变大了,但这也意味着申请内存的耗时变长。
这对 Redis 会有什么影响呢?
写时复制你也可以理解成,谁需要发生写操作,谁就需要先拷贝,再修改。这样做
的好处是,父进程有任何写操作,并不会影响子进程的数据持久化(子进程只持久
化 fork 这一瞬间整个实例中的所有数据即可,不关心新的数据变更,因为子进程只
需要一份内存快照,然后持久化到磁盘上)。
但是请注意,主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此
时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis
在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导
致每个写请求的延迟增加,影响到 Redis 性能。
8. 开启 AOF
• appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比
较大的磁盘 IO 资源,但数据安全性最高;
• appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷
到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,
Redis 宕机时丢失的数据取决于操作系统刷盘时机;
• appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线
程每隔 1 秒执行一次刷盘操作(触发 fsync 系统调用),此方案对性能影响相
对较小,但当 Redis 宕机时会丢失 1 秒的数据。
看到这里,我猜你肯定和大多数人的想法一样,选比较折中的方案 appendfsync
everysec 就没问题了吧?
这个方案优势在于,Redis 主线程写完内存后就返回,具体的刷盘操作是放到后台
线程中执行的,后台线程每隔 1 秒把内存中的数据刷到磁盘中。这种方案既兼顾了
性能,又尽可能地保证了数据安全,是不是觉得很完美?
Redis 性能优化 30
但是,这里我要给你泼一盆冷水了,采用这种方案你也要警惕一下,因为这种方案
还是存在导致 Redis 延迟变大的情况发生,甚至会阻塞整个 Redis。
那什么情况下会导致磁盘 IO 负载过大?以及如何解决这个问题呢?我总结了以下
几种情况,你可以参考进行问题排查:
• 有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源。
9. 绑定 CPU
很多时候,我们在部署服务时,为了提高服务性能,降低应用程序在多个 CPU 核心
之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定 CPU 的方式提高性
能。
10.使用 Swap
如果你对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影
响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,
这些内存数据被换到磁盘上的区域,就是 Swap。
问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要
从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!尤其是针对 Redis 这种对
性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。
输出结果如下:
Size: 1256 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 63488 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 65404 kB
Swap: 0 kB
Size: 1921024 kB
Swap: 0 kB
11.碎片整理
# Memory
used_memory:5709194824
used_memory_human:5.32G
used_memory_rss:8264855552
used_memory_rss_human:7.70G
...
mem_fragmentation_ratio:1.45
这个内存碎片率是怎么计算的?
如果 mem_fragmentation_ratio>1.5,说明内存碎片率已经超过了 50%,这时我们
就需要采取一些措施来降低内存碎片了。
解决的方案一般如下:
Redis 碎片整理的参数配置如下:
# 开启自动内存碎片整理(总开关)
activedefrag yes
# 内存碎片率超过 10%,开始碎片整理
active-defrag-threshold-lower 10
# 内存碎片率超过 100%,尽最大努力碎片整理
active-defrag-threshold-upper 100
二、 Redis 如何优化
1. 慢查询优化
• 尽量不使用 O(N)以上复杂度过高的命令,对于数据的聚合操作,放在客户端
做;
• 执行 O(N)命令,保证 N 尽量的小(推荐 N<=300),每次获取尽量少的数据,
让 Redis 可以及时处理返回。
2. 集中过期优化
一般有两种方案来规避这个问题:
Redis 性能优化 35
# 在过期时间点之后的 5 分钟内随机过期掉
Redis.expireat(key, expire_time + random(300))
3. 实例内存达到上限优化
• 避免存储 bigkey,降低释放内存的耗时;
• 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整);
• 拆分实例,把淘汰 key 的压力分摊到多个实例上;
• 如果使用的是 Redis4.0 以上版本,开启 lazy-free 机制,把淘汰 key 释放内存
的操作放到后台线程中执行(配置 lazyfree-lazy-eviction=yes)。
Redis 性能优化 36
4. fork 耗时严重优化
1) 从建立同步时,优先检测是否可以尝试只同步部分数据,这种情况就是针对于
之前已经建立好了复制链路,只是因为故障导致临时断开,故障恢复后重新建
立同步时,为了避免全量同步的资源消耗,Redis 会优先尝试部分数据同步,如
果条件不符合,才会触发全量同步。
3) 如何避免这种情况?解决方案就是适当调大复制缓冲区 repl-backlog-size 的
大小,这个缓冲区的大小默认为 1MB,如果实例写入量比较大,可以针对性调
大此配置。
5. 多核 CPU 优化
那如何解决这个问题呢?
Redis 性能优化 37
如何再进一步优化?
可能你已经想到了,我们是否可以让主线程、子进程、后台线程,分别绑定在固定
的 CPU 核心上,不让它们来回切换,这样一来,他们各自使用的 CPU 资源互不影
响。
taskset -c 0 ./Redis-server
$ cd /proc/5332
一旦发生内存 swap,最直接的解决方法就是增加机器内存。如果该实例在一个
Redis 切片集群中,可以增加 Redis 集群的实例个数,来分摊每个实例服务的数据
量,进而减少每个实例所需的内存量。
7. 内存大页
看到,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,
这就会影响 Redis 正常的访存操作,最终导致性能变慢。
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
在实际生产环境中部署时,我建议你不要使用内存大页机制,操作也很简单,只需
要执行下面的命令就可以了:
为了保证重启后也能生效:可以在/etc/rc.local 中追加命令。
其实,操作系统提供的内存大页机制,其优势是,可以在一定程序上降低应用程序
申请内存的次数。但是对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们
希望 Redis 在每次申请内存时,耗时尽量短,所以我不建议你在 Redis 机器上开启
这个机制。
支持版本:Redis4.0+
• UNLINK 命令
注意
DEL 命令,还是并发阻塞的删除操作。
• FLUSHALL/FLUSHDB ASYNC
127.0.0.1:7000> DBSIZE
(integer) 1812295
127.0.0.1:7000> flushall //同步清理实例数据,180 万个 key 耗时 1020 毫秒
OK
(1.02s)
127.0.0.1:7000> DBSIZE
(integer) 1812637
127.0.0.1:7000> flushall async //异步清理实例数据,180 万个 key 耗时约 9 毫秒
OK
127.0.0.1:7000> SLOWLOG get
1) 1) (integer) 2996109
2) (integer) 1505465989
3) (integer) 9274 //指令运行耗时 9.2 毫秒
4) 1) "flushall"
2) "async"
5) "127.0.0.1:20110"
6) ""
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
• lazyfree-lazy-eviction
• lazyfree-lazy-expire
• lazyfree-lazy-server-del
• slave-lazy-flush
ü 针对 slave 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运
行 flushall 来清理自己的数据场景,参数设置决定是否采用异常 flush 机制。
如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输
出缓冲区爆涨引起的内存使用增长。
# info memory
# Memory
lazyfree_pending_objects:0
注意
unlink 命令入口函数 unlinkCommand()和 del 调用相同函数 delGenericCommand()
进行删除 KEY 操作,使用 lazy 标识是否为 lazyfree 调用。
举例
• 一个包含 100 元素的 list key,它的 free cost 就是 100;
• 一个 512MB 的 string key,它的 free cost 是 1。
9. AOF 优化
如果占用磁盘资源的是其他应用程序,那就比较简单了,你需要定位到是哪个应用
程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行就好了,避免对
Redis 产生影响。
10.Swap 优化
永久生效使用:
echo vm.swappiness=10>>/etc/sysctl.conf
三、 Redis 变慢了排查步骤
1) 获取 Redis 实例在当前环境下的基线性能;
2) 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把
聚合计算命令放在客户端做;
Redis 性能优化 44
7) 在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关
闭内存大页机制就行了;
一文搞懂 Redis
作者:一洺
一、 什么是 NoSQL
• 关系型数据库:列+行,同一个表下数据的结构是一样的。
• 非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。
2. 传统 RDBMS 和 NoSQL
1) RDBMS
• 组织化结构
• 固定 SQL
• 数据和关系都存在单独的表中(行列)
• DML(数据操作语言)、DDL(数据定义语言)等
• 严格的一致性(ACID):原子性、一致性、隔离性、持久性
• 基础的事务
2) NoSQL
• 不仅仅是数据
• 没有固定查询语言
一文搞懂 Redis 46
• 键值对存储(Redis)、列存储(HBase)、文档存储(MongoDB)、图形数据
库(不是存图形,放的是关系)(Neo4j)
• 最终一致性(BASE):基本可用、软状态/柔性事务、最终一致性
二、 Redis 是什么?
三、 Redis 五大基本类型
Redis 是一个开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息
队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs
等数据类型。内置复制、Lua 脚本、LRU 收回、事务以及不同级别磁盘持久化功能,
同时通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区。
由于 Redis 类型大家很熟悉,且网上命令使用介绍很多,下面重点介绍五大基本类
型的底层数据结构与应用场景,以便当开发时,可以熟练使用 Redis。
1. String(字符串)
应用场景
1) 缓存功能
2) 计数器
3) 统计多单位的数量
4) 共享用户 session
用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存
cookie,这两种方式做有一定弊端:
• 每次都重新登录效率低下;
• cookie 保存在客户端,有安全隐患。
2. List(列表)
list 类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素,一
个列表当中可以存储有一个或者多个元素,Redis 的 list 支持存储 2^32 次方-1 个
元素。
Redis 可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范
围的元素集,或者读取指定下标的元素等操作。Redis 列表是一种比较灵活的链表
数据结构,它可以充当队列或者栈的角色。
Redis 列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可
以重复的。意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。
应用场景
1) 消息队列
Reids 的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成
队列的设计。比如:数据的生产者可以通过 Lpush 命令从左边插入数据,多个数据
消费者,可以使用 BRpop 命令阻塞的“抢”列表尾部的数据。
2) 文章列表或者数据分页展示的应用
比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都
有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用 Reids
的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功
能。大大提高查询效率。
一文搞懂 Redis 49
3. Set(集合)
应用场景
1) 标签
比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容
的用户利用一个标签把他们进行归并。
2) 共同好友功能
共同喜好,或者可以引申到二度好友之类的扩展应用。
3) 统计网站的独立 IP
数据结构
4. Sorted set(有序集合)
Redis 有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,
但是不同的是,有序集合给每个元素多设置了一个分数。
Redis 有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,
但是不同的是,有序集合给每个元素多设置了一个分数,利用该分数作为排序的依
据。
应用场景
1) 排行榜
有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护
可能是多方面:按照时间、按照播放量、按照获得的赞数等。
5. hash(哈希)
应用场景
2) 经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当
中读取,提高系统性能。
四、 五大基本类型底层数据存储结构
结构图如下:
/*
* 字典
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
// 指向具体 RedisObject
void *val;
//
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
1. RedisObject
/*
* Redis 对象
*/
typedef struct RedisObject {
// 类型 4bits
unsigned type:4;
// 编码方式 4bits
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock) 24bits
unsigned lru:22;
// 引用计数 Redis 里面的数据可以通过引用计数进行共享 32bits
int refcount;
// 指向对象的值 64-bit
void *ptr;
} robj;
ptr 指 向 具 体 的 数 据 结 构 的 地 址 ; type 表 示 该 对 象 的 类 型 , 即
String,List,Hash,Set,Zset 中的一个,但为了提高存储效率与程序执行效率,每种对
象的底层数据结构实现都可能不止一种,encoding 表示对象底层所使用的编码。
Redis 对象底层的八种数据结构:
REDIS_ENCODING_INT(long 类型的整数)
REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)
REDIS_ENCODING_RAW (简单动态字符串)
REDIS_ENCODING_HT (字典)
REDIS_ENCODING_LINKEDLIST (双端链表)
REDIS_ENCODING_ZIPLIST (压缩列表)
REDIS_ENCODING_INTSET (整数集合)
REDIS_ENCODING_SKIPLIST (跳跃表和字典)
一文搞懂 Redis 53
前期准备知识已准备完毕,下面分每种基本类型来讲。
2. String 数据结构
String 类型的转换顺序:
1) SDS
/* 针对不同长度整形做了相应的数据结构
* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings.
*/
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
a) 低复杂度获取字符串长度
b) 避免缓冲区溢出
c) 减少修改字符串的内存重新分配次数
C 语言字符串不记录字符串长度,如果要修改字符串要重新分配内存,如果不进行
重新分配会造成内存缓冲区泄露。
c) 二进制安全:因为 C 字符串以空字符作为字符串结束的标识,而对于一些二进
制文件(如图片等),内容可能包括空字符串,因此 C 字符串无法正确存取;
而所有 SDS 的 API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS
不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是
否结束;
d) 遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h>
中的一部分函数。
一文搞懂 Redis 56
• RedisObject 占用空间
4+4+24+32+64=128bits=16 字节。
• sdshdr8 占用空间
1(uint8_t)+1(uint8_t)+1(unsigned char)+1(buf[]中结尾的'\0'字符)=4 字
节。
3. List 存储结构
1) Redis3.2 之前的底层实现方式
2) Redis3.2 及之后的底层实现方式:quicklist
a) ziplist
ziplist 是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是
在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用
ziplist 存储。但当数据量过大时就 ziplist 就不是那么好用了。因为为了保证他存储
内容在内存中的连续性,插入的复杂度是 O(N),即每次插入都会重新进行 realloc。
ziplist 结构如下:
• Zlbytes:用于记录整个压缩列表占用的内存字节数;
• zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节;
• zllen:记录了压缩列表包含的节点数量;
• entryX:要说列表包含的各个节点;
• zlend:用于标记压缩列表的末端。
为什么数据量大时不用 ziplist?
一文搞懂 Redis 58
b) 快速列表(quickList)
为什么不直接使用 linkedlist?
• quicklist 结构:
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
• 结构图如下:
• ziplist 的长度
• 压缩深度
4. Hash 类型
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
5. 渐进式 rehash
1) 扩容条件
不扩容主要是为了尽可能减少内存页过多分离,系统需要更多的开销去回收内存。
一文搞懂 Redis 62
2) 缩容条件
3) rehash 步骤
说明
采用渐进式 rehash 的好处在于它采取分而治之的方式,避免了集中式 rehash 带来
的庞大计算量。特别的在进行 rehash 时只能对 h[0]元素减少的操作,如查询和删
除;而查询是在两个哈希表中查找的,而插入只能在 ht[1]中进行,ht[1]也可以查询
和删除。
过程如下图:
一文搞懂 Redis 63
6. set 数据结构
• 存储的数据都是整数;
• 存储的数据元素个数小于 512 个。
1) inset 结构体
// encoding 的值可以是以下三个常量的其中一个
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
2) inset 的查询
说明
inset 是有序不要和我们 zset 搞混,zset 是设置一个 score 来进行排序,而 inset 这
里只是单纯的对整数进行升序而已。
7. Zset 数据结构
1) ziplist 做排序
每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的
成员(member),而第二个元素则保存元素的分值(score)。
存储结构图如下一目了然:
一文搞懂 Redis 65
2) skiplist 跳表
/*
* 跳跃表
*/
typedef struct zskiplist {
// 头节点,尾节点
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 目前表内节点的最大层数
int level;
} zskiplist;
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// member 对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 这个层跨越的节点数量
unsigned int span;
} level[];
} zskiplistNode;
跳表是什么?我们先看下链表:
3) Redis 跳跃表
• Header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂
度就为 O(1);
• Tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就
为 O(1);
• Level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算
在内),通过这个属性可以再 O(1)的时间复杂度内获取层高最好的节点的层
数;
• Length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不
计算在内),通过这个属性,程序可以再 O(1)的时间复杂度内返回跳跃表的
长度。
1) 层(level)
一文搞懂 Redis 67
每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他
节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越
远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当
程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出
现的概率越小)随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大
小就是层的“高度”。
2) 后退(backward)指针
节点中用 BW 字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退
指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后
退指针,因此每次只能后退一个节点。
3) 分值(score)
4) 成员对象(oj)
五、 三大特殊数据类型
1. geospatial(地理位置)
应用场景
• 查看附近的人;
• 微信位置共享;
• 地图上直线距离的展示。
2. Hyperloglog(基数)
什么是基数?不重复的元素。
hyperloglog 是用来做基数统计的,其优点是:输入的提及无论多么大,hyperloglog
使用的空间总是固定的 12KB,利用 12KB,它可以计算 2^64 个不同元素的基数!
非常节省空间!但缺点是估算的值,可能存在误差。
应用场景
标准判断。但如果这种方式保存大量用户 id,会占用大量内存,我们的目的是为了
计数,而不是去保存 id。
3. Bitmaps(位存储)
应用场景
两种状态的统计都可以使用 bitmaps,例如:统计用户活跃与非活跃数量、登录与
非登录、上班打卡等等。
六、 Redis 事务
事务本质:一组命令的集合。
1. 数据库事务与 Redis 事务
1) 数据库的事务
数据库事务通过 ACID(原子性、一致性、隔离性、持久性)来保证。
数据库中除查询操作以外,插入(Insert)、删除(Delete)和更新(Update)这三
种操作都会对数据造成影响,因为事务处理能够保证一系列操作可以完全地执行或
者完全不执行,因此在一个事务被提交以后,该事务中的任何一条 SQL 语句在被执
行的时候,都会生成一条撤销日志(Undo Log)。
一文搞懂 Redis 70
2) Redis 事务
Redis 事务提供了一种“将多个命令打包,然后一次性、按顺序地执行”的机制,
并且事务在执行的期间不会主动中断——服务器在执行完事务中的所有命令之后,
才会继续处理其他客户端的其他命令。
Redis 中一个事务从开始到执行会经历开始事务(muiti)、命令入队和执行事务(xec)
三个阶段,事务中的命令在加入时都没有被执行,直到提交时才会开始执行(exec)
一次性完成。
一组命令中存在两种错误不同处理方式:
• 代码语法错误(编译时异常)所有命令都不执行;
• 代码逻辑错误(运行时错误),其他命令可以正常执行(该点不保证事务的原子
性)。
• Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或
是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的
命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该
出现在生产环境中。
• 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
说明
鉴于没有任何机制能避免程序员自己造成的错误,并且这类错误通常不会在生产环
境中出现,所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。
2. 事务监控
1) 悲观锁:认为什么时候都会出现问题,无论做什么操作都会加锁;
一文搞懂 Redis 71
2) 乐观锁:认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判
断一下,在此期间是否有人修改过这个数据。
watch 保证事务只能在所有被监视键都没有被修改的前提下执行,如果这个前提不
能满足的话,事务就不会被执行。
watch 执行流程
七、 Redis 持久化
Redis 是一种内存型数据库,一旦服务器进程退出,数据库的数据就会丢失,为了
解决这个问题 Redis 供了两种持久化的方案,将内存中的数据保存到磁盘中,避免
数据的丢失。
一文搞懂 Redis 72
• RDB 持久化方式会在一个特定的间隔保存那个时间点的数据快照;
• AOF 持久化方式则会记录每一个服务器收到的写操作。在服务启动时,这些记
录的操作会逐条执行从而重建出原来的数据。写操作命令记录的格式跟 Redis
协议一致,以追加的方式进行保存;
• Redis 的持久化是可以禁用的,就是说你可以让数据的生命周期只存在于服务
器的运行时间里;
• 两种方式的持久化是可以同时存在的,但是当 Redis 重启时,AOF 文件会被优
先用于重建数据。
1. RDB 持久化
RDB 持久化产生的文件是一个经过压缩的二进制文件,这个文件可以被保存到硬盘
中,可以通过这个文件还原数据库的状态,它可以手动执行,也可以在 Redis.conf
配置文件中配置,定时执行。
1) 工作原理
• Redis 调用 forks。同时拥有父进程和子进程;
• 子进程将数据集写入到一个临时 RDB 文件中;
• 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文
件,并删除旧的 RDB 文件。
2) 触发机制
a) 主动触发
• save 命令是同步的命令,会占用主进程,会造成阻塞,阻塞所有客户端的请求
• bgsave,bgsave 是异步进行,进行持久化的时候,Redis 还可以将继续响应客
户端请求。
bgsave 和 save 对比
b) 自动触发
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/Redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
一文搞懂 Redis 74
# 导入时是否检查
rdbchecksum yes
优点
• RDB 的内容为二进制的数据,占用内存更小,更紧凑,更适合做为备份文件;
• RDB 对灾难恢复非常有用,它是一个紧凑的文件,可以更快的传输到远程服务
器进行 Redis 服务恢复;
• RDB 可以更大程度的提高 Redis 的运行速度,因为每次持久化时 Redis 主进程
都会 fork()一个子进程,进行数据持久化到磁盘,Redis 主进程并不会执行磁盘
I/O 等操作;
• 与 AOF 格式的文件相比,RDB 文件可以更快的重启。
缺点
1) AOF 配置项
# 默认文件名
appendfilename "appendonly.aof"
AOF 的 整 个 流 程 大 体 来 看 可 以 分 为 两 步 , 一 步 是 命 令 的 实 时 写 入 ( 如 果 是
appendfsync everysec 配置,会有 1s 损耗),第二步是对 AOF 文件的重写。
2) AOF 重写机制
3) 触发方式
• 手动触发:bgrewriteaof;
一文搞懂 Redis 76
• 自动触发:就是根据配置规则来触发,当然自动触发的整体时间还跟 Redis 的
定时任务频率有关系。
优点
缺点
3. rdb 与 aof 对比
八、 发布与订阅
Redis 发布与订阅是一种消息通信的模式:发送者(pub)发送消息,订阅者(sub)
接收消息。
一文搞懂 Redis 77
1. 频道(channel)
1) 订阅
2) 发布
一文搞懂 Redis 78
3) 完整流程
b) 订阅者订阅消息
订阅频道后的客户端可能会收到三种消息类型:
• subscribe。表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三
个是当前客户端订阅的频道数量。
• message。表示接收到的消息,第二个值表示产生消息的频道名称,第三个值
是消息的内容。
• unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称,第三
个值是当前客户端订阅的频道数量,当此值为 0 时客户端会退出订阅状态,之
后就可以执行其他非“发布/订阅”模式的命令了。
4) 数据结构
于频道的发布订阅模式是通过字典数据类型实现的。
struct RedisServer {
// ...
dict *pubsub_channels;
// ...
};
一文搞懂 Redis 79
其中,字典的键为正在被订阅的频道,而字典的值则是一个链表,链表中保存了所
有订阅这个频道的客户端。
订阅
发布
退订
遍历关联的链表,将指定的地址删除即可。
2. 模式(pattern)
1) 订阅发布完整流程
a) 发布者发布消息
127.0.0.1:6379> publish b m1
(integer) 1
127.0.0.1:6379> publish b1 m1
(integer) 1
127.0.0.1:6379> publish b11 m1
(integer) 1
一文搞懂 Redis 81
b) 订阅者订阅消息
127.0.0.1:6379> psubscribe b*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "b*"
3) (integer) 3
1) "pmessage"
2) "b*"
3) "b"
4) "m1"
1) "pmessage"
2) "b*"
3) "b1"
4) "m1"
1) "pmessage"
2) "b*"
3) "b11"
4) "m1"
2) 数据结构
pattern 属性是一个链表,链表中保存着所有和模式相关的信息。
struct RedisServer {
// ...
list *pubsub_patterns;
// ...
};
// 链表中的每一个节点结构如下,保存着客户端与模式信息
typedef struct pubsubPattern {
RedisClient *client;
robj *pattern;
} pubsubPattern;
数据结构图如下:
一文搞懂 Redis 82
订阅
当有信的订阅时,会将订阅的客户端和模式信息添加到链表后面。
发布
当发布者发布消息时,首先会发送到对应的频道上,在遍历模式列表,根据 key 匹
配模式,匹配成功将消息发给对应的订阅者。
完成的发布伪代码如下:
退订
使用 punsubscribe,可以将订阅者退订,将改客户端移除出链表。
一文搞懂 Redis 83
九、 主从复制
什么是主从复制?
主从复制的作用主要包括:
• 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
• 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢
复;实际上是一种服务的冗余。
• 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由
从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用
连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点
分担读负载,可以大大提高 Redis 服务器的并发量。
• 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,
因此说主从复制是 Redis 高可用的基础。
主从库采用的是读写分离的方式:
一文搞懂 Redis 84
1. 原理
分为全量复制与增量复制。
• 全量复制:发生在第一次复制时;
• 增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库。
2. 全量复制的三个阶段
1) 第一阶段是主从库间建立连接、协商同步的过程
主要是为全量复制做准备。从库和主库建立起连接,并告诉主库即将进行同步,主
库确认回复后,主从库间就可以开始同步了。
从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响
应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给
从库。
2) 第二阶段,主库将所有数据同步给从库
为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从
库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中
断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证
主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件
生成后收到的所有写操作。
3) 第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库
十、 哨兵机制
哨兵的核心功能是主节点的自动故障转移。下图是一个典型的哨兵集群监控的逻辑
图:
一文搞懂 Redis 86
哨兵实现了以下功能:
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;
而配置中心和通知功能,则需要在与客户端的交互中才能体现。
1. 原理
1) 监控
2) 主观/客观下线
a) 主观下线
b) 客观下线
注意
Sentinel 节点的数量至少为 3 个,否则不满足 quorum 判定条件。
3) 哨兵选举
• 每个 Sentinel 节点都有资格成为领导者,当它主观认为某个数据节点宕机后,
会向其他 Sentinel 节点发送 sentinel is-master-down-by-addr 命令,要求自
己成为领导者;
• 收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的 sentinelis-
master-down-by-addr 命令,将同意该请求,否则拒绝(每个 Sentinel 节点只
有 1 票);
• 如 果 该 Sentinel 节 点 发 现 自 己 的 票 数 已 经 大 于 等 于 MAX ( quorum,
num(sentinels)/2+1),那么它将成为领导者。
如果此过程没有选举出领导者,将进入下一次选举。
4) 故障转移
注意
Leader Sentinel 节点,会从新的 master 节点那里得到一个 configuration epoch,
本质是个 version 版本号,每次主从切换的 version 号都必须是唯一的。其他的哨
兵都是根据 version 来更新自己的 master 配置。
十一、 缓存穿透、击穿、雪崩
1. 缓存穿透
1) 问题来源
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不
命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这
将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流
量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这
就是漏洞。
2) 解决方案
2. 缓存击穿
1) 问题来源
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于
并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库
压力瞬间增大,造成过大压力。
2) 解决方案
• 设置热点数据永远不过期;
• 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接
口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返
回机制;
• 加互斥锁。
3. 缓存雪崩
1) 问题来源
一文搞懂 Redis 91
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力
过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪
崩是不同数据都过期了,很多数据都查不到从而查数据库。
2) 解决方案
• 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生;
• 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中;
• 设置热点数据永远不过期。
「创新汇」 92
「创新汇」
(间隔页,PDF 中更新)
从 Redis7.0 发布看 Redis 的过去与未来 93
作者:仲肥
前言
经历接近一年的开发、三个候选版本,Redis7.0 终于正式发布,这是 Redis 历史上
改变最多的一个大版本,它不仅包含了 50 多个新命令,还有大量核心新特性与改
进,这些不仅能够解决用户使用中的诸多问题,还进一步拓展了 Redis 的使用场景。
虽然 Redis7.0 做了许多大胆的尝试,但是稳定性依然是最重要的基石。Redis 官方
在 7.0 的 GA 博文中也强调这一点:“While user-facing features are easy to boast
of, the real “unsung heroes”in this version are efforts to make Redis more
performant, stable, and lean.”(虽然面向用户的功能更容易得到称赞,但这个版
本中的无名英雄是我们持续不断的对 Redis 的优化,使其变得更加精简、高效、稳
定)。相信从这里可以看出,Redis 发展至今稳定性始终被摆在最重要的位置,因此
对于 7.0 的稳定性担忧大可打消。
一、 Redis7.0 核心新特性概览
1. Function
2. Multi-part AOF
对具体实现感兴趣的同学可以查看本文末尾参考资料。
从 Redis7.0 发布看 Redis 的过去与未来 95
3. Sharded-pubsub
Sharded-pubsub 便是用来解决这个问题,意如其名,sharded-pubsub 会把
channel 按分片来进行分发,一个分片节点只负责处理属于自己的 channel 而不会
进行广播,以很简单的方法避免了资源的浪费。
4. Client-eviction
Client-eviction 并不是终点,还有很多元数据的内存使用会对用户造成困扰,Redis
是基于内存的数据库,我们需要对各个模块的内存进行更精确的统计和控制,让用
户能够对数据存储有更为清晰的理解和规划。
二、 Redis 历史回顾
这些变化与我们在阿里云上的统计结果也是相符合的:Redis 目前同样也已是国内
用户使用规模最大的 NoSQL 数据库之一,并一直处于高速增长中,越来越多的泛
互联网甚至传统行业也在逐步接纳 Redis,用于快速高效的构建业务应用服务。
• 虽然 Redis 发展迅速,但国内用户却大都无法享受技术红利
过多用户担心升级版本的兼容性,一方面阿里云也在要求社区来提供一些兼容性验
证工具,另一方面阿里云对版本跟进是很快的,可以让用户在新版发布后尽快进行
验证。从 Redis 整体和阿里云的大量客户升级情况来看,版本的向前兼容性是非常
好的,大可放心升级。
• 希望更多的国内用户深度参与社区建设
1. 多模服务进入爆发期
Redis 是一直贴着用户使用发展起来的,它如此受欢迎主要因为两个特点,极高的
性能以及丰富、方便使用的数据结构,这些简单好用的数据结构能够大幅度降低开
发业务复杂度。
2. 一致性能力上的发展
落盘一致性和副本一致性是使用数据库绕不开的两个话题。长期以来许多人对
Redis 的应用场景仅仅认定为缓存(尤其是国外用户)。Redis 自诞生之初便支持持
久化机制 RDB 和 AOF,并且 AOF 还提供了不同级别的持久化语义:如 appendfsync
从 Redis7.0 发布看 Redis 的过去与未来 99
基 于 rocksdb 开 源
s 社区版 Tair 持久内存 MemoryDB
Redis-like 系统
落盘一致性
可配置 optane 全持久化 云盘全持久化 无,部分可配置
3. AWS 的 MemoryDB
4. 阿里云自研内存数据库 Tair
在持久化上阿里云走了另外一条路,通过引入新介质持久化内存来解决成本,大容
量和持久化能力的问题。这个方案带来的挑战是使用持久化内存存储结构设计上较
为复杂,既要控制性能衰减,又要保证兼容性。Tair 持久内存很好的解决了这些问
题,对比 MemoryDB 成本更低,读性能基本持平的情况下写入的速度也更快(见本
文末尾参考资料),更关键的是基本原封原样的兼容了 Redis API,大幅降低了用户
的切换成本。
5. 其他开源产品的发展
国内也出现了一些原创性的优秀落盘开源产品(Redis-Like 系统),这些产品大都
基于 LSM 存储结构如 rocksdb 上的。它们的优点主要是磁盘介质相对内存更为便
宜,但同样目前存在的缺点也非常多:运维复杂度较高,直接映射为运维成本、KV
无法原生的支持 Redis 的数据结构、把 Redis 的强类型变成弱类型等等。
目前这类系统在一致性和容错性上仍有很大的改善空间,而在用户使用体感上,由
于很多用户使用习惯还是把 Redis-like 系统用在业务的同步链路上,对于 LSM KV
从 Redis7.0 发布看 Redis 的过去与未来 101
引擎的延时上抖动整体吞吐的影响直接映射成了用户体感,因此很难作为一款通用
型产品,而这些痛点也同样存在与 Tair 容量存储型中(过去叫混合存储版),这也
是一个需要长期在存储和兼容性上优化的方向。
综上所述,容量版本可以很好地解决用户的使用成本问题,但是只有更好地解决了
落盘一致性问题和副本一致性问题,才能够把 Redis 类系统的使用场景拓展到企业
级。这也是目前看来云厂商一个激烈竞争的企业级产品主流赛道,也有较高的技术
门槛。
四、 写在最后
参考资料
[1] Redis7.0 Multi Part AOF 的设计和实现:
https://developer.aliyun.com/article/866957
从 Redis7.0 发布看 Redis 的过去与未来 102
作者:驱动
一、 AOF
二、 AOFRW
当主进程承接了较大的写入流量时,aof_rewrite_buf 中可能会堆积非常多的数据,
导致在重写期间子进程无法将 aof_rewrite_buf 中的数据全部消费完。此时,
aof_rewrite_buf 剩余的数据将在重写结束时由主进程进行处理。
当子进程完成重写操作并退出后,主进程会在 backgroundRewriteDoneHandler
中处理后续的事情。首先,将重写期间 aof_rewrite_buf 中未消费完的数据追加到
临时 AOF 文件中。其次,当一切准备就绪时,Redis 会使用 rename 操作将临时
AOF 文件原子的重命名为 server.aof_filename,此时原来的 AOF 文件会被覆盖。
至此,整个 AOFRW 流程结束。
图 1 AOFRW 实现原理
Redis7.0 Multi Part AOF 的设计和实现 105
三、 AOFRW 存在的问题
1. 内存开销
aof_pending_rewrite:0
aof_buffer_length:35500
aof_rewrite_buffer_length:34000
aof_pending_bio_fsync:0
3351:M 25 Jan 2022 09:55:39.655 * Background append only file rewriting started
by pid 6817
3351:M 25 Jan 2022 09:57:51.864 * AOF rewrite child asks to stop sending diffs.
6817:C 25 Jan 2022 09:57:51.864 * Parent agreed to stop sending diffs.
Finalizing AOF...
6817:C 25 Jan 2022 09:57:51.864 * Concatenating 2135.60 MB of AOF diff received
from parent.
3351:M 25 Jan 2022 09:57:56.545 * Background AOF buffer size: 100 MB
2. CPU 开销
CPU 的开销主要有三个地方,分别解释如下:
/* Append data to the AOF rewrite buffer, allocating new blocks if needed. */
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
// 此处省略其他细节...
// 此处省略其他细节...
}
/* Read again a few times to get more data from the parent.
* We can't read forever (the server may receive data from clients
* faster than it is able to send data to the child), so we try to read
* some more data in a loop as soon as there is a good chance more data
* will come. If it looks like we are wasting time, we abort (this
* happens after 20 ms without new data). */
int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
// 此处省略其他细节...
}
Redis7.0 Multi Part AOF 的设计和实现 107
3) 在子进程完成重写操作后,主进程会在 backgroundRewriteDoneHandler 中进
行收尾工作。其中一个任务就是将在重写期间 aof_rewrite_buf 中没有消费完
成的数据写入临时 AOF 文件。如果 aof_rewrite_buf 中遗留的数据很多,这里
也将消耗 CPU 时间。
// 此处省略其他细节...
}
3. 磁盘 IO 开销
4. 代码复杂度
/* AOF pipes used to communicate between parent and child during rewrite. */
int aof_pipe_write_data_to_child;
int aof_pipe_read_data_from_parent;
int aof_pipe_write_ack_to_parent;
int aof_pipe_read_ack_from_child;
int aof_pipe_write_ack_to_child;
int aof_pipe_read_ack_from_parent;
Redis7.0 Multi Part AOF 的设计和实现 108
四、 MP-AOF 实现
1. 方案概述
• BASE:表示基础 AOF,它一般由子进程通过重写产生,该文件最多只有一个;
• INCR:表示增量 AOF,它一般会在 AOFRW 开始执行时被创建,该文件可能存
在多个;
• HISTORY:表示历史 AOF,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成
功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,
HISTORY 类型的 AOF 会被 Redis 自动删除。
图 2 MP-AOF Rewrite 原理
Redis7.0 Multi Part AOF 的设计和实现 109
2. 关键实现
1) Manifest
a) 在内存中的表示
typedef struct {
sds file_name; /* file name */
long long file_seq; /* file sequence */
aof_file_type file_type; /* file type */
} aofInfo;
typedef struct {
aofInfo *base_aof_info; /* BASE file information. NULL if there
is no BASE file. */
list *incr_aof_list; /* INCR AOFs list. We may have multiple
INCR AOF when rewrite fails. */
list *history_aof_list; /* HISTORY AOF list. When the AOFRW
success, The aofInfo contained in
`base_aof_info` and `incr_aof_list` will be
moved to this list. We
will delete these AOF files when AOFRW
finish. */
long long curr_base_file_seq; /* The sequence number used by the
current BASE file. */
long long curr_incr_file_seq; /* The sequence number used by the
current INCR file. */
int dirty; /* 1 Indicates that the aofManifest in the
memory is inconsistent with
disk, we need to persist it immediately. */
} aofManifest;
struct RedisServer {
// 此处省略其他细节...
// 此处省略其他细节...
}
b) 在磁盘上的表示
Manifest 格式本身需要具有一定的扩展性,以便将来添加或支持其他的功能。比如
可以方便的支持新增 key/value 和注解(类似 AOF 中的注解),这样可以保证较好
的 forward compatibility。
Redis7.0 Multi Part AOF 的设计和实现 111
2) 文件命名规则
3) 兼容老版本升级
• 如果 appenddirname 目录不存在;
• 或者 appenddirname 目录存在,但是目录中没有对应的 manifest 清单文件;
• 如果 appenddirname 目录存在且目录中存在 manifest 清单文件,且清单文件
中只有 BASE AOF 相关信息,且这个 BASE AOF 的名字和 server.aof_filename
相同,且 appenddirname 目录中不存在名为 server.aof_filename 的文件。
// 此处省略其他细节...
}
升级准备工作主要分为三个部分:
// 此处省略其他细节...
}
4) 多文件加载及进度计算
/* Here we calculate the total size of all BASE and INCR files in
* advance, it will be set to `server.loading_total_bytes`. */
total_size = getBaseAndIncrAppendOnlyFilesSize(am);
startLoading(total_size, RDBFLAGS_AOF_PREAMBLE, 0);
listRewind(am->incr_aof_list, &li);
while ((ln = listNext(&li)) != NULL) {
aofInfo *ai = (aofInfo*)ln->value;
aof_name = (char*)ai->file_name;
updateLoadingFileName(aof_name);
loadSingleAppendOnlyFile(aof_name);
}
}
server.aof_current_size = total_size;
server.aof_rewrite_base_size = server.aof_current_size;
server.aof_fsync_offset = server.aof_current_size;
stopLoading();
// 此处省略其他细节...
}
当子进程完成重写操作,子进程会创建一个名为 temp-rewriteaof-bg-pid.aof 的临
时 AOF 文件,此时这个文件对 Redis 而言还是不可见的,因为它还没有被加入到
manifest 文件中。要想使得它能被 Redis 识别并在 Redis 启动时正确加载,我们还
需要将它按照前文提到的命名规则进行 rename 操作,并将其信息加入到 manifest
文件中。
backgroundRewriteDoneHandler 函数通过七个步骤实现了上述逻辑:
c) 将 子 进 程 产 生 的 temp-rewriteaof-bg-pid.aof 临 时 文 件 重 命 名 为
new_base_filename;
f) 如果上述步骤都成功了,我们可以放心的将内存中的 server.aof_manifest 指针
指向临时的 manifest 结构(并释放之前的 manifest 结构),至此整个修改对
Redis 可见;
/* 2. Get a new BASE file name and mark the previous (if we have)
* as the HISTORY type. */
new_base_filename = getNewBaseFileNameAndMarkPreAsHistory(temp_am);
6) 支持 AOF truncate
7) AOFRW 限流
五、 总结
参考资料
[1] http://mysql.taobao.org/monthly/2018/12/06/
[2] https://help.aliyun.com/document_detail/145956.html