从最简单的 setnx 到 Redisson 完整方案,一步步理解分布式锁的演进过程
为什么需要分布式锁?
单机环境 vs 分布式环境
单机环境:
┌─────────────────┐
│ JVM 进程 │
│ ┌───────────┐ │
│ │ synchronized│ │ ← 单机锁可以解决
│ │ Lock │ │
│ └───────────┘ │
└─────────────────┘
分布式环境:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 服务 A │ │ 服务 B │ │ 服务 C │
│ JVM 1 │ │ JVM 2 │ │ JVM 3 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
↓
┌───────────────┐
│ 共享资源 │
│ (MySQL/Redis)│
└───────────────┘
问题:synchronized 只能锁住当前 JVM,无法锁住其他服务
解决:需要一个所有服务都能访问的"共享锁" → Redis
演进过程
第一阶段:最简单的实现
java
public boolean tryLock(String key) {
return redis.setnx(key, "1");
}
public void unlock(String key) {
redis.del(key);
}
问题:死锁
线程A 加锁 → 服务宕机 → 锁未释放 → 其他线程永远无法获取锁
第二阶段:加过期时间
java
public boolean tryLock(String key) {
redis.setnx(key, "1");
redis.expire(key, 30, TimeUnit.SECONDS);
return true;
}
问题:原子性问题
setnx 成功 → 宕机 → expire 未执行 → 死锁
第三阶段:原子性加锁
java
public boolean tryLock(String key) {
return redis.set(key, "1", "NX", "EX", 30);
}
SET key value NX EX 30
NX:不存在才设置
EX:设置过期时间(秒)
一条命令完成,原子性有保障
问题:误删别人的锁
时间线:
T1: 线程A 加锁成功,过期时间 30s
T2: 线程A 业务执行时间过长(GC/网络慢),超过 30s
T3: 锁过期自动释放
T4: 线程B 加锁成功
T5: 线程A 执行完毕,执行 unlock → 删了线程B 的锁!
T6: 线程C 加锁成功 → 线程B 和 C 同时持有锁 → 并发问题
第四阶段:锁标识 + Lua 释放
java
public boolean tryLock(String key) {
String threadId = UUID.randomUUID().toString();
return redis.set(key, threadId, "NX", "EX", 30);
}
public void unlock(String key, String threadId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.eval(script, key, threadId);
}
释放锁逻辑:
- 判断锁是不是自己的(比较 threadId)
- 是自己的才删除
- 用 Lua 脚本保证原子性
问题:锁过期但业务未执行完
业务执行 40s,锁过期 30s → 锁提前释放 → 并发问题
第五阶段:看门狗自动续期
java
public boolean tryLock(String key, String threadId) {
boolean locked = redis.set(key, threadId, "NX", "EX", 30);
if (locked) {
new Thread(() -> {
while (true) {
Thread.sleep(10000); // 每10s检查一次
if (redis.exists(key) &&
redis.get(key).equals(threadId)) {
redis.expire(key, 30); // 续期
} else {
break; // 锁不存在或不是自己的,停止续期
}
}
}).start();
}
return locked;
}
问题:不可重入
同一线程调用 methodA() 获取锁
methodA() 内部调用 methodB() 也需要同一把锁
→ 死锁(自己等自己)
第六阶段:可重入锁(Hash 结构)
java
public boolean tryLock(String key, String threadId) {
String script =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('hset', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"end " +
"if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"end " +
"return 0";
return redis.eval(script, key, threadId, 30);
}
锁结构变化:
String → Hash
原来:lock:key = "thread-1"
现在:lock:key = {
"thread-1": 2 ← 重入次数
}
逻辑:
- 锁不存在 → 创建,计数=1
- 锁存在且是当前线程 → 计数+1
- 锁存在但不是当前线程 → 返回失败
完整方案总结
┌─────────────────────────────────────────────────────────────┐
│ 分布式锁完整方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 加锁: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SET key value NX EX 30 │ │
│ │ ↓ │ │
│ │ 启动看门狗线程(每10s续期) │ │
│ │ ↓ │ │
│ │ Hash结构存储重入次数 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 解锁: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Lua脚本: │ │
│ │ if 锁是当前线程 then │ │
│ │ 计数-1 │ │
│ │ if 计数==0 then 删除锁 │ │
│ │ else 重置过期时间 │ │
│ │ end │ │
│ │ 停止看门狗 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Redisson 实现(生产推荐)
基本使用
java
@Service
public class VoteService {
@Autowired
private RedissonClient redissonClient;
public void submitVote(Long voteId) {
RLock lock = redissonClient.getLock("vote:lock:" + voteId);
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 执行业务逻辑
doVote(voteId);
} else {
throw new RuntimeException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
看门狗机制
java
// 不指定 leaseTime,启用看门狗
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 默认30s,看门狗自动续期
// 指定 leaseTime,禁用看门狗
lock.lock(10, TimeUnit.SECONDS); // 10s后强制过期,不续期
看门狗工作流程:
默认过期时间:30s
看门狗间隔:10s(过期时间/3)
自动续期:每隔10s重置为30s
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 加锁 │────→│ 启动定时 │────→│ 检查锁 │
│ 30s │ │ 任务10s │ │ 是否持有 │
└──────────┘ └──────────┘ └──────────┘
│
┌─────────────────┴─────────────────┐
↓ ↓
┌──────────┐ ┌──────────┐
│ 持有:续期│ │ 未持有: │
│ 重置30s │ │ 停止看门狗│
└──────────┘ └──────────┘
Redisson 加锁 Lua 脚本
lua
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
参数说明:
KEYS[1] = 锁名称
ARGV[1] = 过期时间
ARGV[2] = 线程标识
实际应用场景
场景1:防止重复提交
java
public void submitOrder(Long orderId) {
RLock lock = redissonClient.getLock("order:submit:" + orderId);
if (!lock.tryLock()) {
throw new RuntimeException("请勿重复提交");
}
try {
// 创建订单
} finally {
lock.unlock();
}
}
场景2:库存扣减
java
public void deductStock(Long productId, Integer count) {
RLock lock = redissonClient.getLock("stock:" + productId);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
Integer stock = getStock(productId);
if (stock < count) {
throw new RuntimeException("库存不足");
}
updateStock(productId, stock - count);
}
} finally {
lock.unlock();
}
}
场景3:定时任务防并发
java
@Scheduled(cron = "0 0 2 * * ?")
public void dailyTask() {
RLock lock = redissonClient.getLock("task:daily");
if (lock.tryLock()) {
try {
// 执行定时任务
} finally {
lock.unlock();
}
}
}
注意事项
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 锁超时设置 | 过短业务未完,过长宕机释放慢 | 看门狗自动续期 |
| 异常导致死锁 | 业务异常未释放锁 | finally 中释放锁 |
| 误删别人的锁 | 锁过期被其他线程获取 | Lua 脚本判断锁标识 |
| 主从切换丢锁 | Redis 主从同步延迟 | Redlock 算法 |
演进路线总结
v1: setnx → 死锁
v2: setnx + expire → 原子性问题
v3: set nx ex → 误删别人的锁
v4: set nx ex + Lua释放 → 锁过期业务未完
v5: + 看门狗续期 → 不可重入
v6: + Hash可重入 → 完整方案
v7: Redisson → 开箱即用
小结
Redis 分布式锁的演进过程,本质上是在解决以下问题:
- 原子性:加锁和设置过期时间必须原子
- 安全性:只能释放自己持有的锁
- 可靠性:业务未执行完锁不能过期
- 可重入:同一线程可多次获取同一把锁
生产环境推荐直接使用 Redisson,它已经帮我们解决了所有这些问题。