解决Redis分布式锁误删问题
一、前言:你的分布式锁真的安全吗?
很多开发者以为,只要用 SET key value NX EX 就实现了“安全”的分布式锁。
但当业务执行时间较长或网络抖动时,“锁被别人删了” 的事故频频发生:
- 线程 A 的锁过期释放
- 线程 B 获取到新锁
- 线程 A 执行完后,直接删除了线程 B 的锁
- 线程 C 趁机进入 → 并发安全彻底失效
本文将手把手教你彻底解决 Redis 分布式锁误删问题,并提供可直接用于生产的 Java 工具类。
二、误删的根本原因
❌ 典型错误代码:
// 加锁
redis.set("lock:order", "locked", SET_NX, SET_EX, 10);
// 业务逻辑(可能耗时 15 秒)
processOrder();
// 解锁(危险!)
redis.del("lock:order"); // ← 谁都能删!
🔍 问题分析:
- value 不唯一:所有线程都用
"locked",无法区分持有者 - 解锁无校验:直接
DEL,不判断当前锁是否属于自己 - 锁过期 < 业务时间:导致锁提前释放,其他线程趁机获取
💥 结果:A 删除了 B 的锁,系统出现并发漏洞!
三、解决方案核心:“谁加的锁,谁才能删”
要安全解锁,必须满足两个条件:
- 每个锁的 value 必须唯一(标识持有者)
- 删除前必须校验 value 是否匹配
而这两个操作必须原子执行,否则中间可能被其他线程干扰。
✅ 唯一解法:使用 Lua 脚本!
四、生产级实现:Spring Boot + Lua 脚本
步骤 1:定义 Lua 解锁脚本
-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
📌 说明:
KEYS[1]:锁的 key(如lock:coupon:1001)ARGV[1]:当前线程的唯一标识- 只有 value 匹配时才删除,且
GET + DEL原子执行
步骤 2:封装分布式锁工具类
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final DefaultRedisScript UNLOCK_LUA;
static {
UNLOCK_LUA = new DefaultRedisScript<>();
UNLOCK_LUA.setScriptText(
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
"return redis.call('DEL', KEYS[1]) " +
"else return 0 end"
);
UNLOCK_LUA.setResultType(Long.class);
}
/**
* 尝试加锁
* @param lockKey 锁名称,如 "lock:coupon:1001"
* @param lockValue 唯一标识,建议用 UUID
* @param expireSeconds 过期时间(秒)
* @return 是否加锁成功
*/
public boolean tryLock(String lockKey, String lockValue, long expireSeconds) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(
lockKey, lockValue, Duration.ofSeconds(expireSeconds)
);
return Boolean.TRUE.equals(result);
}
/**
* 安全解锁(仅删除自己持有的锁)
* @param lockKey 锁名称
* @param lockValue 加锁时使用的唯一标识
*/
public void unlock(String lockKey, String lockValue) {
redisTemplate.execute(UNLOCK_LUA,
Collections.singletonList(lockKey),
lockValue);
}
}
步骤 3:在业务中正确使用
@Service
public class CouponService {
@Autowired
private RedisDistributedLock distributedLock;
public void receiveCoupon(Long userId, Long couponId) {
String lockKey = "lock:coupon:" + couponId;
String lockValue = UUID.randomUUID().toString(); // 唯一标识!
try {
// 尝试加锁(最多等待?可扩展为带重试)
if (!distributedLock.tryLock(lockKey, lockValue, 30)) {
throw new RuntimeException("系统繁忙,请稍后再试");
}
// 执行业务:查库存、扣减、发券...
doReceiveCoupon(userId, couponId);
} finally {
// 安全解锁(即使异常也会执行)
distributedLock.unlock(lockKey, lockValue);
}
}
private void doReceiveCoupon(Long userId, Long couponId) {
// ... 你的业务逻辑(确保 ≤ 30 秒)
}
}
✅ 关键点:
lockValue必须是本次请求唯一(UUID 最佳)unlock必须在finally块中调用- 锁过期时间要大于最大业务耗时
五、进阶优化:自动续期(Watchdog)
如果业务可能超过 30 秒,可引入看门狗机制自动续期:
// 加锁成功后启动续期任务
ScheduledFuture> renewTask = scheduler.scheduleAtFixedRate(() -> {
// 如果业务未完成,且仍持有锁,则续期
if (businessRunning && isCurrentThreadOwner()) {
redisTemplate.expire(lockKey, Duration.ofSeconds(30));
}
}, 10, 10, TimeUnit.SECONDS);
// 业务完成后取消任务
renewTask.cancel(false);
⚠️ 注意:此逻辑较复杂,推荐直接使用 Redisson(内置 Watchdog)。
六、常见误区澄清
❌ 误区 1:“用 ThreadID 做 value 就行”
问题:集群环境下,不同机器的 ThreadID 可能重复
✅ 正解:用UUID或IP + ThreadID + Timestamp
❌ 误区 2:“先 GET 再 DEL,加个 if 判断就行”
if (redis.get(key).equals(value)) {
redis.del(key); // ← 非原子!中间可能被改
}
问题:
GET和DEL之间可能被其他线程插入
✅ 正解:必须用 Lua 脚本保证原子性
❌ 误区 3:“Redlock 才是终极方案”
事实:Redis 作者 Antirez 已表示 Redlock 在多数场景过度设计
✅ 建议:单 Redis 实例 + 唯一 value + Lua 解锁,已足够安全
七、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!










