Redis 分布式锁如何实现?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
Redis 分布式锁如何实现?
面试考察点
当面试官询问 “Redis 分布式锁如何实现?” 时,他考察的远不止一个 API 的调用。其核心目的在于评估你是否:
- 理解分布式锁的本质与需求: 为什么在分布式系统中需要锁?与单机锁(如
synchronized、ReentrantLock)的根本区别是什么? - 掌握基于 Redis 实现分布式锁的核心命令与流程: 能否清晰描述从加锁、持有到释放锁的完整步骤,并理解其背后的 Redis 命令。
- 洞察实现细节与挑战: 能否识别并解决基础实现中的关键问题,例如原子性、锁误释放、死锁(锁过期) 以及由此衍生的锁续期问题。
- 了解更复杂场景与高可用方案: 对于更高要求的场景,是否知道 Redlock 算法及其争议,这体现了你的技术视野和深度。
核心答案
基于 Redis 实现分布式锁,最核心的实践是使用 SET key value NX PX milliseconds 命令进行原子性加锁,并为锁设置一个合理的过期时间以防止死锁。释放锁时,需要使用 Lua 脚本来验证锁的 value 值并删除,确保操作的原子性,避免误删其他客户端的锁。对于更高的可靠性要求,可以考虑使用 Redlock 算法。
技术深度解析
原理/机制
分布式锁的本质,是在一个所有客户端都能访问的外部存储系统(这里是 Redis)中,通过占坑(创建一个唯一的键)的方式来实现互斥。其核心挑战在于,所有操作(创建、判断、删除)在分布式并发环境下必须保证原子性,否则就会导致锁失效或混乱。
代码示例与分步解析
1. 加锁
public boolean tryLock(String lockKey, String clientId, long expireTime, TimeUnit unit) {
// 使用 SET 命令,结合 NX(不存在才设置)和 PX(毫秒级过期时间)选项
// 该命令是原子性的:只有当 key 不存在时,才会设置成功并同时设置过期时间
String result = jedis.set(lockKey, clientId, "NX", "PX", unit.toMillis(expireTime));
return "OK".equals(result);
}
lockKey: 锁的唯一标识。clientId: 通常使用 UUID 或 Snowflake ID,作为锁的value。这是识别锁所有者的关键,防止其他客户端误删。NX: 仅当键不存在时设置,确保互斥性。PX: 设置键的过期时间(毫秒)。这是避免死锁的生命线,即使持有锁的客户端崩溃,锁也会自动释放。
2. 释放锁(使用 Lua 脚本)
// Lua 脚本保证了“判断value”和“删除key”这两个操作的原子性
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public boolean unlock(String lockKey, String clientId) {
Object result = jedis.eval(UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
Collections.singletonList(clientId));
return Long.valueOf(1).equals(result);
}
使用 Lua 脚本是必须的。如果分两步(get 然后 del)在并发下会产生竞态条件:客户端 A 在 get 判断是自己的锁之后,锁刚好过期并被客户端 B 获取,此时 A 再执行 del 就会错误地删除 B 的锁。
常见误区与最佳实践
- 锁过期与业务执行时间: 这是最经典的难题。如果锁的过期时间(如 10 秒)短于业务执行时间,锁会提前释放,导致并发安全问题。一个常见的解决方案是使用 “看门狗”(Watch Dog)机制:获取锁成功后,启动一个后台线程,定期(比如在过期时间的 1/3 时)检查锁是否仍持有,并自动续期过期时间。Java 中成熟的客户端如 Redisson 内置了此功能。
- 锁的可重入性: 上述简易实现是不可重入的。在复杂业务中,同一线程可能需要再次获取已持有的锁。Redisson 的
RLock通过 Redis Hash 结构存储线程标识和重入次数,实现了可重入锁。 - Redis 单点问题: 在 Redis 主从或哨兵模式下,如果主节点加锁后数据未同步到从节点就宕机,切换后可能导致锁丢失。为此,Redis 作者提出了 Redlock 算法。
- Redlock 原理简述: 客户端向 N 个独立(非主从)的 Redis 实例依次请求加锁。只有当超过半数(N/2 + 1)的实例加锁成功,且总耗时小于锁的过期时间,才认为加锁成功。释放锁时需向所有实例发送释放请求。
- 关于 Redlock 的争议: 该算法实现复杂,且在某些极端时序假设下(如系统时钟跳跃)仍可能存在争议。业界共识是: 如果仅为了效率(避免重复计算),使用基于单 Redis 实例的锁并接受极小概率的失效是可接受的;如果为了绝对正确性,则应考虑更强一致性的协调服务,如 ZooKeeper 或 etcd。
最佳实践建议: 在生产环境中,优先使用成熟的客户端库,如 Redisson。它提供了开箱即用的分布式可重入锁、公平锁、读写锁以及看门狗机制,安全性和易用性远高于自行实现。
总结
实现 Redis 分布式锁的核心在于 “原子性加锁(SET NX PX)” 与 “原子性解锁(Lua 脚本验证+删除)”,并妥善处理锁续期问题;对于高可用场景,可权衡考虑 Redlock 算法,但理解其复杂度与局限。