Redis 分布式锁是面试和实战中的高频话题。下面我从原理到最佳实践系统性地讲解,并附上 Java 代码示例。
一、为什么需要分布式锁?
在单机环境下,可以使用 synchronized 或 ReentrantLock 实现线程安全。但在分布式系统中,多个进程(不同 JVM、不同机器)需要互斥地访问共享资源时,就需要一个跨进程的锁。
分布式锁应满足:
- 互斥性:同一时刻只有一个客户端持有锁。
- 避免死锁:锁最终能被释放(即使持有者崩溃)。
- 高可用:锁服务自身需要高可用。
- 可重入性(可选):同一个客户端可以多次获取同一把锁。
- 阻塞/非阻塞(可选)。
二、基于 Redis 的简单分布式锁实现
1. 最简版本:SET NX EX
利用 Redis 的 SET key value NX EX seconds 命令(原子性设置,只有 key 不存在时才成功,并同时设置过期时间)。
java
arduino
体验AI代码助手
代码解读
复制代码
public boolean tryLock(String lockKey, String requestId, int expireSeconds) {
String result = jedis.set(lockKey, requestId, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
lockKey:锁的标识。requestId:唯一标识(如 UUID),用于释放时验证,避免误删他人的锁。expireSeconds:锁自动过期时间,防止持有者崩溃导致死锁。
2. 释放锁(必须用 Lua 保证原子性)
如果直接 del lockKey,可能会误删别人持有的锁(比如业务执行超过过期时间,锁自动释放,当前线程又去删除)。因此释放时需要检查 value 是否匹配,且这两步必须原子。
lua
vbnet
体验AI代码助手
代码解读
复制代码
-- unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Java 调用:
java
typescript
体验AI代码助手
代码解读
复制代码
public boolean unlock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, 1, lockKey, requestId);
return Long.valueOf(1).equals(result);
}
这个版本的问题:
- 锁过期时间不好设置:业务可能超时,导致锁提前释放,别的线程获得锁,当前线程业务完成后会误删别人的锁(即使我们用了唯一值校验,但当前线程的
requestId与新的持有者不同,释放会失败,但业务数据可能已被并发破坏)。 - 不可重入:同一个线程无法再次获取同一把锁。
三、Redisson:生产级解决方案
Redisson 是 Redis 官方推荐的 Java 客户端,封装了分布式锁的完整实现,支持可重入、自动续期(看门狗) 、公平锁、读写锁等。
1. 引入依赖
xml
xml
体验AI代码助手
代码解读
复制代码
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.0</version>
</dependency>
2. 配置 Redisson
java
ini
体验AI代码助手
代码解读
复制代码
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
3. 使用锁(自动续期)
java
csharp
体验AI代码助手
代码解读
复制代码
lock.lock(); // 阻塞等待,默认看门狗 30 秒续期,每 1/3 时间自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
4. 带超时时间的锁(非看门狗)
java
ini
体验AI代码助手
代码解读
复制代码
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 等待最多 10 秒,获取后锁有效期最多 30 秒(不自动续期)
5. Redisson 锁的原理(简要)
- 可重入:采用 Hash 结构,key 为锁名,field 为线程 ID(或 UUID),value 为重入次数。
- 看门狗:获取锁成功后,会启动一个定时任务,定期(锁过期时间的 1/3)刷新锁的过期时间,只要持有锁的线程还在运行,锁就不会过期。
- 释放:解锁时减少重入次数,次数为 0 时才真正删除。
四、RedLock:多节点 Redis 分布式锁
Redis 主从架构存在主从切换时锁丢失的风险:客户端 A 在 master 获取锁,master 还没来得及同步到 slave 就宕机,slave 晋升为 master,此时客户端 B 可能获得同一把锁,导致互斥失效。
为了解决这个问题,Redis 作者提出 RedLock 算法:
- 在多个独立的 Redis master 节点(通常 5 个)上获取锁。
- 获取锁时使用相同的 key 和随机值,设置一个较短的过期时间(如 10 秒)。
- 客户端按顺序向所有节点请求锁,使用相同的超时时间(远小于锁过期时间)。
- 当大多数节点(N/2 + 1) 成功获取锁,且总耗时小于锁有效时间,则认为获取成功。
- 释放锁时,向所有节点发送释放请求。
Redisson 也提供了 RedissonRedLock 实现。
RedLock 的争议
- 官方推荐,但业界对其正确性有质疑(时钟漂移、网络分区等问题)。
- 大部分场景下,单节点 + 主从自动故障转移 + 业务幂等 已经足够。如果对锁安全性要求极高,可以考虑 ZooKeeper 或 etcd。
五、最佳实践与常见问题
1. 锁粒度与锁范围
- 锁应该只覆盖需要互斥的代码块,不要在整个方法上加锁。
- 锁的 key 设计要包含业务维度,如
lock:order:1001表示订单 1001 的锁。
2. 避免长时间持有锁
- 业务应快速执行,如果确实需要长时间操作,应使用看门狗续期或拆分任务。
- 如果无法预估时间,可考虑使用 Redis 的
SETEX加轮询,或改用 ZooKeeper 的临时顺序节点。
3. 超时与重试
- 获取锁时应设置合理的等待时间和超时时间,避免死等。
- 重试时采用指数退避策略,避免 Redis 压力过大。
4. 锁的监控与报警
- 监控锁的等待时间、持有时间,异常时报警。
5. 结合业务幂等
- 即使锁偶尔失效,如果能保证业务幂等(如数据库唯一索引、状态机),可以进一步提升鲁棒性。
六、完整的 Redis 分布式锁 Java 示例(基于 Jedis + Lua)
java
arduino
体验AI代码助手
代码解读
复制代码
public class RedisDistributedLock {
private Jedis jedis;
private String lockKey;
private String requestId;
private int expireSeconds;
public RedisDistributedLock(Jedis jedis, String lockKey, int expireSeconds) {
this.jedis = jedis;
this.lockKey = lockKey;
this.expireSeconds = expireSeconds;
this.requestId = UUID.randomUUID().toString();
}
public boolean tryLock() {
String result = jedis.set(lockKey, requestId, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, requestId);
}
}
使用: https://www.gaoding.com/templates/关于北京159.1415.8529北京开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于天津159.1415.8529天津开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于上海159.1415.8529上海开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于重庆159.1415.8529重庆开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于太原159.1415.8529太原开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于石家庄159.1415.8529石家庄开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于呼和浩特159.1415.8529呼和浩特开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于沈阳159.1415.8529沈阳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于吉林159.1415.8529吉林开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于长春159.1415.8529长春开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于哈尔滨159.1415.8529哈尔滨开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南京159.1415.8529南京开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于苏州159.1415.8529苏州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于杭州159.1415.8529杭州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于合肥159.1415.8529合肥开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于厦门159.1415.8529厦门开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于福州159.1415.8529福州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南昌159.1415.8529南昌开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于济南159.1415.8529济南开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于青岛159.1415.8529青岛开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于郑州159.1415.8529郑州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于武汉159.1415.8529武汉开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于长沙159.1415.8529长沙开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于深圳159.1415.8529深圳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于广州159.1415.8529广州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于东莞159.1415.8529东莞开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南宁159.1415.8529南宁开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于海口159.1415.8529海口开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于三亚159.1415.8529三亚开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于成都159.1415.8529成都开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于贵阳159.1415.8529贵阳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于昆明159.1415.8529昆明开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于拉萨159.1415.8529拉萨开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于西安159.1415.8529西安开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于兰州159.1415.8529兰州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于西宁159.1415.8529西宁开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于银川159.1415.8529银川开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于乌鲁木齐159.1415.8529乌鲁木齐开具仪器设备发票‖第一财经.html
java
csharp
体验AI代码助手
代码解读
复制代码
RedisDistributedLock lock = new RedisDistributedLock(jedis, "order:1001", 10);
if (lock.tryLock()) {
try {
// 业务处理
} finally {
lock.unlock();
}
} else {
// 获取锁失败,重试或降级
}
七、总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SET NX EX + Lua | 实现简单,性能高 | 需手动处理续期、不可重入 | 简单互斥,业务时间可控 |
| Redisson | 功能完善,自动续期,可重入 | 引入额外依赖 | 绝大多数分布式锁需求 |
| RedLock | 解决主从切换导致的锁丢失 | 实现复杂,存在争议 | 对安全性要求极高的场景 |
核心原则:
- 使用原子命令或 Lua 脚本保证锁操作的原子性。
- 释放锁时验证持有者,避免误删。
- 设置合理的过期时间,结合看门狗或业务重试。
Redis 分布式锁是解决并发问题的利器,但也要结合业务场景选择合适的方案,并做好容错与监控。
八、其他分布式方案
| 方案 | 一致性 | 性能(QPS) | 可靠性 | 实现复杂度 | 典型应用 |
|---|---|---|---|---|---|
| Redis | 弱(最终一致) | 极高(10w+) | 中(主从切换可能丢锁) | 简单 | 高并发缓存、短时锁 |
| ZooKeeper | 强 | 中(万级) | 高 | 复杂 | 配置中心、分布式协调 |
| etcd | 强 | 中(万级) | 高 | 中等 | 云原生、服务发现 |
| 数据库 | 强(单库) | 低(千级) | 依赖数据库 | 简单 | 低并发、已有数据库系统 |
选择建议
- 追求性能、允许少量锁失效 → Redis(配合业务幂等)。
- 要求强一致性、可容忍性能稍低 → ZooKeeper 或 etcd。
- 系统简单、不想引入新组件 → 数据库唯一索引。
- 云原生环境 → etcd 是首选。
