一、先搞懂:单机锁和分布式锁,根本不是一回事儿!

咱们先打个比方:

单机锁就像你家的房门钥匙—— 家里就你一个人用,钥匙放兜里,想开门就开,不用怕别人抢(单 JVM 进程内,线程间竞争锁,synchronized、ReentrantLock就能搞定)。

但如果是分布式系统,比如你做了个电商平台,部署了 3 台服务器(3 个 JVM),用户下单要扣库存。这时候 3 台机器里的线程都要改 “库存” 这个共享数据,相当于 “3 户人共用一个小区大门”—— 你家的钥匙(单机锁)肯定管不了另外两家啊!

这时候就需要分布式锁:它得是一把 “小区大门卡”,3 台机器的线程都得凭这张卡开门,而且同一时间只能有一个线程拿到卡(保证互斥),还得防止 “卡丢了门一直锁着”(避免死锁)。

二、分布式锁有哪些实现方案?别瞎选!

市面上的分布式锁方案不少,但坑也多,咱们先排排雷:

方案原理优点缺点
数据库锁用for update悲观锁 / 唯一索引不用额外中间件性能差!高并发下数据库扛不住
ZooKeeper基于临时有序节点强一致性,自动释放锁部署复杂,重连机制麻烦
Redis 锁基于键值对的原子操作轻量、高性能、易部署需要自己处理过期、误删问题

显然,Redis 是后端最常用的选择—— 但千万别以为 “用 Redis 的 set 命令就能搞定”,这里面的坑能让你调试到半夜!

三、Redis 的 set 命令:分布式锁的 “入门款”,但要注意姿势

很多同学一开始会这么写:先用setnx key value(只有 key 不存在时才设置成功,保证互斥),再用expire key 10(给锁加过期时间,避免死锁)。

但这是错的! 因为setnx和expire是两步操作,中间如果服务器宕机,锁就没了过期时间,直接变成 “死锁”!

正确姿势是用Redis 的原子 set 命令

SET lock_key unique_value NX EX 10

拆解一下这几个参数的作用:

  • NX:Only if the key does not exist(只有 key 不存在时才设置,保证同一时间只有一个线程拿到锁)
  • EX:Set the expire time to seconds(设置过期时间,单位秒,避免死锁)
  • unique_value:必须是唯一值!比如 “UUID + 线程 ID”,后面解决 “误删问题” 全靠它

这行命令把 “加锁 + 设过期” 变成了一步原子操作,完美避免了 “加锁后宕机” 的死锁问题~ 但别急,这只是入门,还有两个大问题没解决:锁过期了怎么办?锁被别人误删了怎么办?

四、Redisson:分布式锁的 “真香工具”,一键解决所有坑

手写 Redis 锁的同学,迟早会遇到这两个灵魂拷问:

  1. 我加了 10 秒过期锁,但业务逻辑要执行 20 秒,锁提前过期被别人抢了怎么办?
  1. 我执行完业务,要删锁的时候,怎么保证删的是自己的锁,不是别人的?
  1. 抢锁失败了,总不能直接返回吧?重试逻辑怎么写才优雅?

别慌!Redisson早就把这些问题封装好了,相当于给你一个 “现成的锁工具包”,拿来就能用,还不用自己填坑~ 咱们逐个看它是怎么解决的。

1. 锁过期问题:“看门狗” 机制帮你续期

比如你给锁设了 30 秒过期,但业务逻辑要执行 1 分钟 —— 这时候锁到期会被自动释放,别的线程就会抢锁,导致 “并发安全问题”。

Redisson 的解决办法是内置 “看门狗”(Watch Dog)

  • 当你用 Redisson 加锁时,如果没指定过期时间,它会默认给锁设 30 秒过期,同时启动一个 “看门狗线程”
  • 这个线程会每隔 10 秒(30 秒的 1/3)检查一次:如果当前线程还持有锁(业务没执行完),就自动把锁的过期时间续到 30 秒
  • 直到线程执行完业务,主动释放锁,“看门狗” 才会停止续期

相当于小区保安(看门狗)定期巡逻,看到你还在屋里(持有锁),就自动帮你把大门卡的有效期续上,再也不怕 “锁提前过期” 了!

代码示例(Spring Boot 中使用):

// 获取Redisson客户端
RLock redissonLock = redissonClient.getLock("stock_lock");
try {
    // 加锁:默认30秒过期,看门狗自动续期
    redissonLock.lock();
    // 执行业务:就算执行1分钟,锁也不会过期
    updateStock();
} finally {
    // 释放锁
    redissonLock.unlock();
}

2. 锁误删问题:唯一标识 + Lua 脚本原子判断

假设这么个场景:

  • 线程 A 加了 10 秒锁,但业务执行了 15 秒,锁到期自动释放
  • 线程 B 趁机拿到锁,开始执行业务
  • 这时候线程 A 的业务终于执行完了,它要释放锁 —— 但此时的锁已经是线程 B 的了!如果 A 直接删锁,就会把 B 的锁误删,导致并发问题。

Redisson 怎么解决?给锁加唯一标识,释放前先判断

  1. 加锁时,Redisson 会自动给锁值设为 “UUID: 线程 ID”(比如8f4d7b:1234),保证每个线程的锁值唯一
  1. 释放锁时,Redisson 不会直接删锁,而是先执行一段 Lua 脚本:
-- 判断锁值是不是自己的,是才删除
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
  1. Lua 脚本是原子执行的,避免了 “判断 + 删除” 两步操作的中间空隙,完美防止 “误删别人的锁”

你不用自己写这段 Lua 脚本,Redisson 的unlock()方法已经帮你封装好了 —— 调用就行,省心!

3. 抢锁失败重试问题:内置重试机制,不用自己写 while 循环

手写 Redis 锁时,抢锁失败了你得自己写 while 循环重试:

// 手写重试,麻烦又不优雅
while (!redisTemplate.opsForValue().setIfAbsent("lock", "value", 10, TimeUnit.SECONDS)) {
    // 重试间隔:硬编码,不好维护
    Thread.sleep(100);
}

Redisson 直接帮你搞定了重试逻辑,还支持配置重试次数和间隔:

// 抢锁失败后,最多重试3次,每次间隔500毫秒
boolean isLocked = redissonLock.tryLock(3, 500, TimeUnit.MILLISECONDS);
if (isLocked) {
    try {
        updateStock();
    } finally {
        redissonLock.unlock();
    }
} else {
    // 重试3次还没抢到锁,返回友好提示
    return "当前下单人数过多,请稍后再试~";
}

甚至你还能自定义重试策略,比如 “指数退避重试”(重试间隔越来越长),Redisson 都支持 —— 不用自己造轮子,香!

五、总结:分布式锁选 Redisson,准没错!

咱们回头捋一捋:

  • 单机锁管不了分布式场景,得用分布式锁
  • Redis 是分布式锁的主流选择,但手写会踩 “过期、误删、重试” 的坑
  • Redisson 通过 “看门狗续期”“唯一标识 + Lua 脚本”“内置重试”,把这些坑全填了,还封装得特别易用

最后给大家一个小建议:项目里别再手写 Redis 分布式锁了,直接集成 Redisson—— 几行代码搞定,还能少加班!

你们在项目中用分布式锁踩过哪些坑?比如 “锁没释放导致服务卡死”“重试次数没调好导致性能差”?欢迎在评论区交流~ 觉得有用的话,点赞关注走一波,下次再跟大家扒更多后端干货!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]