用途
- 分布式锁:其他方案
Redission
- 限流:
Redis
+Lua
脚本实现 - 消息队列:自带的
list
数据结构可以作为一个简单的队列;5.0 中新增的Stream
更适合用作消息队列(类似于Kafka
,有主题和消费组的概念,支持消息持久化即ACK
机制) - 复杂业务场景:通过
bitmap
统计活跃用户、通过sorted set
维护排行榜等
在线尝试
数据结构
- string
key-value
类型- 常用命令:
set
,get
,strlen
,exists
,decr
,incr
,setex
- list
- 链表
- 常用命令:
rpush
,lpop
,lpush
,rpop
,lrange
,llen
- 应用场景:发布与订阅或者说消息队列、慢查询
- hash
- 类似于
JDK1.8
前的HashMap
- 常用命令:
hset
,hmset
,hexists
,hget
,hgetall
,hkeys
,hvals
- 应用场景:特别适合用于存储对象
- 类似于
- set
- 类似于
Java
中的HashSet
;可以基于set
实现交集、并集、差集 - 常用命令:
sadd,spop,smembers,sismember,scard,sinterstore,sunion
- 类似于
- sorted set
- 和
set
相比,sorted set
增加了一个权重参数score
(元素能够按score
进行有序排列;通过score
范围获取元素列表) - 常用命令:
zadd,zcard,zscore,zrange,zrevrange,zrem
- 应用场景:需要对数据根据某个权重进行排序的场景
- 和
- bitmap
- 存储的是连续的二进制数字(0 和 1)
- 常用命令:
setbit
、getbit
、bitcount
、bitop
- 应用场景:需要保存状态信息并需要进一步对这些信息进行分析的场景
(用户行为分析、活跃用户统计、用户在线状态)
# SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位
127.0.0.1:6379> setbit mykey 7 1
(integer) 0
127.0.0.1:6379> getbit mykey 7
(integer) 1
缓存过期
通过过期字典来保存数据过期的时间。
键为数据库中的某个key
,值为过期时间(long
型值,毫秒精度的UNIX
时间戳)
过期数据删除策略
- 惰性删除:取出
key
时进行过期检查(可能造成太多过期key
不能被删除) - 定期删除:每隔一段时间抽取一批
key
执行删除(底层通过限制删除操作执行的时长和频率最小化影响) Redis
采用的策略:定期删除+惰性/懒汉式删除
Redis
内存淘汰机制
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
持久化
RDB
(Redis DataBase
,快照):默认。创建快照之后,可以对快照进行备份。- 将快照复制到其他服务器,创建具有相同数据的服务器副本(主从结构中,主要用来提高性能)
- 将快照留在原地以便重启服务器的时候使用
AOF
(Append Only File
,追加文件):记录每一条指令并记录到server.aof_buf
中,再根据appendfsync
配置将其同步到硬盘中的AOF
文件中
对比:
AOF
文件的保存位置和RDB
文件的位置相同AOF
实时性能更好
AOF
重写:产生一个新的AOF
文件,新文件与原文件所保存的数据库状态一样,体积更小
事务
通过 MULTI
,EXEC
,DISCARD
和 WATCH
等命令来实现事务(transaction)功能
# 开启事务
> MULTI
OK
# 指令排队
> SET USER "Guide"
QUEUED
# 还未真正执行,拿不到存储的值
> GET USER
QUEUED
# 执行指令
> EXEC
1) OK
2) "Guide"
执行过程
- 开始事务(
MULTI
) - 命令入队(先进先出(FIFO)的顺序执行)
- 执行事务(
EXEC
)
DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令
WATCH
命令用于监听指定的键,当调用 EXEC
命令执行事务时,如果一个被 WATCH
命令监视的键被修改的话,整个事务都不会执行,直接返回失败
Redis
不支持 roll back,不满足原子性(而且不满足持久性)Redis
事务可以理解为命令请求打包。再按顺序执行打包的所有命令,且不会被中途打断
缓存穿透
大量请求的key
根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层
解决办法
- 缓存无效
key
:如果key
变化不频繁可以采用;尽量将无效key
过期时间设置短一些是(如 1 分钟)
缓存key
示例:表名:列名:主键名:主键值
- 布隆过滤器:先通过布隆过滤器判断,存在则从缓存获取(及后续步骤);不存在直接返回
缓存雪崩
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求
(如热点数据在某一时刻大面积失效)
解决办法
Redis
服务宕机- 集群
- 限流
- 缓存失效
- 设置不同的失效时间(比如随机设置)
- 缓存永不失效
缓存与数据库一致性
缓存为何只删除不更新
- 防止并发更新导致的数据不一致
- 降低数据不一致的概率,不应该更新缓存,而是直接将其删除
先更新数据库还是先删除缓存?
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存,也称为
Cache Aside Pattern
,适用于绝大多数场景
如果先删除缓存,并发时,线程A删除缓存后,线程B读取数据发生 Cache Miss,从数据库中读取数据并更新至缓存,然后线程A更新数据库。
上述操作导致缓存中存储的是脏数据,之后的读操作获取的都是脏数据(直至key过期或被LRU策略剔除)
解决方法
- Cache Aside Pattern(旁路缓存模式):写请求时,先更新DB,再直接删除缓存
绝大部分情况下能够做到数据一致,极端情况下仍存在问题。
- 更新数据库A和删除缓存B都不是原子操作,任何再A之后B之前的读操作,都可能会读到旧数据
(超高并发的应用可能会出现问题)- 数据库更新完成后,线程被意外 kill 掉
- 线程 A 读数据时 Cache Miss,从数据库读取的数据还未及时同步至缓存;此时线程B更新数据库并删除缓存旧值,随后线程A把之前查到的数据同步到缓存
如果更新DB成功,患处删除失败:
- 缩短缓存失效时间:对先操作缓存后操作DB场景不适用
- 增加缓存更新重试机制
- 如果缓存服务宕机,隔一段时间进行重试
- 将删除失败的
key
进行入队保存,待服务恢复后,继续重试删除
-
Double Delete
# 执行过程 # 先删除缓存 delete redis cache update database # 睡眠一段时间后再次删除缓存脏数据 sleep(500ms) delete redis cache
-
Read/Write Through Pattern
将同步数据的任务交给缓存系统完成,应用程序不需要关心。
- Read Through是指发生cache miss时,缓存系统自动去数据库加载数据。
- Write Through是指如果cache miss时,直接更新数据库,然后返回;如果cache hit,则更新缓存后,由缓存系统自动同步到数据库
-
Write Behind
更新数据时,只更新缓存,不更新数据库。由另外一个服务异步的把数据更新到数据库。
- 纯内存操作,I/O非常快
- 异步操作,可能存在数据一致性问题
-
设置缓存过期时间
集群方案
主从复制
基本原理
- 包含一个主数据库实例(master)和多个从数据库实例(slave)
- 客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库
工作流程
- slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过 bgsave 保存RDB快照,并使用缓冲区记录保存快照这段时间内执行的写命令
- master将保存的快照文件发送给slave,并继续记录执行的写命令
- slave接收到快照文件后,加载快照文件,载入数据
- master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
- 此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
优缺点
- 优点
- master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
- master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
- 缺点
- 不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
- master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
- 难以支持在线扩容,容量受限于单机配置
Sentinel 哨兵模式
基本原理
- 基于主从复制模式,引入了哨兵来监控与自动处理故障
- slave 节点不提供服务
- 哨兵功能:
- 监控master、slave是否正常运行
- 当master出现故障时,能自动将一个slave转换为master(大哥挂了,选一个小弟上位)
- 多个哨兵可以监控同一个Redis,哨兵之间也会自动监控
- 哨兵启动后,需要与被监控的master建立两条连接
- 一条连接用来订阅master的
_sentinel_:hello
频道,获取其他监控该master的哨兵节点信息 - 另一条连接定期向master发送INFO等命令获取master本身的信息
- 一条连接用来订阅master的
- 与 master 建立连接后,哨兵会执行三个操作
- 定期(一般10s一次,当master被标记为主观下线时,改为1s一次)向master和slave发送INFO命令
- 定期向master和slave的
_sentinel_:hello
频道发送自己的信息 - 定期(1s一次)向master、slave和其他哨兵发送PING命令
INFO
命令可以获取当前数据库的相关信息从而实现新节点的自动发现- 哨兵向主从数据库
_sentinel_:hello
频道发送信息与同样监控这些数据库的哨兵共享自己的信息
重新选举
- 哨兵认为master客观下线后,故障恢复的操作需要由选举的领头哨兵来执行,选举采用
Raft
算法- 发现master下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵
- 如果目标哨兵节点没有选过其他人,则会同意选举A为领头哨兵
- 如果有超过一半的哨兵同意选举A为领头,则A当选
- 如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵
- 选出领头哨兵后,领头者开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master,选择规则如下:
- 所有在线的slave中选择优先级最高的,优先级可以通过
slave-priority
配置 - 如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选
- 如果以上条件都一样,选取id最小的slave
- 所有在线的slave中选择优先级最高的,优先级可以通过
挑选出需要继任的slave后,领头哨兵向该数据库发送命令使其升格为master,然后再向其他slave发送命令接受新的master,最后更新数据。将已经停止的旧的master更新为新的master的从数据库,使其恢复服务后以slave的身份继续运行
优缺点
- 优点
- master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
- master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
- 哨兵模式下,master挂掉可以自动进行切换,系统可用性更高
- 缺点
- 同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
- 需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务
Cluster 模式
基本原理
- 实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题
-
采用无中心结构,特点如下:
- 所有节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
- 节点的fail是通过集群中超过半数的节点检测失效时才生效
- 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
-
工作机制
- Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念
- 集群的每个节点负责一部分 hash 槽
- Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽
- 每一个节点上,都存在两个东西:插槽(范围:1~16383)和cluster(可以理解为一个集群管理插件)
(当接收到一个key查询请求时,当前节点根据算法计算得到哈希槽,再根据这个值到对应节点上取值)
-
为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
-
当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
-
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用
参考资料: