缓存
使用场景
-
缓存
穿透, 击穿, 雪崩, 双写一致, 持久化, 数据过期, 淘汰策略
-
分布式锁
setnx, redission
-
消息队列, 延迟队列
缓存穿透
查询不存在的数据, mysql 中查询不到也不会写入缓存, 导致每次都查询数据库
-
缓存空数据, 把空结果进行缓存
- 简单
- 消耗内存, 发生不一致的问题
-
Redisson 布隆过滤器
bitmap: 一个以bit为单位的数组, 每个单元只存储二进制的 0 或 1
使用: 将数据库中存在的数据通过多个哈希函数获取哈希值, 并将数组对应位置改为 1. 对于请求参数进行相同的哈希获取数组对应元素的值, 如果有元素为 0 则说明该元素一定不存在, 全 1 则说明该元素可能存在. 存储的数据越多则数组中的 1 越多, 误判率越大.
注意: 使用中可能需要注意定时(或增加数据时)更新布隆过滤器解决缓存一致性问题
作用: 检索一个元素是否在一个集合中
- 内存占用少
- 实现复杂, 存在误判 (数组越大, 误判率越小, 但是消耗内存越多, 一般设为 1%)
缓存击穿
给某个 key 设置过期时间, 但恰好 key 过期时有大量并发请求, 这些请求会把数据库压垮
-
互斥锁
缓存未命中时获取互斥锁: 获取成功则查询数据库缓存数据; 获取失败则休眠重试获取锁
- 保证数据强一致性
- 性能较差
-
逻辑过期
设置一个
expire
字段表示过期时间, 后续检验这个字段判断是否过期缓存未命中时获取互斥锁: 获取成功则开启新线程查询数据库缓存数据, 但此时这个线程不用等待锁, 可以直接返回过期数据; 互斥锁获取失败则直接返回过期数据
- 高可用, 性能好
- 返回数据可能是过期的数据
缓存雪崩
同一时间大量的缓存 key 同时失效或者 redis 宕机, 导致大量请求到达数据库
- 给不同的 key 的过期时间添加随机值
- 使用 redis 级群 (哨兵模式, 级群模式)
- 给缓存业务添加降级限流策略 (nginx, spring cloud gateway)
- 添加多级缓存 (Guava, Caffeine)
双写一致性
修改了数据库的数据也要同时更新缓存的数据, 缓存和数据库的数据要保持一致性
读操作: 缓存命中, 直接返回; 未命中则查询数据库, 写入缓存, 设定超时时间
写操作:
-
延迟双删
先删缓存, 操作数据库, 延迟一会然后再写入缓存 (延迟为了数据库的同步)
延迟时间不好控制, 期间会出现脏数据
-
读写锁 (强一致性)
读的时候添加共享锁: 读读不互斥, 读写互斥
写的时候添加排他锁: 读读和读写均互斥, 避免脏数据
-
异步 mq (最终一致性)
使用 MQ 中间件, 更新数据后通知缓存删除
-
使用 Canal (最终一致性)
伪装成 mysql 的从节点, 通过读取 binlog 数据更新缓存, 不需要修改业务代码
redis 持久化
-
RDB
Redis Database Backup file, 将内存中的所有数据都记录到磁盘中, 当 redis 重启后从磁盘读取快照文件, 恢复数据
bgsave 开始时 fork 主进程得到子进程, 子进程共享主进程的内存数据, 完成 fork 后读取内存数据并写入 RDB 文件
fork 采用 copy-on-write 技术:
- 主进程执行读操作时, 访问共享内存
- 主进程执行写操作时, 拷贝一份数据, 执行写操作
- 恢复速度快, 文件体积小
- 大量消耗系统资源, 数据不完整, 两次备份之间的数据会丢失
-
AOF
Append Only File: 将每一个写命令记录在 AOF 文件中, 恢复数据时就是在执行一遍命令
- 数据相对完整 (取决于刷盘策略), 平时消耗 cpu 和内存资源少, 主要消耗 IO 资源 (但恢复数据时会大量占用系统资源)
- 文件体积大, 恢复速度较慢
数据过期策略
redis 对数据设置有效时间, 数据过期以后就需要从内存中删除, 删除数据可以有不同的规则
- 惰性删除
设置 key 过期时间后在使用 key 时进行检查是否过期, 如果过期就删除, 否则返回 key
- cpu 友好, 对于用不到的 key 不会浪费时间检查
- 内存不友好, 长时间不用且已过期的数据就会一直存在内存中不会删除
- 定期删除
每隔一段时间对一些 key 检查, 删除过期的 key (从一定数量的数据库中取出一定数量的随机 key 进行检查, 删除过期的 key)
SLOW 模式: 定时任务, 执行频率默认 10hz, 每次不超过 25ms
FAST 模式: 执行频率不固定, 两次间隔不低于 2ms, 每次耗时不超过 1ms
- 可以限制删除操作执行的时长和频率来减少操作对 CPU 的影响, 定期删除能有效释放过期键占用的内存
- 难以确定删除操作执行的时长和频率
数据淘汰策略
当 redis 内存不够用时, 此时再向 redis 中添加新的 key, 那么 redis 就会按照某一种规则将内存中的数据删除掉, 这种删除数据的规则称之为内存的淘汰策略
- noeviction: 不淘汰任何 key, 内存满时不允许写入新数据, 默认策略
- volatile-ttl: 对设置了 ttl 的 key, 比较 key 的剩余 ttl, ttl 越小越先淘汰
- allkeys-random: 对全体 key 随机淘汰, 访问频率差别不大, 没有冷热数据区分
- volatile-random: 对设置了 ttl 的 key 随机淘汰
- allkeys-lru: 对全体 key 基于 LRU 算法淘汰, 优先使用, 尤其是数据有明显冷热区分
- volatile-lru: 对设置了 ttl 的 key 基于 LRU 算法淘汰, 有置顶需求, 置顶数据不设置过期时间
- allkeys-lfu: 对全体 key 基于 LFU 算法淘汰, 短时高频访问
- volatile-lfu: 对设置了 ttl 的 key 基于 LFU 算法淘汰, 短时高频访问
分布式锁
底层实现
使用 setnx (set if not exists)
实现
获取锁:
# NX 互斥 EX 设置超时时间
SET lock value NX EX 10
释放锁:
DEL key
redisson 实现
redisson
使用 watch dog
机制给锁续期 (默认每 10s 查看一次), 从而合理控制锁的时间
加锁、设置过期时间等操作都是基于 lua 脚本完成, 保证操作的原子性
可重入锁
redisson
的锁是可重入锁, 多个锁重入需要判断是否是当前线程, 在 redis
中使用 hash 结构 来存储 线程信息和重入次数
主从数据一致性
可以使用redisson
提供的红锁解决主从数据一致性, 实现是对多个 $(\frac{n}{2} + 1)$ 节点同时创建锁, 但是性能太低、实现复杂、运维繁琐, 所以不推荐
redis
是 AP
思想, 如果要保证数据的强一致性建议使用 CP
思想的 zookeeper
CAP: C (一致性), A (可用性), P (分区容错)
集群
主从复制
单节点 Redis 的并发能力有上限, 搭建主从集群, 实现读写分离可以提高 Redis 并发能力
-
全量同步
replid: Replication Id, 数据集标志, 每个
master
有唯一的id
,slave
继承master
的replid
offset: 偏移量, 随着记录在
repl_backlog
中的数据增多而增大,slave
完成同步时也会记录当前同步的offset
, 如果slave
的offset
小于master
的则说明需要更新 -
增量同步
- 无法保证集群的高可用
哨兵模式
- 监控:
sentinel
不断检查master
和slave
是否正常工作 - 自动故障恢复:
master
故障时sentinel
会将一个slave
提升为master
, 原master
恢复后也以新的master
为主 - 通知:
sentinel
充当redis
客户端发现来源, 集群发生故障转移时会将最新的信息推送给redis
客户端
-
服务状态监控
哨兵基于心跳检测服务, 每隔 1 秒向集群的每个实例发送一次
ping
命令- 主观下线: 某个实例未在规定时间内响应, 则认为该实例主观下线
- 客观下线: 超过指定数量 (quorum) 的哨兵认为该实例主观下线, 则该实例客观下线, quorum 最好超过哨兵数量的一半
-
哨兵选主原则
- 判断主与从节点断开时间长短, 超过指定值则排除该从节点
- 判断从节点优先级 (slave-priority), 值越小优先级越高
- 优先级一样则判断从节点的 offset 值, 越大证明数据越多, 则优先级越高
- 根据从节点运行 id 选择, 越小优先级越高
-
脑裂
由于网络等原因可能会出现脑裂的情况, 比如, 由于 redis 的 master 节点和 salve 节点和 sentinel 处于不同的网络分区,使得 sentinel 没有能够心跳感知到 master,所以通过选举的方式提升了一个 salve 为 master,这样就存在了两个 master, 这种现象称之为脑裂. 当网络恢复后, sentinel 会将 old master 降级为 slave, 以新的 master 为主. 由于期间客户端仍会向 old master 节点中写入数据, 所以这样会导致大量数据丢失.
设置至少有一个从节点才能写入数据:
min-replicas-to-write 1
设置主从数据复制和同步的延迟时间:
min-replicas-max-lag 5
分片集群
优点
- 集群中有多个 master, 每个 master 保存不同数据
- 每个 master 都可以有多个 slave
- master 之间通过 ping 检测彼此健康状态
- 客户端请求可以访问集群任意节点, 最终都会转发到正确节点
实现
set key value # 使用 CRC16 算法计算 key 的 hash 值取模进行分片
set {aaa} key value # 使用 CRC16 算法计算 aaa 的 hash 值取模进行分片
- 分片集群引入哈希槽概念, 共 16384 个哈希槽
- 将所有哈希槽分配到不同实例
- 根据上述规则计算哈希值并对 16384 取余, 余数作为插槽, 寻找插槽所在的实例
单线程
单线程为什么那么快
- Redis 纯内存操作, 执行速度快 (所以性能瓶颈在于 网络延迟 而不是执行速度)
- 单线程避免不必要的上下文切换可竞争条件, 多线程需要考虑线程安全问题 (锁)
- 使用 I/O 多路复用模型, 非阻塞 I/O
I/O 多路复用模型
利用单个线程来同时监听多个 Socket, 并在某个 Socket 可读、可写时得到通知, 从而避免无效的等待, 充分利用 CPU 资源. 目前的 I/O 多路复用都是采用的 epoll 模式实现 (以前是 select 和 poll), 它会在通知用户进程 Socket 就绪的同时, 把已就绪的 Socket 写入用户空间, 不需要挨个遍历 Socket 来判断是否就绪, 提升了性能.
Redis 网络模型
使用 I/O 多路复用结合事件处理器应对多个 Socket 请求
- 连接应答处理器
- 命令回复处理器, 在 Redis 6.0 之后, 为了提升更好的性能, 使用了多线程来处理回复事件
- 命令请求处理器, 在 Redis 6.0 之后, 在命令的转换 (将请求数据转换成 Redis 命令) 使用了多线程, 增加命令转换速度, 但是在命令执行的时候依然是单线程