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 拖垮系统

八、总结

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

相关推荐
dblens 数据库管理和开发工具1 小时前
开源向量数据库比较:Chroma, Milvus, Faiss,Weaviate
数据库·开源·milvus·faiss·chroma·weaviate
珠海西格电力科技3 小时前
微电网控制策略基础:集中式、分布式与混合式控制逻辑
网络·人工智能·分布式·物联网·智慧城市·能源
草莓熊Lotso4 小时前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
Cx330❀4 小时前
从零实现Shell命令行解释器:原理与实战(附源码)
大数据·linux·数据库·人工智能·科技·elasticsearch·搜索引擎
岁岁种桃花儿10 小时前
MySQL从入门到精通系列:InnoDB记录存储结构
数据库·mysql
jiunian_cn11 小时前
【Redis】hash数据类型相关指令
数据库·redis·哈希算法
冉冰学姐12 小时前
SSM在线影评网站平台82ap4(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm框架·在线影评平台·影片分类
知识分享小能手13 小时前
SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019数据库的操作(2)
数据库·学习·sqlserver
踩坑小念14 小时前
秒杀场景下如何处理redis扣除状态不一致问题
数据库·redis·分布式·缓存·秒杀