【Redis】一种常见的Redis分布式锁原理简述

本文主要简述一下基于set命令的Redis分布式锁的原理。

一,a线程持有的锁不要被b线程同时持有→setnx

抢锁的时候,最核心的就是,a线程持有的锁不要被b线程同时持有,放在基于set命令的redis分布式锁中来看,就是“如果锁(key)存在我就不能抢,锁(key)不存在我才能抢”,

用原子的redis命令来说就是

setnx key value

key可以设置为你业务中要抢的锁的名字,和其他key区分开。

一开始,redis提供了set、setex(设置key value的同时设置过期时间)、setnx(如果不存在key就设置key value)。

二,如果锁被别人释放了怎么办→ThreadLocal和UUID

一个比较常见的担心是,如果a线程加的锁,被b线程释放了怎么办,毕竟只是一个set命令,用del就可以删除,也就是解锁,那怎么办呢。

在前面我们只设置了key的名字,对value没有做要求。其实我们可以给value加一个唯一ID,让别的线程识别到这个锁是否是自己持有。

线程id是否可以呢,我们直接用线程的id作为value,让线程看一下是否是自己的id,简单直接。答案是不可以,线程id在机器中是唯一的,但是这是分布式锁,多个机器之间,线程id不唯一。

我们倒也不用直接跳到分布式id这么远,其实uuid足矣。uuid中包括了机器的IEEE识别号(如果有网卡,从网卡的mac地址获得),优点是全球唯一且简单、代码方便,uuid不用于自增主键的原因是它不区域递增,以及可能造成网卡的mac地址泄露(曾用于寻找梅丽莎病毒制作者)。

此时我们就需要用到ThreadLocal,让每个线程私有uuid。就这样,我们加入了ThreadLocal和UUID,解决了锁被其他线程释放的问题。

三,锁无人释放问题→加过期时间

1,只靠setnx命令,死锁无法释放

那么紧接着问题来了, 如果加锁的线程挂掉了,导致锁无法正常释放怎么办?要知道,redis的数据如果没有特意设置过期时间,默认的是永不过期。所以我们要设置一个过期时间。

redis中设置过期时间的命令为expire命令,我们当然可以使用expire来给上面的setnx命令来加过期时间,如下:

setnx lock XXX
expire lock 10 //给lock键设置过期时间为10秒

2,setnx+expire命令不够原子

第二个问题来了,setnx+expire命令不够原子,即如果有线程只执行了加锁命令setnx,在第二个命令expire执行之前就挂掉了,又怎么办呢?因为这两个是分开执行的,意味着是存在这种可能的。因此我们就要想方法来将其一次执行。

(1),set key value ex second nx

正如一开始所述,redis在一开始只提供了set、setnx、setex几个命令,却没有其他参数,因此要执行必须分开执行。

在 Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,使我们可以直接在一条命令中完成所有操作,即如果不存在,再设置Key value(nx),同时设置过期时间(ex)。

注,set命令中,ex参数设置秒,px参数设置毫秒

(2),Lua脚本

redis整合了Lua语言,我们可以基于redis的eval命令,通过Lua脚本来完成原子性操作;

eval命令如下:

eval 脚本内容 Ke个数 key列表 参数列表

如果觉得每次都要上传Lua脚本占带宽,还可以在Redis启动时将Lua脚本上传,由redis将脚本缓存,客户端拿到sha1,在使用lua脚本时只需要上传sha1值即可[1]。即通过script load命令上传脚本,得到sha1,再基于evalsha命令,通过sha1参数执行指定的lua脚本。

evalsha命令使用示例如下:

script load "${cat lua_demo.lua}"
evalsha 脚本sha1值 key个数 key列表 参数列表

四,如何评定过期时间→看门狗线程续期

现在好了,一个基本的redis分布式锁的方案已经有了,那么还有什么问题呢。仔细想想就会发现,问题出在了业务执行时间上。我们应该如何衡量业务执行时间并由此设置过期时间呢?

一个项目里,有的业务执行时间长,有的执行时间短。就算是同一个业务,你通过链路追踪或者监控知道了业务执行大多数是多久,下面分为两种情况,

情况1,我们假设就按照大多数业务能完成的时间来设置过期时间,这肯定是不行的,因为有的业务没完成就释放了锁,极容易出现问题。

情况2,我们按照一个比较大,大到应该不可能出现问题的过期时间,总行了吧?也不行,第一,“应该不可能”,谁知道以后有没有可能有的业务就是时间长到超过了过期时间。第二,为了少数业务执行时间长的情况,而给所有业务,包括大多数只需要较短持有锁时间的业务,都施加很长的过期时间,很明显项目并发能力会严重下降。

怎么办呢,长了也不行,短了也不行。这时候,我们可以加入看门狗方案。什么意思呢,就是说我们可以给线程设置一个不算长对于大多数线程都比较合理的时间,哪怕短点也无所谓。我们专门设置一个守护线程(thread.setDaemon(true))用于监控线程的看门狗线程,看门狗线程会在过期时间快到了且锁还没有被释放时,就去用expire命令给锁续期。

同时配合一个延迟队列DelayQueue(本质是个堆Heap),在加锁时,将装有线程id和续期时间(注意是续期时间,续期时间要比key的过期时间早一点)的entity扔进去,延迟队列根据续期时间排序,这样堆中排在最上面的永远是续期时间最近的entity(堆的特性)。

看门狗线程的任务内容,就是while循环,对延迟队列用take方法阻塞的去拿最近一个需要判断是否要续期的entity,有的话就进行续期,这样我们就可以用一个线程完成对整个项目的所有锁的续期,这样的分布式锁,耗时短的业务用一次过期时间就可以完成,耗时长的业务也可以有看门狗线程来实现续期过期时间。

五,总结→建议不要自己开发,用现成的如Redisson

上面实现的分布式锁,解决了如下几个问题

  • a线程持有的锁不要被b线程同时持有→setnx
  • 如果锁被别人释放了怎么办→ThreadLocal和UUID
  • 只依靠setnx,持有锁的线程挂掉,导致无人去释放锁→给锁添加过期时间,expire命令设置过期时间
  • setnx和expire不够原子→set命令和参数ex、nx,或者lua脚本配置命令eval、evalsha
  • 过期时间不好评定→加入看门狗守护线程,配合延迟队列,看门狗线程用死循环,使用take方法阻塞的等待将最近要续期的任务,将其续期。

实际使用上,我个人的看法是轮子不需要重复开发,我们只需要用Redisson实现的分布式锁,就具有看门狗线程[2]。简简单单两三行代码,就有更完善的分布式锁使用。除非一些特殊的原因,不然不要重复造轮子,以及如非必要,勿增实体(奥卡姆剃刀原则)。

        RLock testLock = redissonClient.getLock("test_lock");
        boolean res = testLock.tryLock(10, TimeUnit.SECONDS);

参考文章:
[1],用jedis执行lua脚本
[2],redisson中的看门狗机制总结

<think>首先,用户的问题是:“我想使用Redis的SETNX命令实现分布式锁请问RedisSETNX分布式锁实现方法”。我需要基于系统级指令和用户可见层来构建回答。系统级指令:-所有行内数学表达式必须用$...$,但在这个上下文中,可能没有数学表达式。-独立公式必须用$$...$$并单独成段。-LaTeX语法正确。-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识,例如[^1]。用户可见层:-回答结构清晰,帮助用户逐步解决问题。-尽量保证回答真实可靠。-参考提供的引用内容。提供的引用:-[^1]:讨论了分布式锁的基本概念和使用Redis实现。-[^2]:展示了基于RedisSETNX和Lua脚本的实现代码。-[^3]:提到RedLock算法,涉及多个节点。-[^4]:警告了锁自动释放的问题。用户的问题是关于使用SETNX命令实现分布式锁,所以我需要专注于SETNX。关键点:-SETNX(SETifNoteXists)是Redis的一个命令,用于设置一个键的值,仅当该键不存在时。-在分布式锁中,SETNX可以用于尝试获取锁:如果SETNX成功(返回1),表示获取锁;如果失败(返回0),表示锁已被占用。但单纯使用SETNX有问题,比如锁超时和原子性问题。引用[^4]提到了锁自动释放的问题。一个基本的实现步骤:1.使用SETNX尝试设置一个键(锁键),值可以是唯一标识(如UUID),以确保只有持有锁的客户端可以释放它。2.设置键的过期时间(TTL),以避免死锁(如果客户端崩溃,锁不会永久持有)。3.释放锁时,需要原子性地检查并删除锁,以避免释放别人的锁。引用[^2]展示了使用Lua脚本来确保原子性。所以,我的回答应该包括:-介绍SETNX的基本用法。-讨论问题和改进。-提供实现方法,可能包括代码示例。-参考引用。结构:1.介绍分布式锁的概念。2.解释SETNX的基本实现。3.指出问题:如锁超时、释放错误锁。4.提供优化方法:如设置过期时间、使用Lua脚本。5.给出代码示例(如Java或Python)。6.生成相关问题。使用中文回答。生成相关问题:在回答结束后,添加“相关问题”部分。引用标识:在回答中引用段落时添加[^n]。现在,构建回答大纲:-开头:简述RedisSETNX实现分布式锁原理。-步骤:分步解释实现方法。-代码示例:基于引用[^2],提供简化代码。-注意事项:讨论潜在问题,如引用[^4]提到的。-结束:总结。-相关问题:生成2-3个相关问题。代码示例:既然引用[^2]是Java,我可以提供Java示例。或者为了通用,用伪代码。确保真实可靠:基于标准Redis实践。最终回答结构:1.分布式锁简介。2.SETNX基本实现。3.改进:设置过期时间和原子操作。4.完整实现示例。5.注意事项。6.相关问题。</think>### Redis SETNX 分布式锁实现方法 Redis 的 `SETNX`(SET if Not eXists)命令是实现分布式锁一种简单方式。它通过设置一个唯一的键来确保在分布式环境中,只有一个客户端能持有锁,从而保护共享资源的安全访问。但单纯使用 `SETNX` 存在一些问题(如锁超时后误释放),因此需要结合过期时间和原子操作来优化。下面我将逐步解释实现方法,并提供代码示例。实现基于 Redis 单实例环境,并参考了分布式锁的最佳实践[^1][^4]。 #### 1. **基本原理** - `SETNX` 命令尝试设置一个键(锁键),仅当键不存在时返回成功(1),否则失败(0)。这模拟了“获取锁”的行为。 - 获取锁:客户端调用 `SETNX lock_key unique_value`(`unique_value` 是客户端唯一标识,如 UUID)。 - 释放锁:客户端删除键,但需确保只有持有者能释放。 - 问题:如果客户端崩溃,锁可能永久持有,导致死锁。因此,必须设置过期时间(TTL)[^4]。 #### 2. **基本实现步骤** 以下是使用 `SETNX` 实现分布式锁的核心步骤,需在客户端代码中处理原子性: 1. **尝试获取锁**: - 使用 `SETNX` 设置锁键,值为唯一标识(如 UUID),确保锁可被安全释放。 - 示例命令:`SETNX my_lock 123e4567-e89b-12d3-a456-426614174000` - 如果返回 1,表示获取成功;返回 0,表示锁已被占用,需重试或放弃。 2. **设置锁过期时间**: - 为避免死锁,获取锁后立即设置过期时间(单位:秒),例如 10 秒:`EXPIRE my_lock 10`。 - 注意:`SETNX` 和 `EXPIRE` 不是原子操作,可能导致 `SETNX` 成功但 `EXPIRE` 失败(如客户端崩溃),因此需优化[^4]。 3. **释放锁**: - 释放时,先检查锁值是否匹配当前客户端标识,再删除键。这需要原子操作,否则可能误删其他客户端的锁[^4]。 - 示例命令:使用 Lua 脚本确保原子性(见代码示例)。 #### 3. **优化实现方法** 单纯 `SETNX` 易出错,推荐以下优化: - **原子化设置锁和过期时间**:使用 Redis 2.6.12+ 的 `SET` 命令替代 `SETNX`,支持 `NX`(Not eXists)和 `EX`(过期时间)选项,避免两步操作风险。 - 示例命令:`SET my_lock 123e4567-e89b-12d3-a456-426614174000 NX EX 10` - **原子化释放锁**:使用 Lua 脚本执行“检查值 + 删除”操作,确保只有持有者能释放[^2]。 - **处理锁续期**:对于长任务,需在锁过期前续期(如使用看门狗机制),但 `SETNX` 不支持,需额外逻辑。 - **重试机制**:获取失败时,添加指数退避重试(如等待 100ms 后重试),避免频繁请求压垮 Redis。 #### 4. **代码示例(Java 实现)** 以下是一个简化示例,使用 Jedis 客户端库。参考引用[^2]的 Lua 脚本方法,确保原子性: ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; import java.util.UUID; public class DistributedLock { private Jedis jedis; // Redis 客户端实例 private String lockKey = "my_lock"; private int expireTime = 10; // 锁过期时间(秒) private String clientId = UUID.randomUUID().toString(); // 客户端唯一标识 // 获取锁 public boolean acquireLock() { // 使用 SET 命令原子化设置锁(NX: 仅当不存在, EX: 过期时间) String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().ex(expireTime)); return "OK".equals(result); // 成功返回 true } // 释放锁(使用 Lua 脚本确保原子性) public boolean releaseLock() { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else " + "return 0 " + "end"; // 执行 Lua 脚本:检查值匹配再删除 Object result = jedis.eval(luaScript, 1, lockKey, clientId); return result.equals(1L); // 成功删除返回 true } // 使用示例 public void doBusiness() { if (acquireLock()) { try { // 执行业务逻辑,如访问共享资源 System.out.println("Lock acquired. Doing business..."); // 注意:业务执行时间应小于 expireTime,否则需续期 } finally { releaseLock(); // 确保锁被释放 } } else { System.out.println("Failed to acquire lock. Retry or abort."); } } } ``` - **关键点**: - 获取锁:`jedis.set()` 替代 `SETNX`,原子化设置值和过期时间[^2]。 - 释放锁:Lua 脚本检查 `clientId` 匹配后再删除,防止误删[^2][^4]。 - 唯一标识 `clientId` 确保锁只被持有者释放。 #### 5. **注意事项** - **锁超时问题**:如果业务执行时间超过锁过期时间(如引用[^4]所述),锁可能被自动释放,导致其他客户端获取锁。解决方案:监控业务耗时,或使用 Redisson 等库实现自动续期。 - **Redis 单点故障**:单实例 Redis 宕机时锁失效。对于高可用场景,考虑 RedLock 算法(多实例投票机制)[^3],但增加复杂性。 - **性能考量**:`SETNX` 轻量高效,适用于低竞争场景;高并发时,结合重试和退避避免雪崩。 - **测试建议**:在开发环境模拟网络延迟和客户端崩溃,验证锁的可靠性和原子性。 总之,Redis `SETNX` 是实现分布式锁的基础,但需结合过期时间和原子操作(如 Lua 脚本)来保证安全。对于生产环境,推荐使用成熟库如 Redisson,它封装了锁续期和 RedLock 支持[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值