当前位置: 首页 > ops >正文

Redis(六):分布式锁

分布式锁在分布式业务场景中经常用到。想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not Exists,即如果 key 不存在,才会设置它的值,否则什么也不做。

设置锁

客户端1:

127.0.0.1:6379> setnx lock l
(integer) 1

客户端2:

127.0.0.1:6379> setnx lock l
(integer) 0

当客户端1设置一个锁,客户端2想设置同一个锁的时候结果为“0”并未成功,这就是setnx的作用。

设置超时时间

客户端1:

127.0.0.1:6379> setnx lock l
(integer) 1
# 设置超时时间10s
127.0.0.1:6379> EXPIRE lock 10
(integer) 1

客户端2:

127.0.0.1:6379> setnx lock l
(integer) 0
# 客户端1设置了10s超时,在执行上锁成功
127.0.0.1:6379> setnx lock l
(integer) 1

这有个问题,①设置锁设置超时时间,这是两个命令不是原子性的,极易造成死锁(锁设置成功,超时时间设置失败),所以我们需要一个原子性的语句:

SET key value EX 秒 PX 毫秒 NX XX127.0.0.1:6379> set lock l ex 10 nx
OK

如何避免锁被别人释放

在分布式系统中,我们通常会使用业务中的唯一标识作为锁的 key。但这种方式存在一个严重的问题:客户端在释放锁时,往往是“无脑”释放,并没有检查这把锁是否仍然归自己持有

这种做法带来的风险是显而易见的:可能会误删其他客户端持有的锁,导致并发安全问题。显然,这样的解锁流程并不严谨。

在客户端加锁时,不仅要设置锁的 key,还应写入一个只有当前客户端知道的唯一标识作为 value。

# 加锁
SET lock_key uuid_value NX EX 10# 解锁
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end

Java代码实现分布式锁


/*** 分布式锁的实现*/
@Component
public class RedisDistLock implements Lock {private final static int LOCK_TIME = 5_000;private final static String DISTLOCK_KEY = "distkey:";private final static String RELEASE_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('del', KEYS[1])\n" +"    else return 0 end";/*保存每个线程的独有的ID值*/private ThreadLocal<String> lockerId = new ThreadLocal<>();/*解决锁的重入*/private Thread ownerThread;private String lockName = "lock";@Autowiredprivate JedisPool jedisPool;public void setOwnerThread(Thread ownerThread) {this.ownerThread = ownerThread;}@Overridepublic void lock() {while(!tryLock()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException("不支持可中断获取锁!");}@Overridepublic boolean tryLock() {Thread t = Thread.currentThread();if(ownerThread==t){/*说明本线程持有锁*/return true;}else if(ownerThread!=null){/*本进程里有其他线程持有分布式锁*/return false;}Jedis jedis = null;try {String id = UUID.randomUUID().toString();SetParams params = new SetParams();params.px(LOCK_TIME);params.nx();synchronized (this){/*线程们,本地抢锁*/if((ownerThread==null)&&"OK".equals(jedis.set(DISTLOCK_KEY+lockName,id,params))){lockerId.set(id);setOwnerThread(t);return true;}else{return false;}}} catch (Exception e) {throw new RuntimeException("分布式锁尝试加锁失败!");} finally {jedis.close();}}@Overridepublic boolean tryLock(long time, TimeUnit unit){throw new UnsupportedOperationException("不支持等待尝试获取锁!");}@Overridepublic void unlock() {if(ownerThread!=Thread.currentThread()) {throw new RuntimeException("试图释放无所有权的锁!");}Jedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,Arrays.asList(DISTLOCK_KEY+lockName),Arrays.asList(lockerId.get()));if(result.longValue()!=0L){System.out.println("Redis上的锁已释放!");}else{System.out.println("Redis上的锁释放失败!");}} catch (Exception e) {throw new RuntimeException("释放锁失败!",e);} finally {if(jedis!=null) jedis.close();lockerId.remove();setOwnerThread(null);System.out.println("本地锁所有权已释放!");}}@Overridepublic Condition newCondition() {throw new UnsupportedOperationException("不支持等待通知操作!");}
}

分布式锁自动续期

一般我们的业务场景中往往会出现处理时长超过我们设置的分布式锁的期限的场景,此时,我们需要对锁进行续期操作。操作流程如下:

加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

这个守护线程我们一般也把它叫做「看门狗」线程。

/*** 分布式锁,附带看门狗线程的实现:加锁,保持锁1秒*/
@Component
public class RedisDistLockWithDog implements Lock {private final static int LOCK_TIME = 1_000;private final static String LOCK_TIME_STR = String.valueOf(LOCK_TIME);private final static String DISTLOCK_KEY = "distkey2:";private final static String RELEASE_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('del', KEYS[1])\n" +"    else return 0 end";/*还有并发问题,考虑ThreadLocal*/private ThreadLocal<String> lockerId = new ThreadLocal<>();private Thread ownerThread;private String lockName = "lock";@Autowiredprivate JedisPool jedisPool;public void setOwnerThread(Thread ownerThread) {this.ownerThread = ownerThread;}@Overridepublic void lock() {while(!tryLock()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException("不支持可中断获取锁!");}@Overridepublic boolean tryLock() {Thread t=Thread.currentThread();/*说明本线程正在持有锁*/if(ownerThread==t) {return true;}else if(ownerThread!=null){/*说明本进程中有别的线程正在持有分布式锁*/return false;}Jedis jedis = null;try {jedis = jedisPool.getResource();/*每一个锁的持有人都分配一个唯一的id,也可采用snowflake算法*/String id = UUID.randomUUID().toString();SetParams params = new SetParams();params.px(LOCK_TIME); //加锁时间1sparams.nx();synchronized (this){if ((ownerThread==null)&&"OK".equals(jedis.set(DISTLOCK_KEY+lockName,id,params))) {lockerId.set(id);setOwnerThread(t);if(expireThread == null){//看门狗线程启动expireThread = new Thread(new ExpireTask(),"expireThread");expireThread.setDaemon(true);expireThread.start();}//往延迟阻塞队列中加入元素(让看门口可以在过期之前一点点的时间去做锁的续期)delayDog.add(new ItemVo<>((int)LOCK_TIME,new LockItem(lockName,id)));System.out.println(Thread.currentThread().getName()+"已获得锁----");return true;}else{System.out.println(Thread.currentThread().getName()+"无法获得锁----");return false;}}} catch (Exception e) {throw new RuntimeException("分布式锁尝试加锁失败!",e);} finally {jedis.close();}}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new UnsupportedOperationException("不支持等待尝试获取锁!");}@Overridepublic void unlock() {if(ownerThread!=Thread.currentThread()) {throw new RuntimeException("试图释放无所有权的锁!");}Jedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,Arrays.asList(DISTLOCK_KEY+lockName),Arrays.asList(lockerId.get()));System.out.println(result);if(result.longValue()!=0L){System.out.println("Redis上的锁已释放!");}else{System.out.println("Redis上的锁释放失败!");}} catch (Exception e) {throw new RuntimeException("释放锁失败!",e);} finally {if(jedis!=null) jedis.close();lockerId.remove();setOwnerThread(null);}}@Overridepublic Condition newCondition() {throw new UnsupportedOperationException("不支持等待通知操作!");}/*看门狗线程*/private Thread expireThread;//通过delayDog 避免无谓的轮询,减少看门狗线程的轮序次数   阻塞延迟队列   刷1  没有刷2private static DelayQueue<ItemVo<LockItem>> delayDog = new DelayQueue<>();//续锁逻辑:判断是持有锁的线程才能续锁private final static String DELAY_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('pexpire', KEYS[1],ARGV[2])\n" +"    else return 0 end";private class ExpireTask implements Runnable{@Overridepublic void run() {System.out.println("看门狗线程已启动......");while(!Thread.currentThread().isInterrupted()) {try {LockItem lockItem = delayDog.take().getData();//只有元素快到期了才能take到  0.9sJedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(DELAY_LOCK_LUA,Arrays.asList(DISTLOCK_KEY+lockItem.getKey ()),Arrays.asList(lockItem.getValue(),LOCK_TIME_STR));if(result.longValue()==0L){System.out.println("Redis上的锁已释放,无需续期!");}else{delayDog.add(new ItemVo<>((int)LOCK_TIME,new LockItem(lockItem.getKey(),lockItem.getValue())));System.out.println("Redis上的锁已续期:"+LOCK_TIME);}} catch (Exception e) {throw new RuntimeException("锁续期失败!",e);} finally {if(jedis!=null) jedis.close();}} catch (InterruptedException e) {System.out.println("看门狗线程被中断");break;}}System.out.println("看门狗线程准备关闭......");}}@PreDestroypublic void closeExpireThread(){if(null!=expireThread){expireThread.interrupt();}}
}
RedLock

之前分析的场景都是,锁在单个Redis实例中可能产生的问题,并没有涉及到 Redis 的部署架构细节。

而我们在使用 Redis 时,一般会采用主从集群 +哨兵的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现「故障自动切换」,把从库提升为主库,继续提供服务,以此保证可用性。

但是因为主从复制是异步的,那么就不可避免会发生的锁数据丢失问题(加了锁却没来得及同步过来)。从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!
Redis 作者提出的 Redlock 方案,是如何解决主从切换后,锁失效问题的。

Redlock 的方案基于一个前提:

  • 不再需要部署从库和哨兵实例,只部署主库;但主库要部署多个,官方推荐至少 5 个实例。
  • 注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。它们之间没有任何关系,都是一个个孤立的实例。
RedLock的争议点

Dr. Martin Kleppmann:How to do distributed locking

  1. 客户端1获取锁,但是执行业务逻辑的同时,发生GC;
  2. 锁超时发生在在GC执行完毕之前;
  3. 此时客户端2已经重新获取了锁并执行了更新数据的操作;
  4. 客户端1的GC执行完毕,继续执行更新操作;
  5. 客户端1将旧数据更新
    在这里插入图片描述
    Dr. Martin Kleppmann 提出了一个解决办法,请至原文查看。
Redisson

前面介绍了直接使用 Redis 命令,如 SET lock_key uuid NX PX 30000,再加一些 Lua 脚本辅助释放锁,支持简单自动续期逻辑。
Redisson 是基于 Redis 封装的一整套分布式协调工具,支持分布式锁(RLock)、看门狗、异步API、限流、延迟队列等,内部集成了重试、自动续期、RedLock 多节点机制等。

维度🧰 自己实现 Redis 分布式锁🚀 Redisson 分布式锁
基本实现方式使用命令 SET NX EX + Lua 脚本封装多种 Redis 操作 + 看门狗 + 多种锁类型
自动续期(看门狗)❌ 需要自己实现定时任务续期✅ 内置,默认自动续期(默认 30s)
加锁唯一标识(如 UUID)✅ 可自行加 UUID✅ 内部自动管理唯一标识
释放锁前校验持有者身份❌ 容易遗漏,需手动校验✅ 自动校验,只能释放自己加的锁
异常恢复能力(宕机、GC卡顿等)❌ 容易死锁 / 锁丢失✅ 看门狗保障长期持锁,线程意外终止也会安全释放
支持的锁类型🚫 只支持互斥锁✅ 支持可重入锁、读写锁、公平锁、信号量等
集群/哨兵兼容性❌ 自己处理 Redis 路由、主从切换✅ 自动感知主节点变动,稳定性更强
RedLock 多主容灾支持🚫 不支持,需要自己实现✅ 提供 RedissonRedLock 实现(多主 Redis)
连接管理(连接池、故障转移)❌ 自己手动处理✅ 内部封装 Netty 连接池,容错好
API 使用体验🧱 低级命令,需写 Lua 和处理异常🧼 高级 API,支持 tryLock、lockAsync、时间配置等
学习和集成成本✅ 起步简单❌ 依赖大,需要理解配置和生命周期
性能✅ 更轻量,无额外封装⚠️ 略有封装成本,但通常影响可接受
可靠性⚠️ 靠实现质量✅ 社区验证,功能完善,可靠性强
适合的业务场景简单锁、低并发、不要求容灾中高并发、线上核心流程、容错要求高
额外功能❌ 只有锁✅ 内置布隆过滤器、延迟队列、限流器等
http://www.xdnf.cn/news/17055.html

相关文章:

  • 【机器学习深度学习】 知识蒸馏
  • 分布式网关技术 + BGP EVPN,解锁真正的无缝漫游
  • Java面试宝典:深入解析JVM运行时数据区
  • 计算机网络:(十三)传输层(中)用户数据报协议 UDP 与 传输控制协议 TCP 概述
  • python+MySQL组合实现生成销售财务报告
  • AI的第一次亲密接触——你的手机相册如何认出你的猫?
  • QUdpSocket发送组播和接受组播数据
  • Modstart 请求出现 Access to XMLHttpRequest at ‘xx‘
  • FPGA学习笔记——简易的DDS信号发生器
  • Cisco 3750X交换机更新到IOS 15.2后无法启动 提示:Boot process failed...
  • 内部排序算法总结(考研向)
  • VS2019c++环境下OPCUA+Kepserver+open62541实现与三菱plc通信
  • 机器学习Adaboost算法----SAMME算法和SAMME.R算法
  • 【2025年8月5日】将运行一段时间的单机MongoDB平滑迁移至副本集集群
  • LeetCode算法日记 - Day 2: 快乐数、盛水最多容器
  • 计算机常用英语词汇大全
  • 【unitrix】1.1 readme.md
  • Erdős–Rényi (ER) 模型
  • Android10 系统休眠调试相关
  • 文件编译、调试及库制作
  • 视频水印技术中的变换域嵌入方法对比分析
  • 从 “看懂图” 到 “读懂视频”:多模态技术如何用文本反哺视觉?
  • FPGA实现Aurora 8B10B视频点对点传输,基于GTP高速收发器,提供4套工程源码和技术支持
  • RC和RR的区别
  • 关于npx react-native run-android下载进程缓慢以及进程卡壳等问题的解决方案。
  • iouring系统调用及示例
  • 16核32G硬件服务器租用需要多少钱
  • 【安卓][Mac/Windows】永久理论免费 无限ip代理池 - 适合临时快速作战
  • 【数字图像处理系列笔记】Ch01:绪论
  • Vue2项目—基于路由守卫实现钉钉小程序动态更新标题