Redis分布式锁误删情况说明

一、前言:你是否遇到过"业务还没执行完,锁却消失了"?

在使用 Redis 实现分布式锁时,很多开发者会写出如下代码:

java 复制代码
// 加锁
redisTemplate.opsForValue().set("lock:order", "locked", Duration.ofSeconds(10));

// 执行业务逻辑(可能耗时 15 秒)
doBusiness();

// 解锁
redisTemplate.delete("lock:order");

看似没问题,但在高并发或网络延迟场景下,极有可能导致"锁被误删"------即 A 线程删除了 B 线程持有的锁!

本文将深入剖析误删的根本原因 ,并给出生产级安全解决方案


二、什么是"锁误删"?为什么它很危险?

锁误删:线程 A 删除了本应由线程 B 持有的锁。

🧨 后果:

  • 锁提前释放 → 其他线程提前获取锁 → 并发安全失效
  • 可能引发超卖、重复下单、数据错乱等严重资损问题

三、误删场景还原:一个真实案例

假设优惠券秒杀中,两个请求同时处理同一用户:

时间 线程 A(请求1) 线程 B(请求2)
T1 获取锁成功(value=A) 尝试获取锁,失败(等待)
T2 开始执行业务(预计 15s) ---
T3 锁自动过期(10s 到期) ---
T4 --- 获取锁成功(value=B)
T5 业务执行完毕,调用 DEL lock ---
T6 删除了线程 B 的锁! ---
T7 --- 线程 C 立即获取锁,与线程 B 并发执行 → 超卖!

💥 关键问题 :线程 A 在 T5 时不知道锁已不属于它,却直接删除了 key!


四、根本原因分析

❌ 错误 1:解锁时不校验持有者身份

java 复制代码
redis.delete("lock"); // 谁都能删!

❌ 错误 2:锁过期时间 < 业务执行时间

  • 导致锁提前释放,其他线程趁机获取
  • 原持有者仍以为自己持有锁

❌ 错误 3:未使用唯一标识作为 value

  • 若所有线程都用 "locked" 作为 value,无法区分持有者

五、正确做法:用唯一 ID + Lua 脚本安全解锁

✅ 核心原则:

只有加锁的线程,才能删除自己的锁!

步骤 1:加锁时使用唯一 value(如 UUID)

java 复制代码
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("lock:coupon:1001", lockValue, Duration.ofSeconds(30));

步骤 2:解锁时通过 Lua 脚本校验 value

Lua 复制代码
-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

步骤 3:Java 调用 Lua 解锁

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setScriptText(
        "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
        "return redis.call('DEL', KEYS[1]) " +
        "else return 0 end"
    );
    UNLOCK_SCRIPT.setResultType(Long.class);
}

public void unlock(String lockKey, String lockValue) {
    redisTemplate.execute(UNLOCK_SCRIPT, 
                         Collections.singletonList(lockKey), 
                         lockValue);
}

优势

  • GET + DEL 原子执行,避免中间状态
  • 只有 value 匹配的线程才能删除锁
  • 彻底杜绝误删

六、进阶问题:业务执行时间 > 锁过期时间?

即使解决了误删,若业务耗时超过锁 TTL,仍会提前释放锁

解决方案:锁自动续期(Watchdog 机制)

类似 Redisson 的 watchdog 功能:只要线程还活着,就自动延长锁有效期。

简易实现思路:
java 复制代码
// 加锁后启动后台线程
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    // 如果业务未完成,且仍持有锁,则续期
    if (businessNotDone && isLockOwner()) {
        redisTemplate.expire("lock:xxx", Duration.ofSeconds(30));
    }
}, 10, 10, TimeUnit.SECONDS);

⚠️ 注意:需配合 volatile 标志位控制生命周期,避免内存泄漏。


七、其他注意事项

1. 不要使用固定字符串作为 value

java 复制代码
// 危险!所有实例 value 相同
set("lock", "1", EX 10)

✅ 正确:每个请求生成唯一 ID(UUID / ThreadID + IP)

2. 避免 Redlock 过度设计

  • Redis 官方已不推荐 Redlock(复杂且性能低)
  • 对于大多数场景,单 Redis 实例 + 唯一 value + Lua 解锁足够安全

3. 监控锁持有时间

  • 记录加锁/解锁时间,告警异常长持有
  • 避免死锁或慢 SQL 拖垮系统

八、总结

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
努力的lpp10 小时前
SQLMap CTF 常用命令全集
数据库·web安全·网络安全·sql注入
IvorySQL10 小时前
揭开 PostgreSQL 读取效率问题的真相
数据库·postgresql·开源
努力的lpp10 小时前
SQL 报错注入
数据库·sql·web安全·网络安全·sql注入
麦聪聊数据10 小时前
统一 Web SQL 平台如何收编企业内部的“野生数据看板”?
数据库·sql·低代码·微服务·架构
山峰哥10 小时前
吃透 SQL 优化:告别慢查询,解锁数据库高性能
服务器·数据库·sql·oracle·性能优化·编辑器
TDengine (老段)11 小时前
TDengine IDMP 数据可视化——散点图
大数据·数据库·物联网·信息可视化·时序数据库·tdengine·涛思数据
Project_Observer11 小时前
工时日志在项目进度管理中扮演着怎样的角色?
数据库·深度学习·机器学习
Charlie_lll11 小时前
Redis脑裂问题处理——基于min-replicas-to-write配置
redis·后端
倔强的石头_11 小时前
kingbase备份与恢复实战(一)—— 备份体系、RPO-RTO与选型(Windows+ksql)
数据库
西门吹雪分身11 小时前
mysql之数据离线迁移
数据库·mysql