Redis
数据类型
常见物种:String,Hash,List,Set,ZSet
后续又支持:BitMap、HyperLogLog、GEO、Stream
String
value不仅可以是字符串也可以是数字,value最多可以容纳的数据长度是512M。
底层实现:int和SDS(简单动态字符串)
字符串对象的内部编码有三种:int、raw、embstr
- 字符串对象为整数值且能用long类型表示:int
- 字符串对象为字符串且长度小于等于32字节(短字符):embstr
- 长字符:raw
常见命令:
SET key value
GET key
EXISTS key
STRLEN key
DEL key
MSET key1 value1 key2 value2
MGET key1 key2
SET number 0
INCR number # 加一
INCRBY number 10 # 加十
DECR number # 减一
DECRBY number 10 # 减十
EXPIRE name 60 # 设置过期时间60s
TTL name # 查看还有多久过期
SET key value EX 60 #设置键时设置过期时间
SETEX key 60 value # 等效写法
SETNX key value # 不存在则插入
应用场景:
- 缓存对象
- 常规计数(计算访问次数、点赞转发、库存数量)
- 分布式锁:set nx参数可以实现key不存在才插入:set key unique_value nx px 10000(PX单位是毫秒,EX单位是秒)
-
共享Session信息:多服务器情境下将Session信息集中存放在Redis
-
SDS不仅可以保存文本数据,还可以保存二进制数据(图片、音视频、压缩文件)
- SDS获取字符串长度的时间复杂度是O(1):维护
len
属性 - Redis的SDS API是安全的,拼接字符串不会造成缓冲区溢出:拼接前会检查空间是否满足要求,空间不够会自动扩容
-
embstr优势与缺点(与raw相比)
-
创建对象内存分配次数2次降为1次;释放对象也调用一次内存释放函数;所有数据保存在连续的内存,更好的利用CPU缓存
-
字符串长度增加需要重新分配内存时,整个对象和sds都要重新分配空间(因此embstr字符串对象实际上是只读的)
-
分布式锁解锁的过程就是将key删除,要保证操作的客户端是加锁客户端(unique_value),通过lua脚本保证解锁的原子性
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
List
字符串列表,按照插入顺序排序,可以从头部/尾部插入,最大长度2^32-1
内部实现:quicklist(链表+数组的实现形式,每个节点是一个ziplist(数组),结点之间是双向链表)
常用命令:
LPUSH/RPUSH key value [value ...] #从表头/表尾插入一个/多个元素
LPOP/RPOP key # 移除表头/表尾元素
LRANGE key start stop # 返回指定区间元素
BLPOP/BRPOP key timeout # 从key列表表头/表尾弹出元素,没有就阻塞timeout秒,如果timeout是0则一直阻塞
应用场景:
-
消息队列(必须满足消息保序、处理重复的消息、保证消息可靠性)
-
LPUSH+RPOP或RPUSH+LPOP(BRPOP在没有数据时会阻塞等待,节省CPU开销)--保序
- 给消息加上全局ID --防止处理重复消息
- BRPOPLPUSH:让消费者程序从一个List中读取消息的同时,把这个消息再插入另一个List留存 --保证消息可靠性
-
缺陷:
-
不支持多个消费者消费同一条消息(不支持消费组的实现)
Hash
键值对集合,适合存储对象。
内部实现:
- 哈希类型元素个数小于512个,所有值小于64字节,使用listpack(7.0之后)/压缩列表(7.0之前)作为底层数据结构
-
否则,使用哈希表作为底层数据结构(数组+链地址法)
-
元素个数大于哈希桶数量-扩容;元素个数减少-缩容
-
渐进式rehash:(到达扩缩容阈值时触发,进入rehash状态)
-
有两个hash表
-
ht[0]:当前正在用的旧表
-
ht[1]:新申请的表
-
开始hash时:
-
分配一个新表ht[1]
- 设置rehash index,从0开始
- 每次处理请求时,顺带迁移ht[0]中一部分(一个下标上的整个链表/桶)的数据到ht[1]
- rehash index逐步递增,直到ht[0]为空(rehash index到数组末尾),最后把ht[1]替换为ht[0](ht[0]的指针指向ht[1],ht[1]的指针置为null,rehash index设置为-1),
常用命令:
HSET key field value
HGET key field
HMSET key field1 value1 field2 value2
HMGET key field1 field2
HDEL key field1 [field2]
HLEN key
HGETALL key
应用场景:
- 存储对象
- 购物车(用户id为key 商品id为field 数量为value)
Set
无序不重复列表
底层实现:
- 元素都是整数且个数小于512个,使用整数集合作为底层数据结构
- 否则使用hash表作为底层数据结构
常用命令:
SADD key member [member2 ...]
SREM key member [member2 ...]
SMEMBERS key # 获取集合所有元素
SCARD key # 获取集合个数
SISMEMBER key member # 判断member是否在集合key中
SRANDMEMBER key [count] # 随机返回count个元素
SPOP key [count] # 随机删除count个元素
SINTER key [key1 ...] #交集,结果存入key
SINTERSTORE destination key [key1 ...] #将交集结果存入新的集合destination
SUNION key [key1 ...] # 并集
SUNIONSTORE destination key [key1 ...] #将并集结果存入新的集合destination
SDIFF key [key1 ...] #差集
SDIFFSTORE destination key [key1 ...] #将差集结果存入新的集合destination
应用场景:
- 点赞 (SADD、SREM、SMEMBERS、SCARD)
- 共同关注(SINTER)
- 抽奖活动(SRANDMEMBER/SPOP)
Set集合运算的复杂度高,数据量大的情况下,如果直接执行计算会导致Redis实例阻塞,因此可以选择从库进行聚合统计,或把数据返回给客户端,由客户端完成聚合统计,避免直接使用主库
ZSet
相比Set,多了一个排序属性score
内部实现:
- 有序集合的元素个数小于128个,并且每个元素的值小于64时,使用压缩列表(7.0之前)/listpack作为底层数据结构
- 否则使用跳表作为底层数据结构
常用命令:
ZADD key score member [[score member] ...]
ZREM key member
ZSCORE key member
ZCARD key
ZINCRBY key increment member
ZRANGE key start stop [WITHSCORES] # 正序获取有序集合key从start到stop下标的元素,可选返回分数
ZREVRANGE key start stop # 逆序
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] # 返回有序集合中指定分数区间内的成员,分数由低到高排序
# Zset不支持差集运算
应用场景:
- 排行榜(ZINCRBY、ZSCORE、ZRANGE、ZREVRANGE)
- 电话、姓名排序(ZRANGEBYLEX、ZREVRANGEBYLEX)
BitMap
位图,适合数据量大且使用二值计算的场景。
内部实现:使用String类型作为底层数据结构
应用场景:
- 签到统计(SETBIT、GETBIT、BITCOUNT)
- 判断用户登录态(SETBIT、GETBIT、BITCOUNT)
HyperLogLog
一种概率型数据结构,用于基数统计,特点:占用极少内存,每个HyperLogLog指需要12KB就能统计接近2^64个不同元素
优点:
- 内存超小:不管你存 1k、1亿,最多 12KB 内存。
- 操作简单:三个命令搞定(PFADD / PFCOUNT / PFMERGE)。
- 适合大数据场景:做去重计数时非常高效。
缺点:
- 有误差:统计不是精确值,误差大约 ±0.81%。
- 不可获取原始数据:只能得到数量,不能得到去重后的元素集合。
应用场景:(PFADD、PFCOUNT)
- 网站的 UV(独立访客数) 统计;
- 日志系统的 独立 IP 数量 统计;
- 广告平台的 触达用户数 估算;
- 大数据场景下的快速基数估计。
GEO
Redis GEO 命令是基于 Sorted Set (ZSet) 实现的:
- 元素(member)= 位置的名称(如用户ID、地点名)。
- score = 对应位置的 经纬度编码(GeoHash 转换成 52 位整数)。
应用场景:
- 附近的人/店(LBS):存储用户或商户的经纬度,查询半径范围内的目标。
- 打车/外卖:找出一定范围内的骑手/司机。
- 物流调度:计算仓库与用户的距离,分配最近仓库。
Stream
- Redis 的 Stream 类型 = 可持久化的消息队列。
- 它类似 Kafka/RabbitMQ 这种消息流,但更轻量。
- 支持 生产者写入、消费者读取、消费分组、消息确认 等特性。
可以把它理解为 日志型消息队列,每条消息带有:
- 一个 唯一 ID(有序、全局唯一);
- 一个 消息体(键值对)。
应用场景:
- 消息队列:生产者-消费者模型,支持分组、确认。
- 日志收集:存储业务日志、操作流水。
- 实时数据处理:结合
XREAD BLOCK
做实时计算。 - 事件通知:比如订单状态变更通知下游服务。
Redis Stream = 轻量级队列,上手快、集成方便,但缺少专业 MQ 的 大规模存储、复杂消费模型、强一致性保障。如果需求是简单队列或中小规模,用 Stream 足够;如果是金融级、日志分析级别,就要用 Kafka / RocketMQ。
持久化
AOF Append Only File
只会记录写操作命令,读操作命令不会被记录。
先执行写操作再将命令追加到AOF日志(避免额外的检查开销;不会阻塞当前写操作命令的执行)(有丢失的风险;可能会给下一个命令带来阻塞)
Redis执行完写操作命令后,会将命令追加到server.aof_buf缓冲区,然后通过write系统调用,将aof_buf缓冲区的数据写到AOF文件,具体内核缓冲区的数据什么时候落盘由内核决定。
AOF写回策略(redis.conf文件中的appendfsync,调用fysnc函数的时机选择)
- appendfsync=Always,每次写操作执行完后同步将AOF落盘(高可靠,持久化大Key可能会阻塞主线程)
- appendfsync=Everysec,每次写操作执行完后,先将命令写入到AOF文件的内核缓冲区,每隔一秒落一次盘(折中,宕机丢失1秒数据)
- appendfsync=No,由操作系统决定何时将缓冲区内容落盘(高性能)
AOF重写机制
Redis为了避免AOF文件过大,提供了AOF重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启用AOF重写机制,压缩AOF文件。
AOF重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的AOF文件,等到全部记录完后,就将新的AOF文件替换掉现有的AOF文件。(即使某个键值对被反复修改,最终也只需要根据这个键值对当前的最新状态用一条命令去记录,减少了命令的数量)
-
为什么不能直接复用现有的AOF文件
-
如果AOF重写过程中失败了,现有的AOF文件不会被污染
AOF后台重写
Redis的重写AOF过程是由后台子进程bgrewriteaof来完成的:
- 子进程进行重写期间,主进程可以继续处理命令请求,避免阻塞主进程
- 子进程带有主进程的数据副本:创建子进程时,父子进程是共享内存数据的,不过共享的内存只能以只读的方式,当父子进程任意一方修改了共享内存,就会发生写时复制,父子进程就有了独立的数据副本,就不用加锁保证安全
子进程怎么拥有主进程一样的数据副本(写时复制)
- 主进程通过fork系统调用生成bgrewriteaof子进程,os把主进程的页表复制一份给子进程(两者的虚拟空间不同,但是物理空间相同),这样子进程就共享了父进程的物理内存数据了,能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读
- 父进程/子进程向这个内存发起写操作时,CPU会触发写保护中断,os会在写保护中断处理函数里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读取权限设置为可读写
如果写时复制阶段修改的是一个bigkey,复制物理内存数据的过程就会比较耗时,有阻塞主进程的风险
在重写AOF期间,当Redis执行完一个写命令后,它会同时将这个写命令写入到AOF缓冲区和AOF重写缓冲区。
当子进程完成AOF重写工作后,会向主进程发送一条信号,主进程收到信号后,会调用一个信号处理函数:
- 将AOF重写缓冲区的所有内容追加到新的AOF的文件中
- 新的AOF文件进行改名,覆盖现有的AOF文件
整个AOF后台重写过程中,除了发生写时复制时会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候AOF后台重写都不会阻塞主进程。
RDB快照
Redis提供了两个命令来生成RDB文件,分别是save和bgsave,区别在于是否在主线程里执行:
- save会在主线程生成RDB文件,如果写入RDB文件的时间太长,会阻塞主线程
- bgsave会创建一个子进程来生成RDB文件,可以避免主线程的阻塞
RDB快照的缺点:在服务器发生故障时,丢失的数据会比AOF持久化的方式更多,因为RDB快照是全量快照的方式,因此执行的频率不能太频繁,AOF日志可以以秒级的方式记录操作命令,丢失的数据相对更少。
执行bgsave过程中,依然可以继续处理操作命令,关键技术就是写时复制。发生了写时复制后,RDB快照保存的是原本的内存数据。如果系统恰好在RDB快照文件创建完毕后崩溃了,那么Redis将会丢失主线程在快照期间修改的数据。
如果所有的共享内存都被修改,内存占用会是原来的两倍(写时复制)
混合使用AOF日志和RDB快照 (aof-use-rdb-preamble yes)
开启混合持久化后,在AOF重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以RDB方式写入到AOF文件,然后主线程处理的操作命令会被记录在重写缓冲区,重写缓冲区的增量命令以AOF的方式写入到AOF文件,写入完成后通知主进程将新的还有RDB格式和AOF格式的AOF文件替换旧的AOF文件。(前半部分时RDB格式的全量数据,后半部分是AOF格式的增量数据)--加载速度会很快,数据更少丢失
大key除了会影响持久化外(fork、写时复制时阻塞),还可能导致客户端超时阻塞、引发网络拥塞、阻塞工作线程、redis节点内存分布不均
策略
过期删除策略
当给key设置了过期时间,Redis会把该key带上过期时间存储到过期字典。过期字典实际上是哈希表,可以在O(1)的时间复杂度内查找key的过期时间。当我们查询一个key时,Redis首先检查该key是否存在与过期字典中:
- 不存在,正常读取键值
- 存在,获取key的过期时间,和当前系统时间进行比对,没过期正常返回
常见过期策略有:
- 定时删除:设置key的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器执行key的删除操作(优点:保证过期key尽快删除,内存尽快释放;缺点:较多过期key情况下,删除key可能占用相当一部分cpu)
- 惰性删除:不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key(优点:使用很少系统资源,CPU友好;缺点:如果一个key已经过期,仍然会保留在内存中,只要这个key一直没有被访问,占用的内存就不会释放,空间浪费,内存不友好)
- 定期删除:每隔一段时间从数据库中随机取一定数量key进行检查,删除其中过期key(优点:通过限制删除操作执行的时长和频率,来减少删除操作对CPU的影响,同时也能删除一部分过期的数据减少无效内存占用;缺点:内存清理方面没有定时删除效果好,同时也没有惰性删除使用的系统资源少,难以确定删除操作执行的时长和频率。)
Redis选择惰性删除和定期删除两种策略配合使用。
-
Redis在访问/修改key之前会调用expireIfNeeded函数对key进行检查:
-
如果过期,删除key(根据lazyfree_lazy_expire字段决定异步删除还是同步删除),然后返回null
-
没过期,正常返回
-
Redis每隔一段时间随机从数据库中取出一定数量key进行检查,删除过期key
-
从过期字典中随机抽取一定数量key
- 检查是否过期,删除已过期
- 已过期数量超过1/4,继续步骤a,否则停止,等待下一轮检查
为了保证定期删除不会出现循环过度导致线程卡死,增加了定期删除循环流程的时间上限
内存淘汰策略
Redis内存淘汰策略有八种,大体分为不进行数据淘汰和进行数据淘汰两类:
-
不进行数据淘汰策略
-
noeviction(3.0之后默认使用):当运行内存超过最大设置内存时,不淘汰任何数据,禁止写入
-
进行数据淘汰的策略
-
在设置了过期时间的数据中进行淘汰
-
volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- colatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值
-
volatile-lfu:淘汰所有设置了过期时间的键值中最少使用的键值
-
在所有数据范围内进行淘汰
-
allkeys-random:随机淘汰任意键值
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-lfu:淘汰整个键值中最少使用的键值
使用config get maxmemory-policy命令查看当前Redis的内存淘汰策略
传统LRU算法问题:
- 需要用链表管理所有的缓存数据,带来额外空间开销
- 当有数据被访问时,需要在链表上把数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,耗时,降低Redis性能
Redis实现了一种近似LRU算法,在Redis的对象结构体中添加一个额外字段,记录最后一次访问时间。当Redis进行内存淘汰时,会采用随机采样的方式来淘汰数据,随机取n个淘汰最近没有使用的那个。
- 优点:不用为所有数据维护一个大链表,节省空间占用;不用在每次数据访问时都移动链表,提升缓存性能
- 缺点:无法解决缓存污染问题,比如一次读取大量数据,这些数据只会被读一次,但是会留存在redis缓存中很长一段时间
在LFU算法中,Redis对象中的lru字段高16位存储访问时间戳(ldt),低8位存储访问频次(logc),logc会随时间推移而衰减。
高可用
主从复制
避免单点故障:主从复制,主从服务器之间采用读写分离的方式。使用replicaof(5.0之前使用slaveof)命令形成主从关系。
第一次同步
-
建立链接、协商同步:
-
从服务器就会给主服务器发送psync命令,表示要进行数据同步。psync命令包含两个参数,分别是主服务器的runID(每个redis服务器启动时都会自动生产一个随机ID唯一标识自己)和复制进度offset。初始值分别为?和-1。
-
主服务器收到psync命令后会用FULLRESYNC做为响应命令返回给对方,会带上两个参数:主服务器的runID和主服务器当前的复制进度offset。从服务器收到响应后会记录这两个值。FULLRESYNC是全量复制。
-
主服务器同步数据给从服务器
-
主服务器执行bgsave命令生成RDB文件
- 把RDB文件发送给从服务器。
- 从服务器收到RDB文件后会先清空当前数据,然后载入RDB文件。
主服务器会在这三个阶段中将收到的写操作命令写入到replication buffer缓冲区
-
主服务器发送新写操作命令给从服务器
-
收到RDB载入完成的消息后,主服务器将replication buffer缓冲区里所记录的写操作命令发送给从服务器,从服务器执行发来的命令
命令传播
主从服务器在完成第一次同步后,双方会维护一个TCP连接。后续主服务器可以通过这个连接将写操作命令传播给从服务器,从服务器执行该命令,使得与主服务器的数据库状态相同。
分摊主服务器的压力
主服务器是可以有多个从服务器的,如果从服务器数量太多,且都与主服务器进行全量同步的话,就会带来两个问题:
- 由于是通过bgsave命令来生成RDB文件,那么主服务器就会忙于使用fork创建子进程,如果主服务器的内存数据非常大,在执行fork函数时是会阻塞主线程的,从而使得redis无法正常处理请求
- 传输RDB文件会占用主服务器的网络带宽,会对主服务器的响应速度产生影响
从服务器可以有自己的从服务器,通过这种方式,主服务器生成RDB和传输RDB的压力可以分摊到含有从服务器的从服务器上。
增量复制
- 从服务器恢复网络后,发送psync命令给主服务器,offset参数不是-1
- 主服务器收到命令,用continue响应命令告诉从服务器接下来采用增量复制的方式同步数据
- 主服务器将断线期间所执行的写命令发送给从服务器,从服务器执行这些命令
主服务器怎么知道要将哪些增量数据发送给从服务器
- repl_backlog_buffer:唤醒缓冲区,用于断线找差异数据
- replication offset:标记缓冲区的同步进度,主从服务器各有自己的偏移量,主服务器使用master_repl_offset记录自己写到的位置,从服务器使用slave_repl_offset记录自己读到的位置
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到repl_backlog_buffer缓冲区。
网络断开后,从服务器重连时,会通过psync命令将自己的复制偏移量slave_repl_offset发送给主服务器,主服务器根据自己的master_repl_offset和slave_repl_offset之间的差距,决定对从服务器之行哪种同步操作:
- 如果从服务器要读取的数据还在repl_backlog_buffer缓冲区,增量同步
- 否则,全量同步
哨兵
实现主从节点故障转移,监测主节点是否存活,如果挂了就会选举一个从节点切换为主节点,并把新的主节点的相关信息通知给从节点和客户端。
哨兵节点主要负责三件事情:监控(如何监控节点、如何判断主节点是否真的故障)、选主(选主规则)、通知(如何把新的主节点信息通知给从节点和客户端)
监控
每隔一秒给所有主从节点发送PING命令,主从节点收到PING命令会发送一个响应命令给哨兵。如果节点没在规定时间内响应命令,哨兵就会将他们标记为主观下线。
客观下线只适用于主节点。针对主节点设计主观下线和客观下线,是因为主节点可能并没有故障,只是因为主节点的压力较大或者网络拥塞,导致主节点没在规定时间内响应哨兵的PING命令。
为了减少误判,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成哨兵集群。通过多个哨兵节点一起判断,就可以避免单个哨兵因为自己网络问题导致的主节点下线误判。
当一个哨兵判断主节点主观下线后,会向其他哨兵发起命令,进行主节点下线投票,赞成票超过阈值quorum则判定客观下线。
选主
主节点发生故障时,要在哨兵集群中选出一个leader执行主从切换。选leader的过程其实是一个投票的过程。投票开始前,得有候选者,哪个哨兵节点判断主节点位客观下线,这个节点就是候选者。
候选者会向其他哨兵发送命令,表明希望成为Leader来执行主从切换,让其他哨兵投票。每个哨兵只有一次投票机会,如果用完后就不能参与投票了,可以投给自己或别人,只有候选者才能把票投给自己。
候选者成为Leader必须满足两个条件:
- 拿到半数以上的赞成票
- 拿到票数的同时还需要大于等于哨兵配置文件中的quorum值
哨兵节点至少要三个的原因也是因为,如果只有两个哨兵节点,且都为候选者的话,就选不出Leader。或者如果有一个哨兵挂了,另外一个哨兵也同样不能拿到半数以上选票。
如果3个哨兵节点挂了2个,只能人为介入或者增加哨兵节点。
quorum的值建议设置为哨兵个数的二分之一加1,且数量应该是奇数。
主从故障转移过程
-
在旧主节点下属的从节点选一个,作为主节点
-
挑选一个状态良好、数据完整的从节点(首先把已经下线的从节点过滤掉,然后把以往网络连接状态不好的从节点过滤掉:down-after-milliseconds*10,断连超过十次1,说明这个从节点网络状况不好。接下来对所有从节点进行三轮考察:优先级、复制进度、ID号),哨兵leader发送SLAVEOF no one,将这个从节点转为主节点
-
哨兵leader每秒一次的频率给新主节点发送INFO命令,观察命令回复中的角色信息,当观察到slave变为master时,哨兵leader就知道被选中的从节点成功升级为新主节点
-
让旧主节点下属所有从节点修改复制目标为新主节点
-
向旧主节点所有从属节点发送SLAVEOF,让它们成为新主节点的从节点
-
将新主节点的IP和信息通过发布者/订阅者机制通知给客户端
-
客户端和哨兵建立连接后,会订阅哨兵提供的频道。主从切换完成后,哨兵就向+switch-master频道发布新主节点的IP地址和端口信息,这时候客户端就可以收到这个信息,然后和新主节点进行通信
-
继续监控旧主节点,当旧主节点重新上线,将它设置为新主节点的从节点
-
旧主节点重新上线时,向它发送SLAVEOF命令
哨兵集群组成
哨兵节点之间使用过Redis的发布者和订阅者机制来完成相互发现的。主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的(在频道发送自己的信息,其他哨兵都会收到)。
Redis Cluster集群
哨兵模式基于主从模式,实现读写分离,可以实现故障转移,可用性高。但是每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。
Redis Cluster集群实现了Redis的分布式存储。对数据进行分片,每台Redis节点上存储不同的内容,解决在线扩容问题。并且它可以保存大量数据,分散数据到各个Redis实例,还提供复制和故障转移功能。
Redis Cluster采用哈希槽来处理数据和实例之间的映射关系。
一个切片集群被分为16384个slot,每个进入Redis的键值对会根据key进行hash,分配到slot中。
使用CRC16算法计算出一个16bit的值,再对16384取模,落到最终的slot。
在Redis Cluster模式下,节点对请求的处理过程如下:
-
根据 key 计算槽位,检查当前节点是否负责:
-
如果不负责 → 返回 MOVED。
-
如果负责该槽位:
-
如果 slot 正常 → 执行请求。
-
如果 slot 处于 MIGRATING:
-
如果 key 已经迁出 → 返回 ASK(告诉客户端去目标节点)。
-
如果 key 还在本节点 → 继续在本节点执行。
-
如果 slot 处于 IMPORTING:
-
客户端必须带
ASKING
→ 才能临时执行; - 否则 → 返回 MOVED。
Moved重定向:将哈希槽所在的新实例的IP和port带回去,修改客户端槽位映射(完成迁移)
ASK重定向:槽位迁移过程中,临时重定向,不更新槽位映射
节点间通信 Gossip
Gossip是一种谣言传播协议,每个节点周期性地从节点列表中选择k个节点,将本节点存储的信息传播出去,直到所有节点信息一致,算法收敛。
Gossip协议包括多种消息类型:
- meet:通知新节点加入。消息发送者通知接受者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并周期性的ping、pong消息交换。
- ping:节点每秒会向集群中其他节点发送ping消息。
- pong:接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。
- fail:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息后把对应节点更新为下线状态。
故障转移
redis集群通过ping/pong消息实现故障发现:
- 主观下线:某个节点认为另一个节点不可用
- 客观下线:标记一个节点真正下线,集群内多个节点都认为该节点不可用,达成共识。
Redis Cluster 节点下线流程:
- 节点失联 → 被标记 PFAIL。
- 多数 master 确认 → 标记 FAIL 并广播。
- 如果是 master,下属 slave 发起选举。
- 投票选出新的 master,更新槽位映射。
- 客户端通过 MOVED/ASK 被重定向到新的节点。
假设主节点 masterA 宕机(触发故障转移),它有从节点 slaveA1、slaveA2:
-
检测到 masterA 下线
-
集群中大多数 master 节点确认 masterA FAIL。
-
从节点发起选举
-
slaveA1、slaveA2 发现 masterA 下线,开始竞争成为新的 master。
-
它们会向其他 master 请求投票。
-
投票过程
-
其他 master 节点根据条件(例如谁先同步过最新的复制偏移量)来投票。
-
需要超过半数的 master 节点投票,slave 才能成为新的 master。
-
选出新的 master
-
比如 slaveA1 获胜,升级为 master。
-
Cluster 广播新的拓扑信息,更新 slot → 节点映射。
-
客户端重定向
-
客户端访问原来 masterA 的 slot 时,会收到
MOVED
重定向到新的 masterA1。
为什么Redis Cluster的hash slot时16384
Redis Cluster 选择 16384 (2¹⁴) 个 Hash Slot 是一种 性能与内存开销的折中:
- 足够细粒度,便于数据迁移与负载均衡;
- 占用的元数据小(slot信息仅 2KB/节点),心跳通信轻量;
- 位运算方便(基于bitmap),效率高;
- 不必用到 65536 这样更大的值,避免额外开销(gossip消息大)。
缓存
缓存雪崩
在同一时刻,大量缓存数据同时失效或不可用,导致大量请求直接打到数据库/后端服务,引发 数据库压力骤增甚至宕机 的情况:
-
大量缓存同时实效
-
过期时间错峰:过期时间加上随机数
- 加互斥锁:如果发现访问的数据不在Redis,加互斥锁,保证同一时间只有一个请求构建缓存,同时设置超时时间
- 热点数据永不过期:高频访问的数据不设置TTL,后台定时刷新(不仅定时更新缓存,还要频繁检测缓存是否存在)
- 多级缓存(应用缓存+Redis缓存+DB)
-
缓存预热
-
Redis故障宕机
-
限流&降级&熔断:数据库或下游压力过大时,服务端做降级处理(返回默认值/静态数据),并限制流量,甚至可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误。
- 构建Redis缓存高可靠集群:通过主从节点的方式构建Redis高可靠集群
缓存击穿
热点数据过期:
- 热key永不过期+后台刷新
- 互斥锁
- 提前续约:真正过期前,后台线程/任务去刷新缓存(预热机制);业务层看到缓存快过期时触发异步更新
- 限流/降级
缓存穿透
访问的数据既不在缓存中也不在数据库中。缓存穿透发生一般有两种情况:
- 业务误操作:缓存中的数据和数据库中的数据都被误删除
- 黑客恶意攻击:故意大量访问某些读取不存在数据的业务
解决方案:
- 非法请求限制:API入口判断请求参数是否合理,请求参数是否有非法值、请求字段是否存在
- 缓存空值或默认值:针对查询的数据设置一个空值或者默认值
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
布隆过滤器:有初始值都为0的位图数组和N个哈希函数两部分组成。写入数据时,在布隆过滤器做标记,下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
- 使用N个哈希函数分别对数据进行哈希计算,得到N个哈希值
- 将第一步的N个hash值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置
- 将每个哈希值在位图数组的对应位置的值设置为1
- 查询时,判断所有位置全为1,则存在,否则不存在(不存在肯定不存在,存在可能存在)
数据库和缓存一致性问题
造成缓存和数据库的数据不一致的现象是因为并发问题。
Cache Aside 旁路缓存策略
不更新缓存,删除缓存中的数据,然后到读取数据的时候更新缓存。
写策略:(缓存写入远比数据库写入快)
- 更新数据库数据
-
删除缓存中的数据
-
消息队列重试机制:如果删除缓存失败,从消息队列重新读取数据,再次删除缓存,重试超过一定次数,给业务层发送报错信息;如果删除缓存成功,把数据从消息队列移除。
-
订阅MySQL binlog:订阅binlog日志,拿到具体要操作的数据,再执行缓存删除
-
Canal中间件模仿MySQL主从复制协议,把自己伪装成MySQL的从节点,向MySQL主节点发送dump请求,复制binlog,解析后转换为便于读取的结构化数据供下游程序订阅使用。具体做法如下:
-
将binlog日志采集发送到MQ队列
- 编写一个简单的缓存删除,订阅binlog日志
- 根据更新log删除缓存,通过ACK机制确认处理这条更新log,保证数据缓存一致性
读策略:
- 命中缓存,直接返回
- 没有命中,查库,写缓存加过期时间(兜底),返回用户
如果业务对缓存命中率有很高要求,可以采用先更新数据库再更新缓存,为了解决其中的并发更新问题,可以加分布式锁,更新完缓存时,给缓存加上较短的过期时间,即使出现缓存不一致,缓存的数据也会很快过期。
针对先删缓存再更新数据库方案在读写并发请求而造成的缓存不一致的解决办法是:延迟双删--删除缓存,更新数据库,睡眠,删除缓存