【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变

前言:当 synchronized 不再有效

兄弟们,欢迎来到 Redis 进化日志的第五天。在前四天里,我们夯实了 Redis 的基础(数据结构、持久化、高可用)。今天,我们要聊一个后端开发中极具分量的话题------分布式锁

在单体应用时代,遇到并发问题(比如扣减库存),我们习惯用 Java 自带的 synchronizedReentrantLock。但在现在的微服务架构下,服务往往是多节点部署的。

  • 问题核心:JVM 级别的锁只能管住当前这台服务器内部的线程,管不住其他服务器的线程。

  • 解决方案:我们需要一个独立于所有应用服务器之外的"第三方组件"来统一管理锁,Redis 恰好就是最合适的人选。

今天我们像剥洋葱一样,模拟一个分布式锁是如何一步步修复,最终进化成完全体的。


一、 雏形阶段:简单的 SETNX

Redis 提供了一个命令 SETNX,逻辑很简单:如果 Key 不存在,则设值成功(拿锁);如果 Key 存在,则设值失败(排队)。

代码实现 V1.0:

java 复制代码
/**
 * 阶段一:最原始的分布式锁
 * 存在严重死锁风险
 */
public void seckillV1() {
    String lockKey = "product_001";
    
    // 1. 尝试加锁
    // 对应 Redis 命令:SETNX product_001 locked
    Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");
    
    if (result) {
        // 加锁成功
        try {
            // 2. 执行业务逻辑
            doBusiness(); 
        } finally {
            // 3. 释放锁
            // 【潜在风险】:如果代码执行到 doBusiness() 时服务器宕机或断电,
            // finally 块永远不会被执行。Redis 里的 lockKey 将永远存在。
            // 导致后续所有线程都无法拿到锁,形成【死锁】。
            redisTemplate.delete(lockKey);
        }
    }
}

二、 改进阶段:引入过期时间 (Expire)

为了解决 V1 版本宕机导致的死锁问题,最直观的办法是给锁加一个过期时间 (TTL)。即使服务宕机,Redis 也会在一段时间后自动删除这个 Key。

代码实现 V2.0(错误示范):

java 复制代码
// ... 加锁成功后 ...
if (result) {
    // 【原子性问题】:
    // "加锁"和"设置过期时间"是两步独立的操作。
    // 如果刚执行完 setIfAbsent,还没来得及执行 expire,服务器这就挂了...
    // 结果依然是死锁。
    redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS); 
    
    // ... 执行业务 ...
}

代码实现 V2.1(正确姿势):

Redis 从 2.6.12 版本开始,扩展了 SET 命令,支持原子性操作。

java 复制代码
/**
 * 阶段二:利用原子命令解决死锁
 * 但仍存在"误删锁"风险
 */
public void seckillV2() {
    String lockKey = "product_001";
    
    // 1. 原子加锁:同时设置 NX (互斥) 和 EX (过期时间)
    // 对应 Redis 命令:SET product_001 locked NX EX 10
    Boolean result = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS); 
        
    if (result) {
        try {
            doBusiness();
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

三、 进阶阶段:解决"误删锁"问题

V2.1 看起来已经不错了,但在高并发下的极端网络延迟场景中,依然有 Bug。

场景推演:

  1. 线程 A 拿到锁,过期时间 10 秒。

  2. 线程 A 业务卡顿,执行了 15 秒。此时第 10 秒时锁自动失效。

  3. 线程 B 进场,拿到新锁。

  4. 线程 A 终于执行完了,运行 finally 块中的 delete

  5. 后果 :线程 A 删掉的不是自己的锁,而是线程 B 刚加上的锁。系统锁机制失效。

解决方案:

解铃还须系铃人。我们在加锁时存入一个唯一标识(UUID),删除前判断一下:"这是我的锁吗?"

java 复制代码
/**
 * 阶段三:引入 UUID 和 Lua 脚本
 * 保证"加锁者"和"解锁者"是同一个人
 */
public void seckillV3() {
    String lockKey = "product_001";
    // 生成当前线程的唯一标识
    String uuid = UUID.randomUUID().toString();

    // 1. 加锁:Value 存入 UUID
    Boolean isLocked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS);

    if (isLocked) {
        try {
            doBusiness();
        } finally {
            // 2. 释放锁:必须使用 Lua 脚本保证原子性
            // 逻辑:如果 GET 到的值等于我的 UUID,则 DEL;否则返回 0。
            String script = 
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
            
            // 执行 Lua 脚本
            redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class), 
                Collections.singletonList(lockKey), // KEYS[1]
                uuid // ARGV[1]
            );
        }
    }
}

四、 完善阶段:续期问题与 Redisson

V3 版本通过 Lua 脚本已经非常严谨了,但还有一个痛点:过期时间设多少合适?

  • 设短了(如 5s):业务没跑完锁就丢了。

  • 设长了(如 60s):万一服务器挂了,其他线程要等 60s 才能拿锁,用户体验极差。

我们需要一种**"自动续期"**机制:业务只要还在跑,锁的时间就自动延长。

Java 社区最成熟的 Redis 框架 Redisson 帮我们完美解决了这个问题。

1. Redisson 的"看门狗" (WatchDog) 机制

当你调用 Redisson 的 lock() 方法时,它会在后台启动一个定时任务(看门狗):

  1. 默认加锁 30 秒。

  2. 每隔 10 秒(lockWatchdogTimeout / 3)检查一次。

  3. 如果当前线程还持有锁,就通过 Lua 脚本把过期时间重置回 30 秒

  4. 如果服务宕机,看门狗线程消失,不再续期,锁在 30 秒后自动释放。

2. 最终版代码(推荐在生产环境使用)
java 复制代码
@Autowired
private RedissonClient redisson;

/**
 * 阶段四:使用 Redisson 实现工业级分布式锁
 * 支持可重入、自动续期、阻塞等待
 */
public void seckillFinal() {
    String lockKey = "product_001";
    // 1. 获取锁对象(此时还没真正加锁)
    RLock lock = redisson.getLock(lockKey);

    try {
        // 2. 加锁
        // - 默认过期时间 30s
        // - 自动启动看门狗线程进行续期
        // - 支持可重入(底层是 Hash 结构,记录加锁次数)
        lock.lock(); 
        
        // 3. 执行业务
        // 哪怕业务执行 1 分钟,看门狗也会不断续期,锁不会断
        doBusiness(); 
        
    } finally {
        // 4. 释放锁
        // 严谨判断:当前锁是否存在?是否是当前线程持有的?
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            // 解锁,同时会停止后台的看门狗线程
            lock.unlock(); 
        }
    }
}

五、 面试常见追问:关于 Redlock

Q:Redisson 方案已经完美了吗?如果 Redis 主节点挂了怎么办?

A: 这是分布式系统中的经典问题。

  • 场景:线程 A 在 Master 节点加锁成功,但数据还没同步到 Slave,Master 突然挂了。Slave 晋升为 Master,线程 B 去新 Master 加锁也能成功。此时 A 和 B 同时持有锁,互斥失效。

  • 官方方案 (Redlock):

    Redis 之父提出了 Redlock 算法,要求部署 5 个独立的 Redis 节点,加锁时必须在超过半数(3个)节点上都加锁成功才算有效。

  • 实际工程建议:

    不推荐使用 Redlock。

    1. 成本过高:维护 5 个独立 Redis 实例成本太高。

    2. 依然不保证 100%:分布式专家证明了在严重网络延迟下 Redlock 依然可能有问题。

    3. 结论 :如果你的业务是金融级 (坚决不能错),请放弃 Redis 分布式锁,改用 Zookeeper数据库悲观锁 (CP 模型)。对于绝大多数互联网业务(秒杀、防重提交),Redisson + 主从架构 已经足够优秀了。


总结:Redis 分布式锁进化论

回过头看,我们从最简陋的代码一步步优化到工业级方案。为了方便大家记忆,我把这四个阶段整理成了里程碑 ,每个阶段我们先看它到底是个啥

🚩 V1.0:石器时代 (SETNX)
  • 这是什么 :这是利用 Redis 单线程特性的最原始手段,相当于**"占坑模式"**。谁先抢到坑位(Key),谁就拥有锁。

  • 实现方式SETNX key value

  • 致命缺陷死锁风险。如果抢到坑位的人突然"挂了"(服务器宕机),坑位永远不会释放,后面的人永远进不来,业务直接瘫痪。

🚩 V2.0:青铜时代 (Atomic SET)
  • 这是什么 :这是给锁加装了**"自动销毁装置"**。利用 Redis 的 TTL(过期时间)机制,保证锁不会永久存在。

  • 实现方式SET key value NX EX 10(原子命令)。

  • 致命缺陷误删锁风险 。如果业务执行时间太长(超过了 TTL),锁会自动销毁。此时不仅锁失效了,那个超时的线程执行完后,还会把别人的锁给误删掉,导致系统"裸奔"。

🚩 V3.0:白银时代 (Lua Script + UUID)
  • 这是什么:这是给锁发了一张**"身份证"**。在删除锁之前,必须先核对身份(UUID),并利用 Lua 脚本保证"验身+删除"是不可打断的。

  • 实现方式:Value 存 UUID + Lua 脚本删锁。

  • 致命缺陷续期难题。锁的过期时间到底设多少?这是一个死局:设长了影响性能,设短了怕业务跑不完。我们需要一个能自动"续杯"的机制。

🚩 V4.0:王者时代 (Redisson WatchDog)
  • 这是什么 :这是一个全自动化的"管家" 。它不仅帮我们加锁,还在后台启动了一个**"看门狗"线程**,专门负责监测业务状态并自动续期。

  • 实现方式:Redisson 框架 + Netty 时间轮。

  • 地位 :彻底解决了续期和可重入问题,是目前 Java 后端领域最推荐、最成熟的分布式锁方案。


💡下期预告

锁的问题搞定了,但如果有人恶意攻击,疯狂查询不存在的数据,直接穿透缓存打到数据库怎么办?

【Day 6】缓存的三大经典问题:穿透、击穿、雪崩,到底怎么防?(附布隆过滤器实战)

关注专栏,带你接着卷!

相关推荐
hanqunfeng2 小时前
(四十)SpringBoot 集成 Redis
spring boot·redis·后端
lendsomething2 小时前
Spring 多数据源事务管理,JPA为例
java·数据库·spring·事务·jpa
難釋懷2 小时前
Jedis快速入门
redis·缓存
nsjqj2 小时前
JavaEE初阶:多线程初阶(2)
java·开发语言
玩转数据库管理工具FOR DBLENS2 小时前
人工智能:演进脉络、核心原理与未来之路 审核中
数据库·人工智能·测试工具·数据库开发·数据库架构
晓风残月淡2 小时前
高性能MYSQL(四):查询性能优化
数据库·mysql·性能优化
cab52 小时前
MyBatis如何处理数据库中的JSON字段
数据库·json·mybatis
黎雁·泠崖2 小时前
Java面向对象:对象数组核心+综合实战
java·开发语言
天若有情6732 小时前
用MySQL+BI工具搭建企业级数据可视化看板:从数据准备到动态展示全攻略
数据库·mysql·信息可视化