前提
为何要使用分布式锁?
分布式锁相较于传统锁(如单体服务中的锁)的主要好处在于其能够处理分布式系统中的并发访问问题,确保在多个节点或客户端之间对共享资源的互斥访问,从而保持数据的一致性和系统的稳定性。分布式锁相较于传统锁的一些主要优势:
- 全局唯一性:
分布式锁是全局唯一的,这意味着无论系统中有多少个节点或客户端,同一时刻只有一个节点或客户端能够获取到锁,进而对共享资源进行访问或修改。这种全局唯一性确保了数据的完整性和一致性。 - 支持高并发:
在分布式系统中,多个节点或客户端可能同时尝试访问相同的共享资源。分布式锁能够有效地协调这些并发请求,避免数据冲突和不一致,支持高并发的场景。 - 扩展性和灵活性:
随着业务的发展和系统的扩展,分布式系统可能需要添加更多的节点或客户端。分布式锁能够很好地适应这种变化,支持系统的水平扩展。同时,分布式锁的实现方式灵活多样,可以根据具体的业务需求和技术栈进行选择。
SETNX命令的特性
SETNX:向Redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。并且这个命令是原子性的。使用SETNX作为分布式锁时,添加成功表示获取到锁,添加失败表示未获取到锁。至于添加的value值无所谓可以是任意值(根据业务需求),只要保证多个线程使用的是同一个key,所以多个线程添加时只会有一个线程添加成功,就只会有一个线程能够获取到锁。而释放锁锁只需要将锁删除即可。
其实就是总结为:
- 获取锁:通过setnx添加
- 释放锁:通过del将锁删除
SETNX分布式锁
设置过期时间防止死锁
假设线程1通过SETNX获取到锁并且正常执行然后释放锁剩下的线程就可以继续去拿到锁。但是如果此时线程1一直不释放锁,此时其它线程就无法向下继续执行,因为锁在线程1手中。这种长期不释放锁情况就有可能造成死锁。
为了防止像线程1这种情况发生,我们可以设置key的过期时间来解决。
JAVA代码:
//通过java代码实现SETNX同时设置过期时间
//key--键 value--值 time--过期时间 TimeUnit--时间单位枚举
stringRedisTemplate.opsForValue().setIfAbsent(key, value , time, TimeUnit);
SETNX分布式锁时误删情况
情况一
设置过期时间处理了死锁问题,但此时新的情况又出现了,线程1的锁过期之后,在其他线程获得锁,就将锁删除,此时就会有问题出现。
线程1乃至其它线程都不知道删除的是别人的锁,全部线程都以为删除的是自己的锁。直到最后一个线程无锁可删。
这种误删锁的情况让锁的存在荡然无存,本来应该串行执行的线程,在一定程度上都开始并发执行了。
那么误删情况该如何解决了?
我们可以给锁加上线程标识,只有锁是当前线程的才能删除,否则不能删除。在添加key的时候,key的value存储当前线程的标识,这个标识只要保证唯一即可。可以使用UUID或者一个自增数据。在删除锁的时候,将线程标识取出来进行判断,如果相同就表示锁是自己的能够删除,否则不能删除。
获取锁和释放锁:
情况二
加入线程标识后,线程一不能随便删除其它线程的锁,但是问题又出现了,因为这个获取锁值和释放锁是分两步走的,不是原子性的。那假设线程1在判断是自己的锁之后进入方法体,这边没有继续往下走(可能是睡眠了,可能是一直在执行方法体),此时key过期了,线程2进来拿到锁,线程1此时醒了又把锁删了,那就回到了情况1的问题,此时就需要将判断锁的操作和删除锁的操作作为一个整体执行,要么全部成功,要么全部失败,保证删除锁的原子性。
如何处理?
这块需要用到luna脚本,redis执行luna脚本,java进行执行。这块Luna脚本不是很会,但大致是这么做的。
总结
通过上面的分析,我们可以清楚的了解了分布式锁为什么不推荐使用redis的setnx方法,其本质是以下原因导致的:
- 缺乏原子性:
分布式锁的核心要求是原子性,即加锁和设置锁的过期时间应该是原子操作。然而,SETNX 命令只负责设置键的值,而不支持同时设置过期时间。这意味着你需要先调用 SETNX,然后再调用 EXPIRE 来设置锁的过期时间。这两个操作不是原子的,因此在它们之间可能发生一些导致不一致的情况,比如客户端在设置过期时间之前崩溃,导致锁永远不会被释放。 - 锁释放的安全性问题:
释放锁时,需要确保只有持有锁的客户端才能成功释放。使用 SETNX 时,简单的 DEL 命令用于释放锁,但这样任何客户端都可以删除锁,导致潜在的竞态条件。为了防止这种情况,通常需要结合客户端的唯一标识来验证锁的持有者,这增加了实现的复杂性,并且容易出错。 - 不支持可重入锁:
在分布式系统中,有时候同一个客户端可能需要多次获取同一个锁。使用 SETNX 实现的锁不支持可重入性,即一个客户端在持有锁的情况下再次请求锁时会被拒绝,这在实际应用中可能导致问题。
4.虽然可以用luna脚本解决上述问题,但 我们市场上已经有成熟的方案可以去替代,比如Redisson 库等。
以上是我对这个问题的粗略看法,欢迎讨论