[TOC]
Redis模块组成
Redis数据结构(String,List,Hash,Set,SortSet,BitMap,HyperLogLog,GEO)
- Redis 使用了一个哈希表来保存所有键值对。哈希表的每一项是一个 dictEntry 的结构体。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。key、value都是用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据指针。
- Redis 解决哈希冲突的方式,就是链式哈希。
- 渐进式 rehash:
- 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
- 释放哈希表 1 的空间
- 压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1)
- String类型以SDS结构体存储,包括三部分:buf–字节数组,保存实际数据。len:占 4 个字节,表示 buf 的已用长度。alloc:也占个 4 字节,表示 buf 的实际分配长度。
- 内存分配上:jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
- hash类型使用压缩列表还是哈希表作为低层数据结构,主要取决与:
- hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
- hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
- 集合计算
- 时序序列:hash、sortset、 RedisTimeSeries
- 事务:使用MULTI和EXEC命令时,建议客户端使用pipeline,当使用pipeline时,客户端会把命令一次性批量发送给服务端。
IO模型
- Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果
- select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。
- 性能瓶颈主要包括2个方面:
- 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
- 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey、过期释放内存同样会产生耗时;
- 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
- 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
- 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
- AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
- 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
- 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
- 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
日志
AOF
- Redis 是先执行命令,把数据写入内存,然后才记录日志。
- AOF 日志也是在主线程中执行的。策略
1. Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘,主线程调用fsync操作完成。 2. Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;使用后台的子线程异步完成 fsync 的操作。 3. No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
- AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件。重写过程是由子进程 bgrewriteaof 来完成的。
- 重写的过程总结为“一个拷贝,两处日志”:主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
- AOF重写子进程写入量大->fsync线程阻塞->主线程阻塞
RDB
- Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照。策略:
- save:在主线程中执行,会导致阻塞;
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
- Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
主从模式
- 步骤:
- 从库给主库发送 psync 命令,表示要进行数据同步。psync 命令包含了主库的 runID 和复制进度 offset。
- 主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset。
- 主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
- 只要有从库存在,主库会记录 RDB 文件生成后收到的所有写操作到环形缓存区repl_backlog_buffer, 如果主库与从库建立了连接,写入client buffer:replication buffer中。当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库。
- 通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
- 长连接复制是主从库正常运行后的常规同步阶段。在这个阶段中,主从库之间通过命令传播实现同步。
- 主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前对于环形缓存区real_backlog_buffer的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。
- 如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。
- 可以调整repl_backlog_size 这个参数。这个参数和所需的缓冲空间大小有关。缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 操作大小 - 主从库间网络传输命令速度 操作大小。
哨兵机制
- 流程
- 监控:监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。
- 选主:主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。哨兵集群在判断了主库“客观下线”后,经过投票仲裁,选举一个 Leader 出来,由它负责实际的主从切换。
- 通知:在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
- 主观下线:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。
- 客观下线:当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。
- 选主方法:
- 筛选:1. 检查从库的当前在线状态;2. 在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好。
- 打分:进行三轮打分,这三个规则分别是从库优先级(slave-priority)、从库复制进度(slave_repl_offset)以及从库 ID 号(ID 号小的从库得分高)。只要在某一轮中,有从库得分最高,那么它就是主库了。
- 哨兵集群发现:哨兵和主库建立起了连接,就可以在主库上PUB/SUB,当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。
- 哨兵与从库建立连接:哨兵向主库发送 INFO 命令,主库把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接。
- 哨兵与客户端通讯:
- 要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds
切片集群
- 部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
- 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
- Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。
- 重定向
- 原因:实例数量变动;负载均衡
- 步骤:当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。客户端更新缓存。
- 迁移部分完成:客户端就会收到一条 ASK 报错信息,返回最新实例地址,客户端需要给新实例发送 ASKING 命令,然后再发送操作命令。ASK 命令并不会更新客户端缓存的哈希槽分配信息。
消息队列
- List用作队列时,为了保证消息可靠性,使用BRPOPLPUSH命令把消息取出的同时,还把消息插入到备份队列中,从而防止消费者故障导致消息丢失。每次执行BRPOPLPUSH命令后,当消费者成功消费取出的消息后,最好把备份队列中的消息删除,防止备份队列存储过多无用的数据。
- 丢消息情况及处理:
- 生产者在发布消息时异常:
a) 网络故障或其他问题导致发布失败(直接返回错误,消息根本没发出去)
b) 网络抖动导致发布超时(可能发送数据包成功,但读取响应结果超时了,不知道结果如何)
消费者需要保证在收到重复消息时,依旧能保证业务的正确性(设计幂等逻辑)。 - 消费者在处理消息时异常:Streams则是采用ack的方式,消费成功后告知中间件。
- 消息队列中间件丢失消息: 在用Redis当作队列或存储数据时,是有可能丢失数据的:一个场景是,如果打开AOF并且是每秒写盘,因为这个写盘过程是异步的,Redis宕机时会丢失1秒的数据。而如果AOF改为同步写盘,那么写入性能会下降。另一个场景是,如果采用主从集群,如果写入量比较大,从库同步存在延迟,此时进行主从切换,也存在丢失数据的可能。总的来说,Redis不保证严格的数据完整性和主从切换时的一致性。我们在使用Redis时需要注意。
- 生产者在发布消息时异常:
性能
- 影响因素:
- Redis 内部的阻塞式操作;
- CPU 核和 NUMA 架构的影响;
- Redis 关键系统配置;
- Redis 内存碎片;
- Redis 缓冲区。
- 阻塞点:
- 集合全量查询和聚合操作;
- bigkey 删除;删除操作的本质是要释放键值对占用的内存空间,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加。
- 清空数据库;
- AOF 日志同步写;
- 从库加载 RDB 文件。
- Redis4.0以后,Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除(lazy free)以及文件关闭的异步执行。
- bigkey 删除时,建议:先使用集合类型提供的 SCAN 命令读取数据,然后再进行删除。
- 在多核 CPU 架构下,Redis 如果在不同的核上运行,就需要频繁地进行上下文切换,这个过程会增加 Redis 的执行时间,客户端也会观察到较高的尾延迟了。所以,建议在 Redis 运行时,把实例和某个核绑定。为了提升 Redis 的网络性能,我们有时还会把网络中断处理程序和 CPU 核绑定。
- 过期删除规则:默认情况下,Redis 每 100 毫秒会删除一些过期 key,具体的算法如下:
- 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;
- 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
- Redis基线性能:redis-cli 命令提供了–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟。
- AOF重写子进程写入量大->fsync线程阻塞->主线程阻塞
- 操作系统的内存 swap(物理机器内存不足)
- 内存大页(支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行):Redis在写时复制时,如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。
- 内存碎片:
- 内因:内存分配器的分配策略就决定了操作系统无法做到“按需分配”。这是因为,内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。
- 外因:键值对大小不一样和删改操作
- 查看:mem_fragmentation_ratio 的指标表示的就是 Redis 当前的内存碎片率。大于 1 但小于 1.5是合理的,过高表示碎片过高,过低表示wu里内存不足,导致swap。
- 碎片清理:Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes。
缓冲区
- AOF 缓冲区(由定时任务写入文件)、AOF重写缓冲区(由子进程写入文件)
- 在子进程进行AOF重写期间,Redis主进程执行的命令会被保存在AOF重写缓冲区里面
- 当子进程完成AOF重写工作之后,父进程将AOF重写缓冲区中的所有内容写入到新的AOF文件中,对新的AOF文件进行改名、覆盖旧文件
- no-appendfsync-on-rewrite设置为yes(表示在日志重写时,不进行命令追加操作,而只是将命令放在重写缓冲区里,避免与命令的追加造成磁盘IO上的冲突)
- 客户端输入和输出缓冲区
- CLIENT LIST: qbuf,表示输入缓冲区已经使用的大小; qbuf-free,表示输入缓冲区尚未使用的大小
- 如果使用多个客户端,导致 Redis 内存占用过大,也会导致内存溢出(out-of-memory)问题
- 缓冲区溢出:
- 服务器端返回 bigkey 的大量结果;
- 执行了 MONITOR 命令;
- 缓冲区大小设置得不合理 client-output-buffer-limit。
- 复制缓冲区、复制积压缓冲区
- 主节点上复制缓冲区的内存开销,会是每个从节点客户端输出缓冲区占用内存的总和。如果集群中的从节点数非常多的话,主节点的内存开销就会非常大。
缓存异常
缓存不一致
- 问题1: 删除缓存值或更新数据库失败而导致数据不一致。使用重试机制确保删除或更新操作成功。
- 问题2: 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值
- 例子:先删除缓存,再更新数据库。A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取,如果读取到旧值,就把旧值写入回缓存了
- 解决:延迟双删:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作(为了删线程B写的缓存)。
- 先更新数据库值,再删除缓存值。其他线程可能读取到旧值,但影响较少。
缓存雪崩
- 现象:缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
- 原因1 :缓存中有大量数据同时过期。
- 给数据的过期时间增加一个较小的随机
- 服务降级:如果业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息;
- 原因2 :Redis 缓存实例发生故障宕机
- 业务系统中实现服务熔断或请求限流机制;熔断时缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,减少数据库的压力。
- 事前预防:通过主从节点的方式构建 Redis 缓存高可靠集群
缓存击穿
- 现象:热点数据过期失效时,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
- 解决:
- 不设置过期时间
- 使用锁
缓存穿透
- 现象:缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。
- 解决:
- 缓存空值或缺省值
- 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。
- 步骤:
- 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
- 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
- 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。
- 问题:Redis布隆过滤器是使用String类型实现的,存储的方式是一个bigkey,建议使用时单独部署一个实例,专门存放布隆过滤器的数据,不要和业务数据混用,否则在集群环境下,数据迁移时会导致Redis阻塞问题
- 步骤:
- 前端进行请求检测,过滤恶意请求
缓存淘汰策略
- noeviction:不淘汰
- random:随机选择数据进行淘汰
- ttl:剩余存活时间最短的数据就会被淘汰出缓存
- LRU:只考虑数据的访问时效
- Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳;
- Redis 并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如 10 个)的数据放入候选集合,后续在候选集合中根据 lru 字段值的大小进行筛选。
- LFU:首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据
- 把原来24bit大小的lru字段,又进一步拆分成了两部分。前 16bit,表示数据的访问时间戳,后 8bit,表示数据的访问次数。
- 首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
- 计数规则:非线性计数法;counter值衰减机制,解决有些数据在短时间内被大量访问后就不会再被访问的问题