目录
2. Redis 分布式锁(Distributed Lock)
1. Redis 缓存体系(Cache)
1.1. 为什么需要缓存
- 核心思路:将常用数据放到访问速度更快的地方
- 硬件访问速度层级:CPU 寄存器 > 内存 > 硬盘 > 网络
- 二八定律:20% 的热点数据,能够应对 80% 的访问场景。缓存空间有限且成本高昂,因此只缓存热点数据,性价比最高
1.2. 为什么 Redis 适合做 MySQL 的缓存
MySQL 性能瓶颈在于:
- 磁盘 IO:数据存于硬盘,随机访问慢
- 索引失效:导致全表遍历,增加 IO 次数
- SQL 解析:解析、校验、优化消耗 CPU
- 复杂查询:联合查询产生笛卡尔积,效率极低
高并发下,MySQL 资源极易耗尽导致宕机。应对策略分为开源(集群、分库分表)和节流(引入缓存)。Redis 数据在内存中,且仅支持简单的 K-V 存储,能挡住绝大多数读请求
- 缓存是为了加快“读操作”的,如果是“写操作”,仍需老老实实写数据库,缓存不能提高写性能
1.3. 缓存的更新与淘汰策略
如何识别热点数据
- 定期生成:通过日志统计(Hadoop/Spark),挑选高频词。缺点:实时性差,无法应对突发事件(如春节期间的“春晚”)
- 实时生成:设定容量上限(maxmemory)。读请求先查 Redis,没有则查 DB 并写入 Redis。如果 Redis 满了,触发淘汰策略
Redis 内置淘汰策略对比表
| 淘汰策略 | 作用域 | 核心算法/逻辑 | 适用场景 |
|---|---|---|---|
| volatile-lru | 设置了过期时间的 key | 淘汰最近最少使用的 | 缓存同时包含热点数据和持久数据 |
| allkeys-lru | 所有 key | 淘汰最近最少使用的 | 纯缓存场景,推荐使用 |
| volatile-lfu | 设置了过期时间的 key | 淘汰访问频率最少的(Redis 4.0+) | 防止偶发冷数据挤掉热点数据 |
| allkeys-lfu | 所有 key | 淘汰访问频率最少的(Redis 4.0+) | 纯缓存且访问频率差异大 |
| volatile-random | 设置了过期时间的 key | 随机淘汰 | 无明显热点,要求极速淘汰 |
| allkeys-random | 所有 key | 随机淘汰 | 同左 |
| volatile-ttl | 设置了过期时间的 key | 优先淘汰最早过期的 | 相当于局限于过期 key 的 FIFO |
| noeviction | 所有 key | 不淘汰,写入报错(默认) | 不允许数据丢失的场景 |
1.4. 缓存四大灾难与防御体系
| 灾难名称 | 核心现象 | 根本原因 | 解决方案 |
|---|---|---|---|
| 缓存预热 | 系统刚上线/Redis 重启/大促前,MySQL 压力骤增 | 缓存为空,冷启动 | 提前统计热点数据,启动时写入 Redis;定时预热、大促前预热 |
| 缓存穿透 | 请求的 key 在 Redis 和 DB 都不存在 | 恶意攻击、参数缺乏校验、数据误删 | 1. 参数合法性校验 2. 缓存空值(设短过期时间) 3. 布隆过滤器 |
| 缓存雪崩 | 大量 key 同时失效或 Redis 宕机,MySQL 压力骤增甚至宕机 | 大量 key 相同过期时间;Redis 整体不可用 | 1. 高可用 Redis 集群 + 监控 2. 过期时间加随机因子 3. 热点数据不设过期或逻辑过期 4. 多级缓存、限流降级 |
| 缓存击穿 | 单个热点 key 突然过期,大量请求直击 DB | 热点 key 过期 | 1. 热点 key 永不过期或逻辑过期 2. 互斥锁/分布式锁 3. 服务降级 |
2. Redis 分布式锁(Distributed Lock)
在分布式系统中,多个节点访问公共资源,传统的 synchronized(Java)或 std::mutex(C++)只能保证单进程内的线程安全,跨进程/跨主机无能为力
因此分布式锁的本质是使用一个公共服务器来记录加锁状态
这个公共的服务器可以是 Redis, 也可以是其他组件(⽐如 MySQL 或者 ZooKeeper 等), 还可以
是我们自己写的⼀个服务
2.1. 分布式锁的演进史
阶段一:基础实现 SETNX
- 做法:SETNX key value(Set if Not eXists)。成功则视为加锁,操作完成后 DEL 删除
- 致命缺陷:如果服务器 1 加锁后意外宕机,DEL 无法执行,锁永远无法释放(死锁)
阶段二:引入过期时间
- 做法:使用 SET key value NX EX 10(原子操作)
- 避坑:不能分开写 SETNX 和 EXPIRE!分开写有中间失败风险;用事务可以保证一起执行,但 Redis 事务不支持回滚,且不是最优解;最佳实践是 SET NX EX 单命令
- 遗留缺陷:服务器 2 可能会误删服务器 1 的锁(例如业务逻辑阻塞,锁过期后,服务器 2 加锁,此时服务器 1 恢复并执行了 DEL)
阶段三:引入校验 ID
- 做法:value 设为服务器编号(如 "服务器1")。删除前先 GET 校验 value 是否匹配
- 伪代码逻辑:
if (redis.get(key) == serverId) {
redis.del(key);
}
- 遗留缺陷:GET 和 DEL 是两步操作,不具备原子性。如果在 GET 成功之后,DEL 执行之前,key 刚好过期,服务器 2 加锁成功,此时服务器 1 执行 DEL,依然会误删服务器 2 的锁
阶段四:引入 Lua 脚本(原子性)
- 做法:利用 Redis 单线程执行 Lua 脚本的原子性,将“判断”和“删除”合二为一
- Lua 脚本示例:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
阶段五:引入 Watch Dog(看门狗 - 自动续期)
- 问题:业务还没执行完,锁提前过期了怎么办?设置太长,服务器宕机又会导致长时间无法释放锁
- 核心思想:动态调整。加锁的服务器上开启一个单独的线程(Watch Dog)
- 机制:假设初始过期时间 10s,看门狗每隔 3s 检测一次。如果任务未完成,将过期时间重写为 10s(续约);如果任务完成,直接 Lua 脚本释放锁。如果服务器宕机,看门狗线程随之挂掉,无人续约,锁在 10s 后自动过期,其他服务器可获取锁
阶段六:引入 Redlock 算法(解决主从失效)
- 极端场景:服务器 1 向 Master 加锁成功,Master 挂掉,Slave 升级为新 Master,但 key 尚未同步。此时服务器 2 依然能加锁成功,锁形同虚设
- Redlock 核心思想:多节点投票,少数服从多数
- 算法流程:
- 部署 5 个独立的 Redis Master 节点(相互独立,非集群)
- 客户端依次向 5 个节点申请加锁,设置超时时间(如 50ms),如果超时就跳过该节点
- 如果成功加锁的节点数 超过半数(≥3),且总耗时小于锁的有效时间,则视为加锁成功
- 释放锁时,需要向所有节点(包括加锁失败的节点)发送释放请求
- 结论:分布式系统中任何节点都不可靠,Redlock 通过多节点备份将可靠性大幅提升
3. 面试高频接口与命令速查
| 业务场景 | Redis 命令/API | 作用与注意事项 |
|---|---|---|
| 缓存读取 | GET key | 内存操作,O(1) 时间复杂度,拦截绝大多数读请求 |
| 缓存写入 | SET key value [EX seconds] | 配合 maxmemory 和淘汰策略使用 |
| 加锁(原子) | SET key value NX EX 10 | 唯一正确的新式加锁命令,务必使用单条命令 |
| 解锁(原子) | EVAL "lua script" 1 key value | 确保“校验 ID”和“删除 Key”的原子性 |
| 设置缓存上限 | CONFIG SET maxmemory 2gb | 触发淘汰策略的阈值 |
| 内存淘汰策略 | CONFIG SET maxmemory-policy allkeys-lru | 动态调整淘汰算法 |
设计一个高可用的 Redis 缓存和分布式锁架构
- 缓存方面,我会采用 Redis 作为 MySQL 的护盾。首先通过二八定律筛选热点数据,利用 allkeys-lru 或 allkeys-lfu 策略进行实时淘汰。同时,为防穿透,我会接入布隆过滤器并缓存空值;为防雪崩,过期时间会加上随机因子;针对击穿,热点数据采用互斥锁或逻辑过期
- 在分布式锁方面,我会演进式地考虑。首选 SET key value NX EX 原子加锁,配合 Lua 脚本实现带有校验 ID 的原子解锁。为了应对业务长耗时,引入看门狗机制进行动态续约;如果是对可靠性要求极高的金融级场景,我会采用 Redlock 算法,向半数以上的独立节点申请锁,以此规避主从异步复制带来的锁失效问题。当然,在实际开发中,我会直接使用
redis-plus-plus框架,避免重复造轮子
转载自 CSDN-专业IT技术社区



