:
Redis 分布式锁:初级实现的问题与分析
1. 代码实现回顾
加锁逻辑 (tryLock)
利用 Redis 的 setnx (setIfAbsent) 实现互斥。
java
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
long threadId = Thread.currentThread().getId();
// set lock threadId nx ex timeoutSec
// nx: 互斥 (不存在才设置)
// ex: 设置超时时间 (防止死锁)
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
// 防止拆箱空指针异常
return Boolean.TRUE.equals(success);
}
释放锁逻辑 (unlock)
直接删除 Redis 中的 key。
java
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
调用(传入用户主键做key)
java
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
boolean isLock = lock.tryLock(120); //超时自动释放
if (!isLock) {
//获取锁失败, 返回错误或重试
return Result.fail("一个人只允许下一单");
}
//获取成功,
try {
//事务,获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
//创建订单(一人一单)
return proxy.creatVoucherOrder(voucherId);
} finally {
lock.unlock();
}

2. 当前代码引发的三大核心问题
问题一:锁超时释放(业务未执行完)
- 现象:线程 1 获取锁,业务执行时间(150s)超过了锁的超时时间(120s)。
- 后果:Redis 自动释放锁,此时线程 1 的业务还在跑,锁已经没了。
问题二:并发安全被破坏
- 现象 :
- 线程 1 锁超时被自动释放。
- 线程 2 趁机获取锁,开始执行业务。
- 此时,线程 1 和线程 2 同时在操作同一资源。
- 后果 :一人一单规则失效,可能产生数据错乱或超卖。
问题三:误删别人的锁(最严重)
- 现象 :
- 线程 1 业务执行完毕。
- 线程 1 执行
unlock()。 - 但此时锁已经被线程 2 持有了。
- 后果:线程 1 删掉了线程 2 的锁。线程 3 进来再次获取锁,并发问题雪上加霜。
3. 问题根源分析
- 锁超时与业务时间不匹配:无法准确预估业务执行时间,固定超时时间难以应对所有情况。
- 无「锁归属校验」 :
unlock()时只管删除 key,不判断这把锁是不是自己加的,极易误删。 - 缺「锁续期」机制:业务没跑完,锁不会自动续命。
4. 解决方案
方案一:改进释放锁(增加归属判断)
在删除锁之前,判断 Redis 中的 value 是否为当前线程 ID。
java
public void unlock() {
String key = KEY_PREFIX + name;
String currentValue = stringRedisTemplate.opsForValue().get(key);
String threadId = Thread.currentThread().getId() + "";
// 判断锁是不是自己的
if (threadId.equals(currentValue)) {
stringRedisTemplate.delete(key);
}
}
⚠️ 注意 :虽然解决了误删,但
判断和删除是分两步执行的,非原子操作,在高并发下仍有极小概率出错。
方案二:引入锁续期(看门狗机制)
- 原理:开启一个后台线程(守护线程),定期(如每 10 秒)检查业务是否还在执行。
- 动作:如果业务未结束,自动重置锁的过期时间。
- 效果:保证业务执行完之前,锁永远不会过期。
方案三:设置合理的超时时间
- 根据业务平均耗时设置较长的超时时间(如 30s),但这只是缓解,不能彻底解决问题。
5. 一句话总结
当前初级实现会导致 「锁提前过期 → 多线程并发 → 误删他人锁」 的连锁反应,直接破坏分布式锁的互斥性。必须引入标识校验 和锁续期机制 才能在生产环境安全使用。


🔍 为什么不用 threadId,而推荐用 UUID?
核心原因:threadId 在分布式场景下存在重复风险,无法保证全局唯一,而 UUID 可以做到绝对唯一。
1. 单机 vs 分布式的差异
- 单机环境 :
同一时刻,JVM 内的threadId是唯一的,不会重复。 - 分布式环境 (多实例/多机器):
不同 JVM 进程、不同机器上的线程,threadId完全可能重复。
比如:机器A的线程ID是1001,机器B的线程ID也可能是1001。
如果这同时还是一个user用户账户:
那 1.分布式锁照样获取成功
java
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
2.unlock判断锁是不是自己的再释放还是无效的
→ 这就会导致:不同机器的不同线程,却持有相同的 threadId,锁归属判断直接失效。
2. 你提到的那句话的影响
"但当一个线程终止后,JVM 可能会在未来将它的 ID 分配给一个新的线程。"
这句话是单机层面的风险:
- 线程A结束后,JVM 可能把
threadId=1001回收,再分配给新线程B。 - 如果锁还没过期,线程B就会误以为自己持有线程A的锁,导致误删。
结合分布式场景后,问题更严重:
- 单机内
threadId会复用 - 多机间
threadId会重复
→ 最终导致 锁归属判断完全不可靠。
3. UUID 为什么更安全?
- 全局唯一:UUID 是基于时间、机器、随机数生成的,在分布式环境下几乎不可能重复。
- 线程+实例唯一 :可以用
UUID + threadId组合,进一步保证"某个实例的某个线程"的绝对唯一标识。 - 避免复用风险:UUID 不会被 JVM 回收复用,彻底解决线程ID复用导致的误判问题。
4. 代码层面的对比
❌ 旧版(用 threadId,有风险)
java
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
- 分布式下
threadId可能重复 - 单机下
threadId可能被复用
✅ 推荐版(用 UUID,安全)
java
// 生成全局唯一的线程标识
String threadId = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
- 分布式环境下绝对唯一
- 不会被 JVM 复用
- 锁归属判断 100% 可靠
java
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间, 过期后自动释放
* @return true代表获取锁成功;false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁,解决了锁误删
*/
void unlock();
}
java
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识: UUID + 线程Id
String threadId = ID_PREFIX + Thread.currentThread().getId();
//set lock thread1 nx ex 10 (nx是互斥,ex是设置超时时间)
//这里redis存的value 一石二鸟, 解决了释放锁时判断是不是自己所属的问题。
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
//不用判断获取锁是否成功
// 但返回包装类自动拆箱有风险,若是null,得返回false啊
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程标识
String threadId = Thread.currentThread().getId() + ID_PREFIX;
//获取锁中的标识
String threadValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + threadId);
//一致才释放锁
if(threadId.equals(threadValue)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}