Redis八股文
数据结构
String类型
最基础、最常用,key - value
底层:
int:整数存int类型,省空间embstr:短字符串raw:长字符串
embstr和raw底层都会使用SDS来保存值,区别在于embstr只分配一次连续内存来保存redisObject和SDS;而raw会分配两次内存分别给redsObject和SDS
原因是embstr通常用于存储短字符串,而且是只读的,如果需要修改,redis会将其转化为raw类型再修改。因为如果字符串发生修改需要扩容,再用embstr就会导致redisObject和SDS整个内存都需要重新分配,因此长字符串通常用raw,便于修改和扩容
字符串在存储时不直接用C语言的字符串char*类型,而是SDS类型
1 | struct sdshdr { |
SDS存储二进制数据安全,因为使用len和free来判断字符串是否结束,而c语言使用\0判断,但二进制数据中可能存在\0SDS获取字符串长度O(1),c语言获取字符串长度O(n)SDS有自动扩容机制,不会溢出
使用场景:
- 缓存对象
- 计数:redis是单线程的,执行命令的过程是原子的
- 分布式锁:
SET key value NX EX ... - 共享session / 存token
List类型
底层使用quicklist,本质是 双向链表 + listpack
用双向链表连接节点,每个节点内部是listpack,listpack是一块连续的内存,可以存储多个数据,而不是每个节点只存储一个数据,避免了大量的指针内存开销
早期版本的redis,quicklist每个节点内部使用ziplist,新版改为listpack
原因是ziplist内部存储多个节点数据,每个节点除了存储自身的数据外,还要存储上一个节点的长度,这样可以通过内存地址减去长度来计算上一个节点的地址,便于反向遍历;然而这种设计存在连锁更新的问题,一旦前面某个节点的数据长度发生变化,所有后面节点的数据都要发生变化,向后移动;而新版本的listpack为了解决连锁更新问题,每个节点不再记录前面节点的长度,而是记录自身节点的长度
使用场景:
- 最新动态
- 评论列表
- 消息队列
Hash类型
key - value(field1 - value1; field2 - value2...)
底层:
listpack(旧版为 ziplist):小数据量,连续存储[name][kirito][age][18]hashtable:大数据量
对比hash和string:
- hash可以改单个字段,string改一个字段需要整个对象重新序列化
- 小对象场景,hash使用
listpack压缩存储,更省空间,而string存json需要额外引号,逗号等文本 - 取数据时hash反序列化时需要自己判断类型;而string可以直接反序列化整个对象,跨语言方便
使用场景:
- 存对象
Set类型
key - value(member1 member2 member3...)
member无序且唯一,支持交并差运算(SINTER / SUNION / SDIFF)
底层:
intset:小整数集合,本质是有序的去重数组,使用二分查找hashtable:一般情况
使用场景:
- 标签(去重)
- 共同好友(交集) / 推荐好友(差集)
- 抽奖(去重)
ZSet类型
在Set类型的基础上,每个member多了一个score属性,ZSet中的member保留了唯一的特点,但是可以根据score排序
底层使用hashtable + skiplist(哈希表 + 跳表),二者同时使用
hashtable:存member - score,根据member快速找到scoreO(1)skiplist:按照score排序,每个节点存储[score, member]
跳表结构
例如查找8:
- 在第二级索引从1开始找。查到13时,因为8 < 13,所以从7所在的节点开始向下往第一级索引查;
- 查到9时,因为 8 < 9,所以从7所在的节点开始向下往原始链表查;
- 最终在原始链表中找到了8。
查找,插入,删除都是O(log n),范围查询也十分方便
使用场景:
- 排行榜
- 延时队列:
member放任务,score放时间戳,按时间排序,定时扫描到期的任务
BitMap类型
位图,是一串连续的二进制位,可以通过偏移量定位元素,对最小单位bit设置0或1来表示状态
底层实际上是string类型,利用string字节数组的每个bit位表示元素的二值状态
非常节省空间,但不能存复杂数据
使用场景:
- 用户签到
- 用户是否在线
- 活跃用户统计
- 布隆过滤器(hash函数算出的hash值,在bitmap的对应位上标1)
例如:
统计在线人数
设置一个位图用来记录所有用户的在线状态,用户的id即为bitmap上的offset
id=1001的用户上线:
1
SETBIT online:users 1001 1
id=1001的用户下线:
1
SETBIT online:users 1001 0
检查id=1001的用户在线状态:
1
GETBIT online:users 1001
统计在线人数
1
BITCOUNT online:users // 返回位图中有多少个1
统计用户签到的次数
类似的 可以让每个用户每月一个bitmap
1
SETBIT sign:user:1001:202605 2 1 // 5月的2号用户签到一次
统计连续签到的用户数量
每天一个bitmap,用户的id即为bitmap上的offset
1
2
3
4SETBIT sign:day:20250501 1000 1 // id=1001的用户在5月1号签到了
SETBIT sign:day:20250502 1000 1
...
SETBIT sign:day:20250507 1000 1同一名用户在不同bitmap上的offset是一样的,都是自己的id
对这七天的bitmap做按位与
&,有多少个1则说明在这七天里连续签到的用户有多少;根据id查询offset,可以判断某用户在这七天里是否连续签到了1
BITOP AND destmap sign:day:20250501 sign:day:20250501 ... sign:day:20250507
HyperLogLog类型
用于基数统计,即统计集合中不重复元素的数量。底层基于概率统计,统计的是近似值,存在一定误差,而且只能统计数量,不能统计出具体有哪些元素
常用命令
PFADD:添加元素PFCOUNT:统计基数PFMERGE:合并多个HyperLogLog
GEO类型
GEO底层是ZSET,用于存储地点名称和经纬度,并通过GEOHASH编码,把经纬度编码为一个整数,并把该整数作为地点名称(member)的score
可以用于查询附近的人 / 店铺 / 车辆,以打车系统查询附近车辆为例:
1 | // 把id=33的车辆的经纬度信息存入GEO cars:locations 中 |
Stream类型
用于做redis实现的消息队列,比起List实现的简单消息队列,Stream消息队列支持唯一消息ID、消费组、Pending List、ACK、消息堆积查询等机制
TODO 后面复习消息队列补充详细说明
Redis为什么快?
- redis是基于内存的数据库,读写比磁盘IO快
- redis优化了高效的数据结构,比如
SDS, quicklist, skiplist - 单线程,避免了锁竞争和上下文切换(redis运行速度瓶颈不在CPU计算,而是网络IO和内存访问)(redis的单线程指执行命令单线程,但新版的redis在网络IO、持久化会有后台线程的参与)
- IO多路复用,用一个线程,通过epoll同时监听多个socket连接,只有当事件发生时才处理IO,而不是阻塞等待某一个连接
热Key与大Key
热key
在短时间内被高频访问的key
热key的问题
- 单点压力过大,对同一个key的所有请求都会集中到同一个redis实例,甚至是同一个CPU核心
- 影响其他请求,由于redis单线程,大量请求排队等待,表现为:
- 响应时间变长
- CPU占用飙升
- QPS下降
- 网络带宽打满
- 如果热点key过期,可能引发缓存击穿
如何发现热key
redis自带命令
monitor命令1
redis-cli monitor
实时打印redis正在执行的命令,观察哪些key访问频繁
但
monitor性能开销较大,线上不适合长时间使用,一般只用于临时排查hotkeys命令1
redis-cli --hotkeys
他会扫描并打印出访问频率较高的key
原理是基于LFU信息统计,因此只能用于LFU缓存淘汰策略
业务侧统计(更推荐)
在网关或业务层统计:
- 统计key访问次数
- 做TopN(前N个高频访问的key)
这种用法更推荐,因为redis自身不适合做全量热点统计
热key解决方案
本地缓存(最核心)
不要所有请求都打redis,先访问本地缓存
1
本地缓存 -> redis -> mysql
本地缓存直接访问当前进程内存,速度更快,并且可以有效降低redis压力;但分布式部署时需要考虑缓存一致性问题
热key拆分
如果某个key访问压力太大,可以把一个热key拆成多个副本key,让请求随机访问不同副本
1
2
3hot:user:1:0
hot:user:1:1
hot:user:1:2读取时随机选择一个副本key,写入时全量同步更新或者后台异步更新(会出现短暂不一致)
适合读多写少的场景;不适合强一致性场景(比如订单、库存等)
大key
value内存占用特别大的key
大key的问题
阻塞redis
redis单线程,读写大key可能会造成长时间阻塞
网络传输开销大
一次读写大量数据可能会占满带宽、增加延迟
内存倾斜
Redis Cluster中的一个slot特别大,导致某个节点的内存占用特别大
删除危险(高频)
删除大key时会释放大量内存,开销很大,导致主线程长时间阻塞,影响其他请求
如何发现大key
bigkeys命令1
redis-cli --bigkeys
它会返回每种数据类型中元素数量最大的key,但对于String类型主要看字符串长度,对于集合类型主要看元素数量,不一定完全等于真实内存占用
MEMORY USAGE命令1
MEMORY USAGE key
可以查看某个key实际占用的内存大小
使用
SCAN渐进式扫描1
SCAN cursor MATCH pattern COUNT 100
不建议线上使用
KEYS *全量扫描,会阻塞redis。可以使用SCAN分批遍历key,再配合MEMORY USAGE判断key大小
大key解决方案
拆分大key(最标准)
把一个大key拆成多个小key,避免单个key过大
1
2user:posts:1001
=> 存10万条帖子拆分:
1
2user:posts:1001:1
user:posts:1001:2分页存储
压缩数据
用protobuf / msgpack代替json做序列化,减少value size
使用合适的数据结构
例如不要用String类型存大数据,而是按字段拆分存hash,hash支持局部读写
异步删除(高频)
删除大key时不要直接使用
DEL,可以使用UNLINK异步释放内存1
UNLINK bigkey
UNLINk主线程只会解除引用,由后台线程异步释放内存
缓存穿透、击穿、雪崩
缓存穿透
请求频繁查询一个不存在的数据
1 | redis miss -> mysql miss -> 返回空 |
redis缓存未命中,导致大量请求打到数据库,造成压力
解决方案
缓存空值
例如在第一次访问
id=99的user,redis缓存未命中,查询数据库数据也不存在,此时可以让redis缓存一个带过期时间的空值1
SET user:99 null EX 60
最简单有效的方法;但是会占用一部分内存
布隆过滤器
在请求进入redis / mysql查询之前,先用布隆过滤器判断请求的内容是否可能存在,拦截大部分非法请求
布隆过滤器的底层可以理解为一个很长的
bitmap,以及多个不同的hash函数。把数据库中已经存在的key提前写入布隆过滤器,先对key执行多次hash计算,得到多个下标,然后把bitmap中对应位置都标记为11
2key -> hash1(key), hash2(key), hash3(key)
-> bitmap中hash1 hash2 hash3位置标记为1查询时也对请求的key执行相同的hash计算:
- 如果对应位置中有任意一个为0,说明这个key一定不存在,直接拦截请求,不再查询redis和mysql
- 如果对应位置全部为1,说明这个key可能存在,再继续查询redis / mysql
布隆过滤器的特点:
- 不会漏判,只要真实存在的数据已经写入布隆过滤器,就不会被判断为不存在
- 存在误判,不存在的数据也可能穿过布隆过滤器,因为不同key经过hash后可能映射到相同位置
布隆过滤器通常适合读多写少、数据稳定的场景。如果数据库新增了数据,需要同步把新key加入布隆过滤器;如果删除了数据,普通布隆过滤器不支持直接删除,可能需要使用计数布隆过滤器或者定期重建
缓存击穿
某个热点key的redis缓存过期,导致大量请求同时打到数据库,造成压力
解决方案
互斥锁
多个线程竞争互斥锁,只有一个线程能拿到锁,查询数据库并回写缓存,其他线程等待并重新查询缓存
热点数据永不过期
热点数据不设置TTL,由后台线程定时更新缓存
逻辑过期
热点缓存数据里额外带一个逻辑过期时间,业务线程查询数据时需要根据逻辑过期时间判断数据是否过期,若未过期就返回缓存值,若过期先返回旧值,由抢到锁的线程开启后台线程异步更新缓存
请求合并(singleflight)
同一时刻的多个相同请求只允许一个真正执行加载数据,其他请求复用这个加载结果
分布式场景使用有局限性,只能合并同一个机器上的请求
多级缓存
多级缓存是指在redis之外再增加本地缓存等,让请求不只依赖单层redis缓存
缓存雪崩
大量key缓存在同一时间失效,或者redis宕机,导致大量请求无法命中缓存打到数据库,造成压力
解决方案
过期时间加随机值
1
SET key value EX 3600 + random(1, 300)
避免大量key同时过期
缓存预热
系统启动前,提前把热点数据加载到redis中,避免系统刚上线时大量请求直接查询数据库
redis集群高可用
如果缓存雪崩是由于redis宕机导致的,就需要从架构上提高redis可用性,避免单点故障
常见方案:
- 主从复制:主节点负责写,从节点复制数据,提高读能力和容灾能力
- 哨兵机制:监控redis主节点状态,主节点故障时自动选举新的主节点
- Redis Cluster:通过分片把数据分散到多个节点,同时支持故障转移
限流、熔断、降级
当缓存雪崩已经发生时,此时需要通过限流、熔断、降级保护数据库
- 限流:限制单位时间内进入系统的请求数量,超过阈值的请求直接拒绝或排队
- 熔断:如果数据库或redis异常严重,后续请求立即失败,不再调用下游
- 降级:返回兜底数据、默认值、静态页面,或者关闭非核心功能
Redis分布式锁
基础锁用法
获取锁
获取锁本质是redis的SET操作,EX给锁设置超时自动释放;NX确保了互斥性,只有一个线程能获得锁;同时value需要设置为一个随机生成的token,用于释放锁时做校验
1 | SET lock token NX EX 10 |
释放锁
释放锁本质是redis的DEL操作,但删除之前需要做token校验,确保删除的是自己的锁,这个“先判断,再删除”的过程需要用Lua脚本保证原子性
1 | if redis.call("GET", KEYS[1]) == ARGV[1] then |
为什么value要用随机token?
因为删除时要用token做校验,防止误删别人的锁
如果不用随机token,例如线程A获取锁,但业务还没执行完锁就超时自动释放了,于是线程B成功获取同一把锁。之后线程A业务执行完毕,如果直接DEL就会把线程B的锁误删掉
为什么释放锁需要Lua脚本?
释放锁需要先判断token,再删除key,这两个操作必须保证原子性
如果不用Lua脚本,而是在客户端分两步执行:
1 | GET lock |
例如线程A先GET lock,发现token是自己的;但还没来得及DEL,锁过期了,线程B获取到了新锁。此时线程A继续执行DEL lock,就会把线程B的锁删掉
redis会把整个Lua脚本作为一个原子性的命令执行,由于redis是单线程的,因此redis执行Lua脚本的过程是原子性的
锁续期
如果业务执行时间超过了锁的过期时间,锁会提前自动释放。此时其他线程可能获取到同一把锁,导致多个线程并发执行业务逻辑
解决方案
线程获取锁成功之后,启动一个后台线程定期校验token并给锁续期(这个过程也需要Lua脚本确保原子性),即看门狗机制。但如果主线程挂了,为了防止看门狗线程一直给锁续期,导致锁无法超时释放,在go中,可以通过context和defer cancel()来管理看门狗线程的生命周期;但如果业务线程出现死循环或长期阻塞,看门狗线程可能仍然会续期,因此可能还需要给看门狗线程设置最大续期次数等兜底机制来避免锁长期不释放
看门狗不是万能的,看门狗续期依赖网络连接。如果客户端和 Redis 之间网络断了 / 存在波动,续期失败,锁依旧可能会过期。而你的业务代码可能还在本地跑着,完全不知道锁已经没了
RedLock(了解即可)
RedLock是为了解决单个Redis节点做分布式锁时的单点故障问题。比如线程A在主节点加锁成功,但锁还没同步到从节点,主节点就宕机了,从节点被提升为新主节点,此时由于新主节点中没有线程A的加锁数据,线程B获取锁也能加锁成功,导致两个线程同时持有锁
RedLock的思路是准备多个相互独立的Redis节点,多数节点都成功才算加锁成功,但存在网络分区和时钟问题,大部分业务使用普通的单redis锁即可
缓存更新策略
先删除缓存,再更新数据库(一致性问题严重)
会出现的问题:
- 线程A先删除缓存,还没来得及更新数据库
- 线程B查询缓存,发现缓存为空;于是查询数据库,将旧数据回写到缓存中
- 线程A此时再更新数据库
结果redis缓存与数据库内容不一致
由于线程A更新数据库耗时较长,这段时间窗口内有很大的概率线程B完成查询数据库并回写缓存的操作,存在严重的一致性问题
先更新数据库,再删除缓存(常用)
会出现的问题:
- 线程A先查询缓存,发现缓存为空;于是查询数据库,但还没来得及把数据回写到缓存
- 线程B到来,更新了数据库并完成了删除缓存的操作
- 线程A再把旧数据回写到缓存
结果redis缓存与数据库内容不一致
由于线程A回写缓存耗时较短,这段时间窗口内线程B到来,完成更新数据库并回写缓存的概率较低,所以几乎不会出现缓存不一致的问题;但在一个线程更新数据库的期间,如果有其他线程查询可能会查询到redis缓存中的旧值,会有短暂的数据不一致问题
延迟双删(重点)
基本流程:
1 | 删除缓存 |
为什么需要第二次删除?
因为第一次删除缓存后,可能有其他线程查询缓存发现为空,于是查询数据库并把旧值写回缓存。此时如果当前线程更新数据库完成后不再处理缓存,redis中就可能一直保存旧值
为什么要延迟?
延迟是为了等待其他线程完成“查询数据库 -> 写回缓存”的过程。如果不延迟,第二次删除可能发生得太早,线程B还没来得及把旧值写回缓存,等第二次删除结束后线程B再写入旧值,仍然会出现缓存不一致
延迟时间一般要大于一次读请求的耗时,比如:
1 | 读数据库耗时 + 写缓存耗时 + 一点冗余时间 |
一般为几百毫秒~几秒
延迟双删不能保证强一致,只是降低缓存不一致的概率
如果想要保证强一致性,一个简单的策略是利用互斥锁,线程先获取锁,然后删除缓存,更新数据库,还可以选择把新数据主动回写到缓存,然后释放锁,保证整个操作是原子性的,其他线程在读取前也要先获取锁,就不会出现缓存不一致的问题
缓存淘汰策略
当redis内存达到maxmemory限制时,需要根据配置的缓存淘汰策略决定是否淘汰key,以及淘汰哪些key
Redis常见的八大缓存淘汰策略:
| 淘汰策略 | 含义 | 适用场景 |
|---|---|---|
noeviction |
不淘汰任何key,内存满后写入新数据会报错 | 数据不能丢失,希望由业务自己处理写入失败 |
volatile-lru |
只从设置了过期时间的key中,淘汰最长时间未使用的key | 只希望淘汰缓存数据,不希望影响没有设置过期时间的核心数据 |
allkeys-lru |
从所有key中,淘汰最长时间未使用的key | 通用缓存场景,热点数据会被保留,最常用 |
volatile-lfu |
只从设置了过期时间的key中,淘汰访问频率最低的key | 希望保留高频访问数据,同时只淘汰设置了过期时间的key |
allkeys-lfu |
从所有key中,淘汰访问频率最低的key | 热点访问特征明显,希望长期保留高频访问数据 |
volatile-random |
只从设置了过期时间的key中,随机淘汰key | 对访问热度不敏感,只要求实现简单 |
allkeys-random |
从所有key中,随机淘汰key | 所有key访问概率接近,或者对淘汰结果不敏感 |
volatile-ttl |
只从设置了过期时间的key中,淘汰剩余过期时间最短的key | 希望优先淘汰快过期的数据 |
一般后端缓存场景中,如果redis只作为缓存使用,比较常用的是allkeys-lru或allkeys-lfu
allkeys-lru:适合短期热点allkeys-lfu:适合长期热点
Redis持久化
为什么redis需要持久化?
- redis是内存数据库,数据默认存在内存里。如果redis宕机,机器重启,内存里的数据就会丢失
- 如果redis不仅做缓存,还存放了重要的业务数据,就需要持久化机制把数据落到磁盘
redis的持久化有RDB与AOF两种方式
RDB(Redis Database)
RDB是在某个时间点把redis内存中的数据做成快照写入到磁盘,生成的文件默认叫dump.rdb
触发方式
手动触发
1
2SAVE
BGSAVESAVE:主线程执行快照保存,会阻塞redis,线上基本不用BGSAVE:redis会fork一个子进程,由子进程负责生成RDB文件,主进程继续处理请求
自动触发
可以通过配置文件设置自动触发条件:
1
2
3save 900 1
save 300 10
save 60 10000表示在指定时间内,如果发生了指定次数的数据修改,就自动触发
BGSAVE
工作原理
RDB通常通过BGSAVE生成快照,大致流程:
1 | redis主进程执行fork |
这里用到了操作系统的**写时复制(Copy On Write)**机制。fork之后,父子进程刚开始共享同一份物理内存。生成RDB期间,如果主进程修改了某个页面,操作系统才会复制这个页面,让父子进程看到不同的数据
因此RDB生成的是fork那一刻的数据快照,而不是生成过程中实时变化的数据
优点
- RDB文件是紧凑的二进制快照,体积小
- 直接加载快照数据,恢复快
缺点
- RDB是间隔一段时间生成一次快照;如果两次快照之间redis宕机,这段时间的数据会丢失
fork子进程会带来额外内存开销
AOF(Append Only File)
AOF是把redis执行过的写命令追加到AOF文件中。redis重启时,通过重新执行AOF文件中的写命令来恢复数据
工作流程
1 | 客户端执行写命令 |
优点
- 数据安全性高,丢失数据窗口比RDB小
- AOF文件记录的是写命令,可读性比二进制快照更好
缺点
- AOF文件通常比RDB文件更大
- 恢复速度通常比RDB慢,因为需要重新执行大量写命令
- AOF文件可能不断膨胀,需要AOF重写来压缩体积
AOF三种刷盘策略
| 策略 | 含义 | 优缺点 |
|---|---|---|
appendfsync always |
每次写命令都刷盘 | 性能差,数据安全性高 |
appendfsync everysec |
每秒刷盘一次 | 最常用,性能和数据安全性折中 |
appendfsync no |
由操作系统决定什么时候刷盘 | 性能好,但最危险,可能丢失大量数据 |
一般生产环境常用everysec
AOF重写策略
如果AOF文件不断追加,文件会越来越大,所以redis提供了BGREWRITEAOF 机制,fork一个子进程,子进程会用更精简的命令重新生成一份新的AOF文件,替换旧AOF文件。例如对同一个 key 的多次 INCR、SET,重写后可以合并成更少的命令,减少文件体积
AOF重写可以手动触发,也可以自动触发:
1 | BGREWRITEAOF |
新版本redis支持AOF混合持久化:重写AOF时,前半段使用RDB快照保存全量数据,后半段再追加写命令;可以同时利用RDB恢复快,AOF丢失少的优点
RDB vs AOF
| 对比 | RDB | AOF |
|---|---|---|
| 数据安全性 | 较弱 | 较强 |
| 文件大小 | 通常较小 | 通常较大 |
| 恢复速度 | 较快 | 较慢,需要重放命令 |
| 性能影响 | 小 | 大 |
最佳实践:
1 | RDB + AOF混合持久化 |
如果同时开启RDB和AOF,redis重启时会优先使用AOF文件恢复,因为它通常更完整
Redis集群
主从复制
主从复制是指一个redis主节点(master)可以把数据复制到多个从节点(replica / slave)
1 | 客户端写入 -> master |
主从复制主要实现:
- 数据备份:主节点数据同步到从节点,主节点故障后从节点还有数据副本
- 读写分离:主节点负责读写,从节点只负责读,提升读能力
工作原理
全量同步
从节点第一次连接主节点时,会进行全量同步
1
2
3
4
5
6
7从节点发送同步请求
↓
主节点生成RDB快照
↓
主节点把RDB文件发送给从节点
↓
从节点加载RDB文件主节点在生成和发送 RDB 期间,并不会停止接收写请求,而是把新增写命令先写进复制缓冲区。RDB 发送完成后,主节点再把缓冲区里的增量命令补发给从节点
增量同步
全量同步完成后,主节点后续执行的写命令会持续发送给从节点,从节点执行这些命令,这个过程也叫命令传播
主从之间可能会发生短暂短线,并不是每次断线都要重新做全量同步,redis会尽量做增量同步,为了处理主从之间短暂断线的情况,Redis引入了两个重要概念:
- 复制偏移量:主节点和从节点都会维护一个偏移量,主节点每传播一批写命令,自己的偏移量会增加;从节点每接收一批写命令,自己的偏移量也会增加
- 复制积压缓冲区:主节点维护的一块固定大小的环形缓冲区,用来保存最近传播过的一部分写命令
例如主节点当前复制偏移量是10000,从节点因为网络断开,只同步到了9000。等从节点重新连接后,会告诉主节点:“我上次同步到9000”
1
2
3主节点当前偏移量:10000
从节点当前偏移量:9000
从节点重连后请求:从9001开始补给我如果主节点的复制积压缓冲区里还保留着
9001 ~ 10000这段命令,主节点就只需要把这部分命令补发给从节点,也就是做增量同步如果断线时间太长,主节点的复制积压缓冲区已经把这段历史命令覆盖掉了,就无法增量同步,只能重新做一次全量同步
注意
- 主从复制是异步进行的,最终一致,但不是强一致性。读取时从节点可能还没有完全同步主节点的数据
- 需要设置合理的复制挤压缓冲区大小,避免频繁全量同步;由于全量同步需要
fork子进程生成RDB、网络传输、走磁盘;对大实例有明显开销 - 单纯主从复制无法确保高可用,主节点宕机系统无法恢复
哨兵 Sentinel
哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。哨兵节点是一种特殊的Redis节点,不负责存储数据,它会监控主节点和从节点的运行状态。当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移
如何判断master下线?
哨兵会定期向master发送心跳。如果某个哨兵在规定时间内没有收到master的有效回复,就会认为master不可用,这叫主观下线
但单个sentinel可能因为网络抖动误判,所以这个sentinel会询问其他sentinel,只有当达到规定数量的sentinel都认为master故障,才会认为master确实不可用,即客观下线
故障转移流程
master被判定为客观下线后,sentinel之间会选出一个leader,负责这次故障转移;为了确保只会选出一个leader,sentinelc采用过半票选的机制,当某个sentinel拿到多数票后才会成为leader
leader会从replica中选取一个最合适的节点成为新的master,并通知其他节点和客户端
为什么至少需要三个Sentinel?
- 判断master故障需要多个sentinel,避免单个sentinel误判
- 进行故障转移时需要多数派投票选举leader,负责这次故障转移
Redis Cluster
Redis Cluster是redis官方提供的分布式集群方案,核心是数据分片 + 高可用,解决redis单机内存容量问题
它把所有key划分到16384个哈希槽(slot)中,每个master节点负责一部分slot
1 | key -> CRC16(key) % 16384 -> slot -> 对应master节点 |
每个master通常还会配置一个或多个replica。当某个master故障时,它的replica可以被提升为新的master,继续负责原来的slot
请求路由
客户端访问某个key时,会根据key计算slot。如果请求发送到了错误节点,redis会返回重定向信息,让客户端去正确的节点访问
常见重定向:
MOVED:slot已经固定迁移到其他节点,客户端需要更新本地slot缓存ASK:slot正在迁移过程中,客户端临时访问目标节点
说些什么吧!