为什么不推荐使用redis的setnx进行分布式加锁

前提

为何要使用分布式锁?

分布式锁相较于传统锁(如单体服务中的锁)的主要好处在于其能够处理分布式系统中的并发访问问题,确保在多个节点或客户端之间对共享资源的互斥访问,从而保持数据的一致性和系统的稳定性。分布式锁相较于传统锁的一些主要优势:

  • 全局唯一性:
    分布式锁是全局唯一的,这意味着无论系统中有多少个节点或客户端,同一时刻只有一个节点或客户端能够获取到锁,进而对共享资源进行访问或修改。这种全局唯一性确保了数据的完整性和一致性。
  • 支持高并发:
    在分布式系统中,多个节点或客户端可能同时尝试访问相同的共享资源。分布式锁能够有效地协调这些并发请求,避免数据冲突和不一致,支持高并发的场景。
  • 扩展性和灵活性:
    随着业务的发展和系统的扩展,分布式系统可能需要添加更多的节点或客户端。分布式锁能够很好地适应这种变化,支持系统的水平扩展。同时,分布式锁的实现方式灵活多样,可以根据具体的业务需求和技术栈进行选择。

SETNX命令的特性

SETNX:向Redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。并且这个命令是原子性的。使用SETNX作为分布式锁时,添加成功表示获取到锁,添加失败表示未获取到锁。至于添加的value值无所谓可以是任意值(根据业务需求),只要保证多个线程使用的是同一个key,所以多个线程添加时只会有一个线程添加成功,就只会有一个线程能够获取到锁。而释放锁锁只需要将锁删除即可。

其实就是总结为:

  1. 获取锁:通过setnx添加
  2. 释放锁:通过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方法,其本质是以下原因导致的:

  1. 缺乏原子性:
    分布式锁的核心要求是原子性,即加锁和设置锁的过期时间应该是原子操作。然而,SETNX 命令只负责设置键的值,而不支持同时设置过期时间。这意味着你需要先调用 SETNX,然后再调用 EXPIRE 来设置锁的过期时间。这两个操作不是原子的,因此在它们之间可能发生一些导致不一致的情况,比如客户端在设置过期时间之前崩溃,导致锁永远不会被释放。
  2. 锁释放的安全性问题:
    释放锁时,需要确保只有持有锁的客户端才能成功释放。使用 SETNX 时,简单的 DEL 命令用于释放锁,但这样任何客户端都可以删除锁,导致潜在的竞态条件。为了防止这种情况,通常需要结合客户端的唯一标识来验证锁的持有者,这增加了实现的复杂性,并且容易出错。
  3. 不支持可重入锁:
    在分布式系统中,有时候同一个客户端可能需要多次获取同一个锁。使用 SETNX 实现的锁不支持可重入性,即一个客户端在持有锁的情况下再次请求锁时会被拒绝,这在实际应用中可能导致问题。
    4.虽然可以用luna脚本解决上述问题,但 我们市场上已经有成熟的方案可以去替代,比如Redisson 库等。

以上是我对这个问题的粗略看法,欢迎讨论

  • 48
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RedisSETNX命令用于在键不存在时设置键值对,如果键已经存在,则不做任何操作。利用SETNX命令可以实现简单的分布式锁。以下是一个使用SETNX实现分布式锁的示例代码: ```python import redis import time def acquire_lock(lock_name, acquire_timeout=10, lock_timeout=10): # 创建redis连接 redis_conn = redis.Redis() # 生成唯一的锁标识 lock_key = f"lock:{lock_name}" # 获取当前时间戳,用于计算锁超时时间 timestamp = int(time.time()) + acquire_timeout # 循环尝试获取锁 while int(time.time()) < timestamp: # 尝试获取锁 if redis_conn.setnx(lock_key, "locked"): # 设置锁超时时间 redis_conn.expire(lock_key, lock_timeout) return True # 短暂休眠,避免频繁尝试获取锁 time.sleep(0.1) return False def release_lock(lock_name): # 创建redis连接 redis_conn = redis.Redis() # 生成唯一的锁标识 lock_key = f"lock:{lock_name}" # 删除锁 redis_conn.delete(lock_key) ``` 在上述示例代码中,`acquire_lock`函数用于获取分布式锁,`release_lock`函数用于释放分布式锁。具体实现过程如下: 1. 创建Redis连接。 2. 生成唯一的锁标识,一般以`lock:`为前缀加上具体的锁名。 3. 计算获取锁的超时时间戳,即当前时间戳加上获取锁的超时时间。 4. 循环尝试获取锁,如果成功获取到锁,则设置锁的超时时间,并返回True;如果超过超时时间仍未获取到锁,则返回False。 5. 释放锁的过程比较简单,直接删除对应的锁标识即可。 需要注意的是,分布式锁的实现还需要考虑异常情况下的处理、防止锁被误释放等问题,上述代码仅作为示例,具体应用场景中可能需要根据实际需求进行适当修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值