Java分布式锁实战:基于Redis的可靠实现与避坑指南(附完整案例)
目录导读
- 为什么需要分布式锁? —— 从超卖问题说起
- Redis实现分布式锁的三个核心原则
- 方案演进:从SETNX到Redisson的完整代码案例
- 1 基础版:SETNX + 过期时间(易踩坑)
- 2 进阶版:Redisson看门狗机制(生产级推荐)
- 常见问题深度问答(FAQ)
- 性能与可靠性权衡:锁粒度与续期策略
- 如何选择适合你的锁方案
为什么需要分布式锁?
在单体应用中,synchronized 或 Lock 即可保证线程安全,但在微服务或集群部署下,多个JVM进程共享同一资源(如数据库库存),本地锁无法跨进程互斥,经典案例:电商秒杀系统,若不对库存扣减操作加锁,高并发下会出现“超卖”,分布式锁的核心需求是:在分布式环境中,确保同一时刻只有一个客户端能执行临界区代码。

Redis实现分布式锁的三个核心原则
- 互斥性:任意时刻,锁只能被一个客户端持有。
- 安全性:避免死锁,即锁必须设置过期时间,防止客户端宕机后锁永久占用。
- 可重入性(可选):同一个线程可重复获取同一把锁,避免自身死锁。
方案演进:从SETNX到Redisson的完整代码案例
1 基础版:SETNX + 过期时间(易踩坑)
错误示范(早期代码):
// 问题:两步操作非原子性,若设置过期时间前宕机,锁无法释放
if (redisTemplate.opsForValue().setIfAbsent("lock", "1")) {
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
// 业务逻辑...
}
正确姿势(原子操作):
// 使用SET key value NX EX 保证原子性
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("product:1001", "order-service", 30, TimeUnit.SECONDS);
if (locked) {
try {
// 执行业务:扣减库存、创建订单
} finally {
// 释放锁时需校验持有者身份,防止误删他人锁
String owner = redisTemplate.opsForValue().get("product:1001");
if ("order-service".equals(owner)) {
redisTemplate.delete("product:1001");
}
}
}
⚠️ 隐患:若业务执行超过30秒,锁自动释放,其他线程可进入,造成并发问题,此时需要“看门狗”机制。
2 进阶版:Redisson看门狗机制(生产级推荐)
引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
核心代码:
@Autowired
private RedissonClient redissonClient;
public void deductStock(Long productId) {
RLock lock = redissonClient.getLock("stock:lock:" + productId);
try {
// 尝试加锁,最多等待5秒,锁30秒后自动过期(可配置)
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 执行库存扣减逻辑...
System.out.println("线程:" + Thread.currentThread().getName() + " 成功获取锁并执行业务");
} else {
System.out.println("获取锁失败,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 注意:若锁持有者线程已释放,finally中只会删除自身持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
原理:Redisson内部通过Lua脚本保证加锁与设置过期时间的原子性,并启动“看门狗”定时任务,默认锁的超时时间为30秒,每10秒自动续期一次(若业务未结束),这完美解决了业务长于锁过期时间的痛点。
常见问题深度问答(FAQ)
Q1:为什么不能直接用SETNX+EXPIRE?
A:两个命令非原子,中间宕机会导致锁永不释放,引发死锁。
Q2:使用Redisson时,锁自动续期是否会耗尽CPU?
A:不会,看门狗是Netty线程池中的延迟任务,仅在锁存在时每10秒触发一次,业务结束立即取消,开销极低。
Q3:redis主从切换时,锁丢失怎么办?
A:Redis官方建议使用RedLock(多节点独立锁),但RedLock本身有争议,更稳妥的方案是配合ZooKeeper或Etcd实现分布式锁(CP模型),对于多数业务场景,可用Redis主从+哨兵保证高可用。
Q4:锁的粒度如何设计?
A:锁粒度越细,并发越高,库存扣减应按商品ID加锁,而不是将所有商品用同一把锁,但需要注意锁键的碰撞概率与Redis内存占用。
Q5:业务抛异常时,锁是否一定能释放?
A:必须在finally块中释放锁,并检查isHeldByCurrentThread(),避免抛出IllegalMonitorStateException。
性能与可靠性权衡:锁粒度与续期策略
| 策略 | 优点 | 缺点 |
|---|---|---|
| 细粒度锁(按商品ID) | 高并发 | 锁数量多,内存开销大 |
| 粗粒度锁(全局) | 简单 | 吞吐率低 |
| 固定过期时间 | 实现简单 | 不适用于长任务 |
| 看门狗续期 | 自适应 | 依赖Redisson库 |
优化建议:
- 业务执行时间稳定时,可设置一个合理的过期时间(如业务最大耗时的5倍),并关闭续期,减少网络交互。
- 关键业务建议开启看门狗。
- 考虑用
tryLock(waitTime, leaseTime)控制等待与持有时间,避免线程无限等待。
如何选择适合你的锁方案
- 简单业务(秒杀、优惠券):推荐使用Redisson,代码侵入小,开箱即用。
- 对一致性要求极高(金融交易):优先选择ZooKeeper或Etcd。
- 避免自研轮子:在涉及主从切换、重复加锁、可重入等问题上,成熟框架已处理边界情况,自研极易出Bug。
最终建议:在你的Spring Boot项目中集成Redisson,既满足高并发性能,又规避了底层复杂的分布式一致性实现,监控Redis内存与慢查询日志,确保锁键回收及时。
(文章结束)