锁其实我们已经有了一定的了解了,尤其是对于数据库锁来说,但是redis锁他有什么作用呢?我们都知道redis是一个读写单线程的,那加锁还有一样吗?有的,当然是有的,和数据库的锁相比,redis的锁其实是一种业务锁,他锁的是这个业务,而数据库的锁,他锁的是这个数据,这个就是他们最大的区别。
而我们的java本身也是有锁的,但是并不是分布式的,因为java的锁只能管当前JVM中的线程,然后是分布式服务那么java的锁就没有用了,得需要借助其他的中间件。
分布式锁的特性
独立性
这个很好理解,就是同一时刻只有一个线程持有该锁。总不能A和B都有各干各的不是乱套了
高可用
首先就是在高并发的情况下,系统的性能可以得到保障,不能因为加锁也导致系统阻塞,服务不可用吧,其次就是在redis集群的情况下,然后一个节点挂了,不能因为挂了个节点导致获取锁或者释放锁失败
防死锁
必须有兜底机制,不能因为一个业务卡住导致一直持有锁,一定要设置超时机制或者撤销机制
不乱抢
自己只能获取自己的锁,释放自己的锁,不能把别人的锁给抢了或者释放了,这个是怎么来的呢,就是一个A业务卡住啦,超时释放啦锁,刚好这个时候B获取啦这个锁,等到A回过头来,把B的锁给释放啦,那B不就懵逼啦。
重入性
是指同一个线程(或进程)可以多次获取同一把锁而不会导致死锁。也就是我们的递归或者嵌套方法,如果不能同一把锁的话,就会导致死锁,因为一直没被释放,而且这个方法一定会死锁。
setnx
这个我们应该很熟悉啦,就是只有存在才能设置,最简单的分布式锁就是使用这个命令,获取锁其实就是set锁,等到key过期或者等到线程释放那么就是删除啦这个key,其他线程才能再次set再次获取,很好理解吧,那为什么会是分布式的呢?其实也很好理解,因为不管是主从、哨兵、集群都是多个从机,而且set只能是主机,所以就是分布式的啦。
但是redis2的时候还不能一条命令直接设置,需要使用setnx设置成功后,要给这个key设置过期时间,这个设置时间的过程可能就有其他命令篡改了,所以之前有setnx+lua脚本组合实现分布式锁的方式,因为lua脚本是原子的,但是lua脚本主包没学过,所以有兴趣的小伙伴自行修炼吧。
# ❌ SETNX (SET if Not eXists) - 2.6.12之前
SETNX key value
EXPIRE key seconds
# 需要两条命令,不是原子操作
# ✅ SET 带NX参数 (SET with NX) - 2.6.12及之后
SET key value NX EX seconds
# 一条命令,原子操作
问题
| 问题/缺点 | SETNX+EXPIRE方案 | SET with NX EX方案 | 严重程度 | 影响 |
|---|---|---|---|---|
| 原子性问题 | ⚠️ 两条命令非原子 | ✅ 单条命令原子 | 严重 | 可能导致死锁 |
| 死锁风险 | ⚠️ 高(崩溃导致锁不过期) | ✅ 低 | 严重 | 系统不可用 |
| 时钟漂移 | ⚠️ 受系统时钟影响 | ⚠️ 受系统时钟影响 | 中 | 锁提前/延迟释放 |
| 网络延迟 | ⚠️ 可能过期时间不准确 | ⚠️ 可能过期时间不准确 | 中 | 锁时间不准确 |
| 可重入性 | ❌ 不支持 | ❌ 不支持 | 高 | 复杂业务受限 |
| 自动续期 | ❌ 不支持 | ❌ 不支持 | 中 | 长任务风险 |
| 锁误删 | ⚠️ 可能(需配合Lua) | ⚠️ 可能(需配合Lua) | 高 | 数据不一致 |
| 集群问题 | ⚠️ 主从同步延迟 | ⚠️ 主从同步延迟 | 中 | 锁失效 |
| 性能影响 | ⚠️ 两次网络往返 | ✅ 一次网络往返 | 中 | 延迟增加 |
| 实现复杂度 | ⚠️ 较高 | ⚠️ 中等 | 低 | 维护成本 |
我们一个一个来说哈,首先是这个原子性问题,刚刚我们已经说过了,然后就是这个死锁,如果setnx成功后服务器挂了,不是redis哦,那么这个key就永远不会过期,就死锁了。
然后就是时钟和网络延迟,时钟就是服务器的时间可能和redis的时间有点不同,那么过期的时间就会有冲突,网络延迟就是从机还没同步好,导致其他线程误以为没有尝试获取锁。
可重入性这个上面也有说到,其实setnx和set也可以实现这个,就是value使用分布式机器id,这样使用递归的时候,同一个机器的肯定是可以获取到锁的。
自动续期就是一些业务设置的过期时间是30秒,但是由于GC或者其他原因导致60秒才能完成,这样的可能就会出现吴删除,就是把其他线程的key给删除了,因为执行完肯定是要删除key的嘛,可以使用看门狗机制进行续期,就是只要任务没成功,看门狗这个任务没释放,那么定期就给key续命。
误删除刚刚已经说了,只是结合lua会更加安全,也就是删除前先检查是不是直接的key再删除,但是也可能存在误删,比如同一个机器的同一个业务,但是也有解决办法,比如利用系统时间加一个盐得到一个数,把他和分布式机器id结合,那么误删除率就大大减少了。
Redisson
下面这个是简单的对比表格,可以抗到redisson要比普通的setnx强大太多了,这个也是无可厚非的,因为redisson怎么说也是一个框架或者叫做一个中间件,和消息队列一样专业的事情交给专业的人来干。
| 度 | Redisson方案 | SETNX方案 |
|---|---|---|
| 成熟度 | ✅ 生产级框架 | ⚠️ 基础实现 |
| 易用性 | ✅ 高(API简单) | ❌ 低(需手写) |
| 安全性 | ✅ 高(防各种异常) | ⚠️ 中(需完善) |
| 性能 | ✅ 高(优化好) | ✅ 高(原始操作) |
| 功能完整性 | ✅ 丰富功能 | ❌ 基础功能 |
| 维护成本 | ✅ 低(社区维护) | ⚠️ 高(自维护) |
| 学习成本 | ⚠️ 中(需学框架) | ✅ 低(简单) |
这个玩意怎么用这里就不说啦,是一个非常强大的框架,解决的就是redis分布式锁的关系,通过lua脚本和看门狗机制保障不会出现setnx出现的问题。看门狗机制给key续命,之前我们讲缓存双写的时候也说过看门狗机制,简单来说,看门狗机制就是一种保障机制,开启业务后另外再开一个线程,只要主业务没报错,就会定期的去redis中给key续命,通常为过期时间的二分之一,直到业务结束,使用lua脚本释放锁,然后再释放这个看门狗线程。如果主业务报错啦,看门狗机制就会执行预定的错误逻辑,比如重试或者发送日志等操作。
这里再简单讲一下redisson的流程,他其实有2个时间,一个是存活时间,一个是等待时间,如果在等待时间内抢到啦锁,就执行上门说的看门狗流程,如果没抢到就放回错误。
RedLock红锁
RedLock 是Redis作者Antirez 提出的一个分布式锁算法,用于在多个独立Redis实例上实现高可用分布式锁。我们上面说的redisson也是支持红锁的,但是现在却不建议使用了,因为他还是会出现超卖的问题。
其实上面讲的不管是setnx还是redisson还是现在的redlock都会存在超卖的问题,只是出现概率的多少的问题,因为我们这个是分布式锁,而且写入值只能在主机,那么主从复制的时候因为是异步的就一定会有超卖的问题。
而redlock就是要求向所以节点请求加锁,只有达到n/2+1个节点都加锁成功啦,才算获取到锁了,如果没有或者超过了锁过期时间到一半,那么就认为获取锁失败了,锁会自动过期。看起来好像解决了分布式锁不一致的情况,其实并没有完全解决,如果一个节点加锁成功了,但是还没有同步到从机就挂了,那么从机上位就不会有这个锁,那么其他线程请求这个锁就会被成功,那么还是会出现误删除或者锁竞争的问题。
另外还有时钟不同步问题:RedLock 依赖系统时间计算锁过期时间。若节点间时钟漂移(如某节点时间快),可能导致锁提前失效,其他客户端可重复获取锁,破坏互斥性。
GC停顿导致锁失效:客户端A在部分节点加锁后发生GC停顿(如JVM STW),锁因超时自动释放。客户端B成功获取锁并操作资源,而客户端A恢复后误以为仍持有锁,导致数据并发冲突。
网络延迟敏感:需等待多数节点(N/2+1)响应才能加锁成功,高延迟环境下性能显著下降。
维护成本高:需部署多个独立Redis主节点(通常≥5个),且需保证节点无主从复制关系,增加运维复杂度。
结论
其实红锁还是个不错的解决方案,但是只针对于允许部分出错的业务场景,比如推送消息之类的简单业务场景,redisson推荐的替代方案是:
Redisson 普通锁(RLock)
他其实和红锁很相似,区别就是加锁后通过WAIT命令等待异步复制到从节点(需Redis 3.0+),降低主从切换导致锁失效的概率。看门狗机制还是有的,只是为了获得更低的容错率,获取锁的时间就变长了,没办法这个因为需要等待异步复制。
ZooKeeper的分布式锁
如果是对业务有强一致的需求,那么使用专业的框架ZooKeeper 基于临时有序节点和Watcher机制,确保锁互斥性与释放可靠性。客户端会话结束(如宕机)时,临时节点自动删除,避免死锁。金融交易、库存扣减等高一致性要求的场景。如使用Apache Curator的InterProcessMutex。这里介绍一个很好的文章,大家可以去看看https://bbs.huaweicloud.com/blogs/459097
总结
本篇主要说了分布式锁。