这 3 篇文章是我在学习 Redis 的过程中,总结的笔记:
- 第一篇 Redis学习笔记1-理论篇
- 1,Redis 中的数据类型
- 2,Redis 的 IO 模型
- 3,Redis 的 持久化
- 4,Redis 集群原理
- 5,将 Redis 用作缓存
- 第二篇 Redis学习笔记2-性能篇
- 6,Redis 高性能的影响因素
- 6.1,Redis 内部的阻塞式操作
- 6.2,CPU 核和 NUMA 架构的影响
- 6.3,Redis 关键系统配置
- 6.4,Redis 内存碎片
- 6.5,Redis 缓冲区
- 第三篇 Redis学习笔记3-实战篇
- 7,Redis 中如何进行原子操作
- 8,Redis 是否适合用作消息队列
- 9,使用 Redis 实现分布式锁
1,Redis 数据类型的底层结构
1.1,Redis 中的数据类型
Redis 中的数据类型及其特点:
Redis 还提供了 3 种扩展数据类型,分别是:
- Bitmap:可以把 Bitmap 看作是一个 bit 数组
- Bitmap 提供了
GETBIT/SETBIT
操作,使用一个偏移值 offset 对 bit 数组的某一个 bit 位进行读和写。 - Bitmap 的偏移量是从 0 开始算的,也就是说 offset 的最小值是 0。
- 当使用
SETBIT
对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。 - Bitmap 还提供了
BITCOUNT
操作,用来统计这个 bit 数组中所有“1”的个数。 - Bitmap 支持用
BITOP
命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中
- Bitmap 提供了
- HyperLogLog:这是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。
- 常用命令:
PFADD
(添加元素)、PFCOUNT
(统计数量) - 每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
- 注意 HyperLogLog 的统计规则是基于概率的,所以它的统计结果是有一定误差的,标准误算率是 0.81%。这意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。如果需要精确统计结果,最好还是用 Set 或 Hash 类型。
- 常用命令:
- GEO:GEO 类型的底层数据结构就是用
Sorted Set
来实现的。主要用于提供位置信息类服务,比如打车软件,附近的餐馆等。常用命令:GEOADD
命令:用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中;GEORADIUS
命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内(可以自己定义)的其他元素。
1.2,全局哈希表
Redis 的高性能离不开高效的数据结构,其使用一个全局哈希表来存储所有的键值对:
1.3,数据类型的底层结构
Redis 中的 5 种数据类型及其对应的底层数据结构:
整数数组和双向链表
整数数组和双向链表通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N)
,操作效率比较低;它们的优势是节省空间。
压缩列表
压缩列表类似于一个数组,数组中的每一个元素都保存一个数据。压缩列表在表头有三个字段,表尾有一个字段:
- zlbytes:列表长度
- zltail:列表尾的偏移量
- zllen:列表中的元素个数
- zlend:表示列表结束
压缩列表的查找效率:
- 定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是
O(1)
- 而查找其他元素时,只能逐个查找,复杂度是
O(N)
跳表
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转(在多级索引上跳来跳去),实现数据的快速定位(其查找复杂度是 O(logN)
),如下图所示:
Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?
Redis 中有两个配置项:
hash-max-ziplist-entries
:表示用压缩列表保存时,哈希集合中的最大元素个数hash-max-ziplist-value
:表示用压缩列表保存时,哈希集合中单个元素的最大长度
当 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries
,或者写入的单个元素大小超过了 hash-max-ziplist-value
,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。
1.4,哈希冲突
Redis 使用链式哈希的方式来解决哈希冲突。
哈希冲突链上的元素只能通过指针逐一查找再操作。当哈希表里写入的数据越来越多,就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。
1.5,rehash 操作
Redis 使用 Rehash 来处理哈希链过长的问题,通过增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存。
为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:
- 哈希表 1
- 哈希表 2
一开始,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。Redis 的 rehash 过程分三步:
- 给哈希表 2 分配更大的空间(比如是当前哈希表 1 的两倍)
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
- 该过程涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求
- 为了避免这个问题,Redis 采用了渐进式 rehash:把一次性大量拷贝的开销,分摊到了多次处理请求的过程中
- 即拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
- 释放哈希表 1 的空间
渐进式 rehash 过程图如下:
2,Redis 的 IO 模型
通常所说的Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供服务的主要流程。
但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,是由额外的线程执行的。
2.1,Redis 为什么使用单线程
Redis 为什么使用单线程,而不是多线程或多进程?
2020 年 5 月,Redis 6.0 的稳定版发布了,Redis 6.0 中提出了多线程模型。
多线程的主要问题在于:系统中通常会存在被多线程同时访问的共享资源,当有多个线程要修改这个共享资源时,就需要额外的机制进行保证全安性,从而带来了额外的开销。
2.2,多路复用机制
Redis 采用多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
在网络连接的建立(accept
)、数据的读取(recv
)和发送(send
)都有可能导致 socket 阻塞,从而阻塞整个线程,影响并发处理能力。
所以,高效 IO 模型中 socket 必须是非阻塞的。
Socket 的非阻塞模式设置,主要体现在三个关键的函数调用上:
多路复用机制
Linux 中的 IO 多路复用机制是指,一个线程处理多个 IO 流,也就是 select/epoll 机制,该机制允许内核中,同时存在多个监听套接字和已连接套接字。
select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
3,Redis 的持久化
Redis 的持久化主要有两大机制,即 AOF(Append Only File
)日志和 RDB 快照( Redis DataBase
)。
3.1,AOF 机制
AOF 叫做写后日志:先执行命令,把数据写入内存,然后才记录日志。
AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
AOF 的优缺点:
- 优点:
- Redis 在向 AOF 里面记录日志的时候,并不需要对命令进行语法检查,因为在内存中执行命令时,已经检查过。
- Redis 是在命令执行后才记录日志,所以不会阻塞当前的写操作。
- 缺点:
- 数据丢失风险:比如刚执行完一个命令,还没有来得及记日志就宕机了
- AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险
- 因为 AOF 日志也是在主线程中执行的,如果日志文件在写磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了
appendfsync
参数的三个可选值控制了 AOF 的写盘时机:
- Always:每个写命令执行完,立马同步地将日志写回磁盘
- 对于 always 策略来说,Redis 需要确保每个操作记录日志都写回磁盘,如果用后台子线程异步完成,主线程就无法及时地知道每个操作是否已经完成了,这就不符合 always 策略的要求了。
- 所以,always 策略并不使用后台子线程来执行。
- Everysec:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
fsync
的执行时间很长,如果是在 Redis 主线程中执行 fsync,就容易阻塞主线程- 所以,当写回策略配置为 everysec 时,Redis 会使用后台的子线程异步完成 fsync 的操作
- No:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
三种写回策略的优缺点:
AOF 重写机制
随着接收的写命令越来越多,AOF 文件会越来越大,这会引发三个问题:
- 文件系统本身对文件大小有限制,无法保存过大的文件;
- 如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
- 如果发生宕机,为了故障恢复,AOF 中记录的命令要一个个被重新执行,如果日志文件太大,整个恢复过程就会非常缓慢
AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。
而在AOF 重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样,一个键值对在重写日志中只用一条命令就行了。
AOF 重写过程是否会阻塞主线程
要把整个数据库的最新数据的操作日志都写回磁盘,是一个非常耗时的过程。
因此,重写过程是由后台子进程 bgrewriteaof
来完成的,这避免了阻塞主线程,导致数据库性能下降。
每次执行重写时,主线程 fork
出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。
如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite
设置为 yes
:
no-appendfsync-on-rewrite yes
- 这个配置项设置为
yes
时,表示在 AOF 重写时,不进行 fsync 操作。- 即 Redis 把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了。当然,如果此时实例发生宕机,就会导致数据丢失
- 这个配置项设置为
no
(默认配置)时,在 AOF 重写时,Redis 会调用后台线程进行 fsync 操作,这就会给实例带来阻塞
3.2,RDB 快照机制
内存快照,是指内存中的数据在某一个时刻的状态记录(RDB 记录的是某一时刻的数据,并不是操作)。
RDB 快照是指:把某一时刻的内存状态以文件的形式写到磁盘上,这个快照文件称为 RDB 文件。
在做数据恢复时,可以直接把 RDB 文件读入内存,很快地完成恢复。
给内存的全量数据做快照,把它们全部写入磁盘会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。
Redis 提供了两个命令来生成 RDB 文件,分别是 save
和 bgsave
:
- save:在主线程中执行,会导致阻塞
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞(不影响整个系统的读写服务)
- 这也是 Redis RDB 文件生成的默认配置
- bgsave 子进程是由主线程 fork(会阻塞主线程) 生成的,可以共享主线程的所有内存数据。
- bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件
- 在写 RDB 文件的过程中,如果主线程对这些数据进行读操作,那么,主线程和 bgsave 子进程相互不影响。
- 如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据写入 RDB 文件。这叫做写时复制技术(
Copy-On-Write, COW
),由操作系统提供。
使用 INFO 命令查看 Redis 的
latest_fork_usec
指标值,表示最近一次 fork 的耗时。
写时复制的底层原理
主线程 fork 出 bgsave 子进程后,bgsave 子进程复制了主线程的页表(保存了所有数据)。当 bgsave 子进程生成 RDB 时,是根据页表读取这些数据,再写入磁盘中。
写时复制是指,主线程在有写操作时,才会把这个新写或修改后的数据写入到一个新的物理地址中,并修改自己的页表映射。
从下图中可看出,主线程修改了它自己的页表,而并没有影响子进程的页表。
多久做一次快照?
如果做快照的频率比较密集(比如一秒一次),则会带来两个问题:
- 频繁将全量数据写入磁盘,会给磁盘带来很大压力
- fork 操作本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长
AOF 与 RDB 的另一个区别:
- AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢
- RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度快
3.3,混合 AOF 与 RDB
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。
简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作;等到下一次做全量快照时,就可以清空上一次的 AOF 日志。
关于 AOF 和 RDB 的选择的三点建议:
- 如果数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用 RDB;
- 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
4,Redis 集群原理
Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
- 对于读操作:主库、从库都可以接收;
- 对于写操作:首先到主库执行,然后,主库将写操作同步给从库。
主从库模式采用读写分离,所有数据的修改只会在主库上进行。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。
4.1,主从库之间的第一次数据同步
启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof
(Redis 5.0 之前使用 slaveof
)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
例如,现在有:
- 实例 1(172.16.19.3)
- 实例 2(172.16.19.5)
在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:
replicaof 172.16.19.3 6379
主从库之间的第一次数据同步:
psync
命令包含了主库的 runID 和复制进度 offset 两个参数:
- runID:每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例
- 当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”
- offset:此时设为 -1,表示第一次复制
FULLRESYNC
响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset。
一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接(长连接),主库会通过这个连接将后续陆续收到的命令操作再同步给从库。
4.2,主-从-从模式
如果从库比较多,那么数据的同步过程就会占用主库较多的资源。这时,我们可以使用主从从模式,该模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
4.3,当主从间网络断开
在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制(开销非常大)。
从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
全量复制是同步所有数据,而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。
当主从库断连后,主库会把断连期间收到的写操作命令,写入replication buffer
,同时也会把这些操作命令也写入 repl_backlog_buffer
这个缓冲区。
repl_backlog_buffer
是一个环形缓冲区,主库会记录自己写到的位置(master_repl_offset),从库则会记录自己已经读到的位置(slave_repl_offset )。
正常情况下,这两个偏移量基本相等:
主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库。
由于 repl_backlog_buffer 是一个环形缓冲区,在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作,这会导致主从库间的数据不一致。
因此需要调整 repl_backlog_size
参数:
- repl_backlog_size 调整为缓冲空间大小
*
2 - 缓冲空间大小 = 主库写入命令速度
*
操作大小-
主从库间网络传输命令速度*
操作大小
增大 repl_backlog_size
只能缓解该问题,而不能彻底解决该问题。如果并发请求量非常大,连两倍的缓冲空间都存不下新操作请求的话,此时,主从库数据仍然可能不一致。针对这种情况:
- 一方面,可以根据 Redis 所在服务器的内存资源再适当增加 repl_backlog_size 值,比如说设置成缓冲空间大小的 4 倍
- 另一方面,可以考虑使用切片集群来分担单个主库的请求压力
4.4,Redis 主从切换原理
当主库挂了,就需要运行一个新主库,比如说把一个从库切换为主库。
在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制。
哨兵就是一个运行在特殊模式下的 Redis 进程,它主要复制三项任务:
- 监控:判断主库是否真的挂了。哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。
- 如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;
- 如果主库没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程(选主)。
- 选主:从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库
- 通知:把新主库的相关信息通知给从库和客户端
- 哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
- 哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上
如何判断主库下线
哨兵对主库的下线判断有“主观下线”和“客观下线”两种:
- 如果发现从库对 PING 命令的响应超时了,哨兵就会先把它标记为主观下线
- 如果检测的是主库,哨兵还不能简单地把它标记为“主观下线”(有可能判断错误)
为了减少误判,通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。
只有大多数的哨兵实例,都判断主库已经主观下线了,主库才会被标记为客观下线,接下来,哨兵会进行主从切换流程。
客观下线的标准就是:
- 当有 N 个哨兵实例时,要有
N/2 + 1
个实例判断主库为“主观下线”,才能最终判定主库为“客观下线” - 当然,有多少个实例做出“主观下线”的判断才行,可以由 Redis 管理员自行设定
哨兵如何选主
在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库。
在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。
如何之前的网络连接状态呢?使用配置项 down-after-milliseconds * 10
:
down-after-millisecond
是认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就认为主从节点断连了- 如果发生断连的次数超过了
10
次,就说明这个从库的网络状况不好,不适合作为新主库。
打分规则有三轮,只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
- 从库优先级:可以通过
slave-priority
配置项,给从库设置不同优先级。- 如果从库的优先级都一样,那么哨兵开始第二轮打分。
- 从库复制进度:和旧主库同步程度最接近的从库得分高。
- 哪个从库的
slave_repl_offset
最接近主库的master_repl_offset
,则得分最高 - 如果无法区分,则进行第三轮打分
- 哪个从库的
- 从库 ID 号:每个实例都会有一个 ID,ID 号最小的从库得分最高,会被选为新主库。
4.5,哨兵集群
通过部署多个实例,就形成了一个哨兵集群,即使有某个哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作。
在配置哨兵的信息时,我们只需要用到下面的这个配置项,设置主库的 IP 和端口,并没有配置其他哨兵的连接信息。
sentinel monitor <master-name> <ip> <redis-port> <quorum>
哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。
哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP 和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。
在主从集群中,主库上有一个名为__sentinel__:hello
的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
哨兵通过向主库发送 INFO 命令来获取从库的信息,从而与从库建立连接,实现对从库的监控。
通过 pub/sub 机制,哨兵之间可以组成集群;同时,哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。
哪个哨兵执行主从切换?
哨兵集群需要通过投票选出一个 Leader 去执行主从切换,选举过程(三个哨兵,quorum 为 2)如下:
quorum 是哨兵配置文件中的一个配置项,当某个哨兵的投票数达到 quorum 时,它才能称为 Leader。
在 T1 时刻,S1 判断主库为“客观下线”,它想成为 Leader,就先给自己投一张赞成票,然后分别向 S2 和 S3 发送命令,表示要成为 Leader。
在 T2 时刻,S3 判断主库为“客观下线”,它也想成为 Leader,所以也先给自己投一张赞成票,再分别向 S1 和 S2 发送命令,表示要成为 Leader。
哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。
4.6,切片集群
Redis 切片集群用于保存海量数据,切片集群涉及到多个实例的分布式管理问题。
切片集群,也叫分片集群,是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。
- 如果把 25GB 的数据平均分成 5 份(也可以不做均分),使用 5 个实例来保存,每个实例只需要保存 5GB 数据
切片集群是一种保存大量数据的通用机制,从 3.0 开始,官方提供了一个名为 Redis Cluster
的方案,用于实现切片集群。
在 Redis 3.0 之前,Redis 官方并没有提供切片集群方案,但是,当时业界已经有了一些切片集群的方案,例如基于客户端分区的 ShardedJedis,基于代理的 Codis、Twemproxy 等。
切片数据如何分布?
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
在部署 Redis Cluster
方案时,可以使用 cluster create
命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。
- 例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
当每个机器的硬件配置不一样时,也可以使用 cluster meet
命令手动建立实例间的连接,形成集群,再使用 cluster addslots
命令,指定每个实例上的哈希槽个数。
如下示例:
示意图中的切片集群一共有 3 个实例,同时假设有 5 个哈希槽。我们可以通过下面的命令手动分配哈希槽:
- 实例 1 保存哈希槽 0 和 1
- 实例 2 保存哈希槽 2 和 3
- 实例 3 保存哈希槽 4
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4
注意:在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
- 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
- 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
4.7,主从同步中的问题
1,主从数据不一致
主从数据不一致,就是指客户端从从库中读取到的值和主库中的最新值并不一致;这是由于主从库间的命令复制是异步进行的,主库收到新的写命令后,会发送给从库。但是,主库并不会等到从库实际执行完命令后,再把结果返回给客户端,而是主库自己在本地执行完命令后,就会向客户端返回结果了。
如何知道主从库间的复制进度?
Redis 的 INFO replication
命令可以查看主库接收写命令的进度信息(master_repl_offset
)和从库复制写命令的进度信息(slave_repl_offset
),用 master_repl_offset
减去 slave_repl_offset
,就能得到从库和主库间的复制进度差值了。
2,读到过期数据
Redis 中设置数据过期时间的命令一共有 4 个:
EXPIRE
和PEXPIRE
:它们给数据设置的是从命令执行时开始计算的存活时间EXPIREAT 和
PEXPIREAT`:它们会直接把数据的过期时间设置为具体的一个时间点
一个数据过期后,应该是被删除的,客户端不能再读取到该数据。
但是有时候删除数据不及时,会导致读到过期数据,这是由 Redis 的删除过期数据的策略导致的。
Redis 有两种过期数据删除策略:
- 惰性删除策略:当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
- 如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。
- 但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,从库并不会触发数据删除。那么,从库会给客户端返回过期数据吗?
- Redis 3.2 之前的版本,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。
- 在 3.2 版本后,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。
- 定期删除策略:Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除。
当主从库全量同步时,如果主库接收到了一条 EXPIRE 命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了。
为了避免这种情况,建议在业务应用中使用 EXPIREAT/PEXPIREAT
命令,把数据的过期时间设置为具体的时间点,避免读到过期数据。
总结:
- 主从数据不一致。Redis 采用的是异步复制,所以无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。我给你提供了应对方法:保证良好网络环境,以及使用程序监控从库复制进度,一旦从库复制进度超过阈值,不让客户端连接从库。
- 对于读到过期数据,这是可以提前规避的,一个方法是,使用 Redis 3.2 及以上版本;另外,你也可以使用
EXPIREAT/PEXPIREAT
命令设置过期时间,避免从库上的数据过期时间滞后。不过,这里有个地方需要注意下,因为EXPIREAT/PEXPIREAT
设置的是时间点,所以,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的 NTP 服务器(时间服务器)进行时钟同步。
3,不合理配置项导致服务挂掉
4.8,集群中脑裂
脑裂是指在主从集群中,同时有多个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
5,将 Redis 用作缓存
5.1,缓存淘汰策略
一般来说,建议把缓存容量(Redis 容量)设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。
对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:
CONFIG SET maxmemory 4gb
当缓存写满后(超过 maxmemory
值),如果再向缓存中写入数据,就涉及到数据的淘汰策略。Redis 中有 8 种数据淘汰策略:
- 不进行数据淘汰:
noeviction
(默认策略):一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误 - 在设置了过期时间的数据中进行淘汰,包括:
volatile-random
:在设置了过期时间的键值对中,进行随机删除volatile-ttl
: 根据过期时间的先后进行删除,越早过期的越先被删除volatile-lru
:使用 LRU 算法筛选设置了过期时间的键值对volatile-lfu
(Redis 4.0 后新增):使用 LFU 算法选择设置了过期时间的键值对,它是在 LRU 算法的基础上,同时考虑了数据的访问时效性和数据的访问次数,可以看作是对淘汰策略的优化
- 在所有数据范围内进行淘汰,包括:
allkeys-lru
:使用 LRU 算法在所有数据中进行筛选allkeys-random
:从所有键值对中随机选择并删除数据allkeys-lfu
(Redis 4.0 后新增):使用 LFU 算法在所有数据中进行筛选
淘汰策略使用建议:
- 优先使用
allkeys-lru
策略。这样可以把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。 - 如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用
allkeys-random
策略,随机选择淘汰的数据就行。 - 如果你的业务中有置顶的需求(比如置顶新闻、置顶视频),可以使用
volatile-lru
策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
5.2,缓存中的数据和后端数据库中的不一致问题
在实际应用 Redis 缓存时,经常会遇到一些异常问题:
- 缓存中的数据和数据库中的不一致
- 缓存雪崩
- 缓存击穿
- 缓存穿透
5.3,缓存雪崩
缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
有两种情况会导致缓存雪崩:
- 缓存中有大量数据同时过期
- Redis 缓存实例发生故障宕机
5.4,缓存击穿
缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。
5.5,缓存穿透
缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。
此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。
5.6,缓存污染
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。