Redis 修炼手册~
时不时打个补丁,让自己的知识体系少点漏洞。
1. 缓存穿透、缓存击穿、缓存雪崩
这三个都是缓存失效,请求全部打到数据库的场景;
缓存穿透:查询缓存和数据库中不存在的数据,由于一直查不到数据导致不能回写缓存,可能导致数据库挂掉。
解决方法:
参数入口校验合法性
缓存空值,空值设置过期时间短一些,避免内存撑爆
使用布隆过滤器拦截非法key,将所有合法数据都加载到布隆过滤器中,布隆过滤器对于存在可能误判,但是能确认一定不存在
缓存击穿:当缓存中的某个热点key过期的瞬间,大量请求涌进来全部打到数据库,导致数据库崩溃。
解决方法:
使用互斥锁,当缓存失效时只让一个请求线程去加载数据库并回写缓存,成功后再释放锁;
设置缓存数据逻辑过期,将缓存中的value值设为hash类型,在其中设置过期时间戳,当请求来的时候判断数据是否过期,判断过期的话加锁并启动第二个线程从数据库中加载数据并回写缓存,对数据一致性不敏感的话第一个线程可返回缓存中的旧数据。
缓存雪崩:缓存中的大批量key同时过期,或者Redis整个挂掉了,请求全部打到数据库,导致数据库崩溃。
解决方法:在过期时间的基础上增加随机值,将缓存key的过期时间分散开
2. 缓存与数据库保证一致性
1、先更新数据库,再删缓存
2、缓存延迟双删:先删缓存,更新数据库,延迟再删一次缓存
3、Binlog 异步更新:用Canal监听mysql的Binlog日志,使用消息队列异步更新缓存。
3. Redis 分布式锁
Redis 实现分布式锁的核心就是 SET key value EX seconds NX (NX = Not eXists,XX = eXists)命令配合 Lua 脚本。加锁时用 NX 保证互斥,EX 设置过期防止死锁;解锁时必须用 Lua 脚本先校验再删除,保证原子性。
加锁流程:
1)客户端执行 SET lock_key uuid EX 30 NX
2)Redis 检查 lock_key 是否存在
3)不存在则设置成功,返回 OK,加锁成功
4)已存在则返回 nil,加锁失败
解锁流程:
1)客户端执行 Lua 脚本
2)GET lock_key 获取当前值
3)判断值是否等于自己的 uuid
4)相等则 DEL lock_key,解锁成功
5)不相等则返回 0,说明锁不是自己的
4. Redisson 分布式锁
Redisson 分布式锁的核心就是用 Redis 的 Hash 结构 + Lua 脚本保证原子性,再加上一个后台线程做自动续期。
加锁流程
加锁时 Redisson 执行一段 Lua 脚本,逻辑分三步:
1)锁不存在,直接创建 Hash 结构,field 是线程标识,value 设为 1,然后设置过期时间,加锁成功
2)锁存在且 field 匹配当前线程,说明是重入,value 加 1,刷新过期时间,加锁成功
3)锁存在但 field 不匹配,说明被别人占着,返回锁的剩余存活时间,加锁失败
用 Hash 而不是 String 的好处是天然支持可重入,同一个线程多次加锁只需要累加计数。
自动续期
加锁成功后(不手动设置过期时间),Redisson 会启动一个后台定时任务,默认每隔 10 秒把锁的过期时间重置为 30 秒。只要业务线程还活着、还持有锁,这个续期任务就会一直跑。业务执行完调用 unlock 或者线程挂了,续期任务才停止,锁自然过期。
这套机制叫 Watch Dog,解决了”业务还没跑完锁就过期”的难问题。
解锁流程
解锁同样是一段 Lua 脚本:
1)锁不存在或者 field 不匹配当前线程,直接返回,防止误删别人的锁
2)field 匹配,计数减 1。如果减完还大于 0,说明还有重入没退出,刷新过期时间
3)计数减到 0,删除 key,同时 publish 一条消息通知其他等待的线程
5. Redis 中内存淘汰策略
1、不淘汰,达到内存上限直接报错
2、allkeys-random 随机淘汰所有key
3、volatile-random 随机淘汰设置过期时间的key
4、volatile-ttl 优先淘汰剩余key中剩余存活时间最短的key
5、allkeys-lru 淘汰所有key中最近最少使用的key
6、allkeys-lfu 淘汰所有key中最低访问频率的key
7、volatile-lru 淘汰设置过期时间的key中最近最少使用的key
8、volatile-lfu 淘汰设置过期时间的key中最低访问频率的key
LRU(Least Recently Used):最近最少使用算法
传统 LRU 通常通过哈希表 + 双向链表实现。每次访问一个key,就把该key移动到链表头,内存不足时,淘汰链表尾部的key。
Redis 使用的是近似 LRU 算法:
- 每个 Key 的对象元数据中保存最近访问时间信息。
- 需要淘汰时,从候选 Key 中进行采样。
- 根据空闲时间选择较久未访问的 Key。
- 通过候选池优化淘汰效果。
这样减少了额外的内存和维护成本,但不能保证每次淘汰的都是全局最久未访问的 Key。
LFU(Least Frequently Used):最不经常使用算法
如果一个 Key 在一段时间内访问次数很少,那么它未来被访问的可能性也较低,因此优先淘汰它。
Redis 的 LFU 是根据 Key 的访问频率进行内存淘汰的,但它并不是简单地累计访问次数,而是通过概率递增和时间衰减两个机制实现。
首先,Redis 使用 8 位计数器保存访问频率,最大值为 255。为了让计数器能够表示更大的访问量,它使用概率递增算法。计数器越大,每次访问使计数器加 1 的概率就越低,这个过程可以通过 lfu-log-factor 参数进行调整。
其次,Redis 引入了时间衰减机制。因为一个 Key 过去访问频率很高,不代表它现在仍然热门。Redis 会根据距离上次记录的时间计算衰减值,默认每经过一分钟衰减一个计数单位。不过 Redis 并不会定时遍历所有 Key,而是在访问 Key 或进行淘汰采样等场景下惰性计算衰减,从而降低维护开销。
最后,当内存不足时,Redis 通过采样和候选池机制,优先淘汰衰减后访问频率较低的 Key。
6. Redis 中的数据过期后的删除策略
惰性删除和定期删除两种策略配合使用
惰性删除,被动触发,redis每次读写某个key之前检查该key是否过期,过期了就删掉返回空值。好处是不用占用cpu去扫描,坏处是如果一个key过期了但是一直没人访问就会一直赖在内存里;
定期删除,主动触发,redis每100ms触发一次定时任务,随机抽一批设置了过期时间的key进行检查,发现过期的就删掉。但是他不会一股脑的将所有key都扫描一遍而是进行有限制的采样清理,避免清理动作本身把cpu吃满。
定期删除的具体步骤:
从设置了过期时间的key集合中随机选取20个key
删掉其中已经过期的
如果已经过期的key的比例超过25%,说明还有已过期的key的概率较大,继续采样20个key
如果已经过期的key的比例低于25%,说明差不多了,停止定期删除的操作
整个定期删除过程硬性设置25ms,超时强制结束,防止主线程被卡住
7. Redis 的持久化机制
RDB 和 AOF
RDB 是快照持久化,某个时间点将整个redis内存数据dump成一个二进制文件。恢复速度快,文件体积小,适合做备份和灾难恢复;缺点是redis在两次快照期间宕机的话这期间的数据就丢了。
AOF是日志持久化,redis的每条写命令都会追加到文件中。缺点是文件体积大,恢复数据的时候会将所有命令执行一遍,数据量大的时候恢复会很慢。
混合持久化,在AOF重写的时候先写一份RDB快照,后面的增量部分使用AOF记录,恢复的时候先加载RDB部分,再执行AOF的增量命令。
8. Redis 的主从同步机制
Redis 主从同步就是开启多个 Redis 实例让从节点认一个主节点当大哥,提高Redis的并发能力,实现读写分离,一般是一主多从;
1、建立连接与全量同步
当从节点第一次连接主节点时,会发送 PSYNC 命令,因为是第一次,主节点会进行全量复制,主节点生成一份RDB快照发送给从节点,从节点清空自己的旧数据加载这份快照;在主节点生成和发送RDB快照时,主节点不会停止服务,会将这段时间的增量命令暂存到Replication Buffer 的内存缓冲区中,等快照发完,再把这些增量命令发送给从节点,避免数据丢失。
2、命令传播
全量同步之后,主从之间就会建立一个长连接,主节点的新增命令都会异步的发送给从节点,从节点跟着执行相同的命令,主从之间通过心跳机制确认对方存活。
3、断线重连与增量同步
主节点内部有一个环形的缓冲区叫 repl_backlog_buffer(积压缓冲区,环形可能会形成数据覆盖)
从节点记录自己同步数据的offset,主节点记录自己写的新数据的offset,当从节点断线重连之后把自己的 offset 告诉主节点,主节点拿这个 offset 去 repl_backlog_buffer 里查。buffer 里每条数据都有对应的 offset 范围,能查到就增量同步,查不到说明数据被覆盖了就全量同步。
9. Redis 常见数据类型
5种:String、List、Hash、Set、Zset
10. Redis 的哨兵机制
Redis Sentinel 是一套高可用的监控系统,专门盯着主从集群,主节点挂了能自动把从节点提升为主节点,客户端也能自动感知到新主节点的地址。
功能:
1)监控:每秒给所有 Redis 节点发 PING,看谁没响应
2)故障转移:主节点挂了,从从节点里选一个升级成新主节点
3)通知:把新主节点的地址推送给客户端和其他从节点
11. Redis 集群
Redis 集群主要解决了单机内存和并发的瓶颈,它通过多个 Redis 实例组成,每个实例存储不同的数据分片。
它的实现原理,可以用三个关键词来概括:分片、Gossip 协议、去中心化。
1)数据怎么存的?
Redis 集群引入了哈希槽的概念,把整个数据集群划分为 16384 个槽。集群里的每个主节点负责维护一部分槽。
存一个 Key 时,Redis 先对 Key 算一个 CRC16 值,然后对 16384 取模,算出它属于哪个槽,再把数据存到负责这个槽的节点上。
这样做的好处是扩容缩容非常方便。要加一个新节点,只需要把其他节点身上的一部分槽”过户”给新节点就行了,不需要像传统哈希那样重新计算所有数据的位置。
2)节点之间怎么联系?
集群里的节点是去中心化的,没有所谓的”中心大脑”。它们之间通过 Gossip 协议互相通信。
每个节点定期随机选几个邻居,交换彼此的状态信息,比如”我负责哪些槽”、”我是否还活着”。这样只需要很短的时间,集群的所有节点就能达成一致,知道整个集群的拓扑结构。
3)客户端怎么找数据?
客户端可以连接集群的任意一个节点。如果客户端要找的 Key 刚好在这个节点上,那就直接返回;如果不在,这个节点不会充当代理去转发请求,而是会返回一个 MOVED 错误,告诉客户端:”这个 Key 不归我管,你去连 IP:Port 这个节点吧。”
12. Redis 中 Big key 的处理方式
发现大key,分析业务,拆分数据,优化访问,安全删除
由于Redis是单线程命令,操作大key耗时较长,从而导致redis出现其他命令阻塞的问题
Redis 中的 “big Key” 是指一个内存空间占用比较大的键(Key),它的危害是:
1、内存分布不均。在集群模式下,不同 slot 分配到不同实例中,如果大 key 都映射到一个实例,则分布不均,查询效率也会受到影响。
2、由于 Redis 单线程执行命令,操作大 Key 时耗时较长,从而导致 Redis 出现其它命令阻塞的问题。
3、大 Key 对资源的占用巨大,在你进行网络 I/O 传输的时候,导致你获取过程中产生的网络流量较大,从而产生网络传输时间延长甚至网络传输发现阻塞的现象,例如一个 key 2MB,请求个 1000 次 2000 MB。
4、客户端超时。因为操作大 Key 时耗时较长,可能导致客户端等待超时。
解决 Big Key 问题可以从三个方向入手:
开发层面:把大对象拆成小对象,一个 Hash 100 万个 field 可以按 ID 取模拆成 100 个 Hash;对存储的数据做压缩,比如用 Snappy、LZ4 压一下再存进去;选合适的数据结构,有些用 String 存 JSON 的场景换成 Hash 能省不少内存。
业务层面:只存必要的数据,用户的详细地址、历史订单这些低频数据没必要全塞 Redis 里,用的时候再查数据库;从根源上避免大 Key 产生,比如评论列表只缓存前 100 条热门的。
架构层面:用 Redis Cluster 部署,把大 Key 拆分后散落到不同节点,响应速度能快一截。
13. Redis 中 热点 key 的处理方式
1、使用Caffeine实现在服务器内本地缓存,热点数据直接从 JVM 内存里读,不走网络
2、热点key进行拆分
3、通过主从复制,实现Redis的读写分离
4、实行限流和降级
14. Lua 脚本的功能及使用
Redis 允许用户在 Redis 服务器端执行自定义的 Lua 脚本,用来实现原子操作和复杂逻辑。
其核心点包括:
原子性:Lua 脚本的所有命令在执行过程中是原子的,避免了并发修改带来的问题。
减少网络往返次数:通过在服务器端执行脚本,减少了客户端和服务器之间的网络往返次数,提高了性能。
复杂操作:可以在 Lua 脚本中执行复杂的逻辑,比如批量更新、条件更新等,超过了单个 Redis 命令的能力。
15. Redis 事务
Redis 支持事务,但它和 MySQL 事务完全是两码事。Redis 事务只保证命令打包执行不被其他客户端插队,不支持回滚。
Redis 事务用 MULTI、EXEC、WATCH、DISCARD 四个命令实现:
1、MULTI 开启事务,之后发的命令不会立即执行,而是进入一个队列排队
2、队列里可以塞任意多条命令,Redis 会回复 QUEUED 表示入队成功
3、EXEC 一次性把队列里的命令全部执行,期间不会被其他客户端的命令打断
4、如果中途不想执行了,用 DISCARD 丢弃整个队列
5、WATCH 可以在 MULTI 之前监视某些 key,如果这些 key 在 EXEC 之前被别人改了,整个事务作废
最重要的一点:MySQL 事务执行到一半出错可以回滚,Redis 不行。Redis 事务里某条命令执行失败,剩下的命令照样继续跑,已经执行的也不会撤销。
16. Redis I/O多路复用
Redis 的 I/O 多路复用(I/O Multiplexing) 是 Redis 实现高并发的重要技术之一。
I/O 多路复用就是让一个线程同时监听多个客户端的网络连接,当某个连接有数据可读或可写时,再去处理对应的 I/O 事件,而不是为每个客户端创建一个线程。
它主要解决的问题是:如何使用少量线程,高效处理大量客户端的网络连接。
评论