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

从Redisson分布式锁看锁的设计思路

Redisson

我们先来了解一下Redisson是个什么东西,他是一个客户端,通过网络和redis来沟通。那么他用的什么东西呢,如果你找一下他的依赖,那么就会发现netty,他通过netty来和redis沟通,这对于后边看门狗的续期,有很大的相关性。

锁的一些性质:

  • 互斥性
  • 可重入性
  • 原子性
  • 防止死锁
  • 阻塞等待上的优化 / 通知他人
    我们依次来看这里的性质,在Redisson分布式锁中都有说法,通过抓住锁的一些核心性质,来实现锁的操作部分。

互斥性

这里直接通过key 是否exist来判断就可以了,来看这里的lua脚本

"if ((redis.call('exists', KEYS[1]) == 0) " +  "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  "redis.call('pexpire', KEYS[1], ARGV[1]); " +  "return nil; " +  "end; " +  "return redis.call('pttl', KEYS[1]);"

这里直接exists去判断,是否有这个key,这里h也是hash的意思,对吧,这里的hincrby就是去实现的重入嘛。 "redis.call('pexpire', KEYS[1], ARGV[1]); " + 这一句给当前锁重新加一个过期时间。

可重入性。

首先来看一看设计思路:
为什么普通的锁不可重入? 因为setnx 可以看成一种boolean的状态,他只有true or false , 但是我们想要的不仅仅是true or false , 我们想要的是state , 我们想要值。 这里AQS有一个state变量,就是干这个用的,为什么他state是 volatile int,而不是用的boolean?就是为了实现可重入的效果。 那么为什么不可以用String ? 他的key作为锁名和持有的名 , value作为这个锁的重入次数,而是用hash这种key - fieldKey - value的格式。

我觉得实现可重入这些都是次要的,对于可重入的实现,主要是将boolean改为int值就可以了。但是hash结构的这种key - fieldKey - value的模式可以为我们提供一些别的遍历。在一个key中,我们可以通过fieldKey存储更多的信息。
myLock: {
“8743c9c0…:1”: 1, // 主锁字段
“mode”: “fair”, // 锁模式
“timeout”: 30000, // 超时时间
“created_at”: 1630000000 // 创建时间戳
}
我觉得这个东西是最重要的,单个的string的key存的东西比较单一,取出来的时候操作会很复杂,所以我们主要选取hash,string可以实现重入,但是很麻烦。而hash双重key自动spilt,并且还有其他信息可以使用。有简单的数据结构,就不要去用复杂的东西来折磨自己了

原子性

使用lua脚本来保持原子性。为什么redis执行是原子性?因为redis使用单个主线程来处理任务。 redis会一股脑读完lua脚本中的内容,那么为什么redis单单用个单线程就可以那么快呢?其实核心就在于IO多路复用。这里详细讲一下当复习了。某个redis可能会连接多个client,当某个socket可读的时候,内核通过事件触发的方式来通知Redis,然后Redis立马对该事件进行处理。为了更形象的理解,我们来假设redis不用多路复用,那么如果有大量的请求连过来,他必须去对应的处理。比如fork一个子进程,或者开一条线程。如果不这么做,那么大量的client就会互相阻塞,因为路就这么大,你不开更多的路,那么只能像排队那样等前边的人弄完才可以继续走,或者用简单的NIO,比如之前的select,但是select要去轮训,且连接管理大小有限。poll只优化连接数,但是仍然是去轮训请求的。如果请求数很多,他们的开销,时间消耗都是不晓得。但是如果用IO多路复用呢?每个连接都有自己的channel,kernel会通过channel表来通知redis哪个channel有对应的数据,redis直接去拿然后操作就可以了,简单,高效。

防止死锁。

如果redis挂了,那么普通的锁永远都不会被释放,所以我们需要考虑expire ,但是如果expire的时间太短,线程执行的过程中被释放那不炸了吗,所以我们需要使用watchDog,给锁续时。看门狗的续期逻辑不依赖当前的线程,而是有他自己的一个线程,还记得前文我们说的netty吗?他里边就有个时间轮,Redisson通过时间轮的方式来管理锁续期。来看代码(虽然我也不想粘)

CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {  // lock acquired  if (acquired) {  if (leaseTime > 0) {  internalLockLeaseTime = unit.toMillis(leaseTime);  } else {  scheduleExpirationRenewal(threadId);  }  }  return acquired;  
});
private void renewExpiration() {  ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());  if (ee == null) {  return;  }  Timeout task = getServiceManager().newTimeout(new TimerTask() {  @Override  public void run(Timeout timeout) throws Exception {  ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());  if (ent == null) {  return;  }  Long threadId = ent.getFirstThreadId();  if (threadId == null) {  return;  }  CompletionStage<Boolean> future = renewExpirationAsync(threadId);  future.whenComplete((res, e) -> {  if (e != null) {  log.error("Can't update lock {} expiration", getRawName(), e);  EXPIRATION_RENEWAL_MAP.remove(getEntryName());  return;  }  if (res) {  // reschedule itself  renewExpiration();  } else {  cancelExpirationRenewal(null, null);  }  });  }  }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);  ee.setTimeout(task);  
}

这里new了一个定时任务,他是TaskTime类型的任务,每隔锁的释放时间的1 / 3 去更新一次,最开始为30,所以每隔10s去做一次。里边的任务会去继续调用他自己,实现定时任务的循环。最后把这个任务用ee来启动. HashedWheelTimeout你去这里的newTimeout中去看一下,就会看到这个哈希时间轮了。 然后就是执行这个定时任务的线程了,他是redisson来管理的线程,你去debug看一下,就会看到他的线程名称对应为(我的) redisson-netty-4-3 这种名称很明显是新创建的内容。但是具体在哪创建的,我没找到。反正肯定是和主线程没什么关系。二者声明周期是不同的。主线程挂了后,那么这个是会一直续期,所以一定要用finally来修饰。

阻塞通知 : pub / sub

这里最后就是当锁释放后怎么通知其他的线程可以去拿锁了。这里也是发布订阅模式的应用。而不是直接用比如AQS那种用阻塞队列的模式来拿等待的锁。
根本原因是 “分布式” vs “单机” 的物理差异,导致无法像 AQS 那样维护一条集中式等待队列。

  1. AQS 为什么能用 CLH 队列
    • 所有线程在同一 JVM,共享同一块堆内存;
    • 用 volatile + CAS 就能原子地把线程节点插到链表;
    LockSupport.park/unpark 直接在用户态挂起/唤醒线程,零网络开销。

  2. Redis 场景下如果硬做“链表”会遇到什么
    节点放哪?
    – 放客户端本地:别的 JVM 看不到,无法全局排队。
    – 放 Redis 里:需要把“客户端 ID + 线程 ID”序列化成 List/Stream 的元素,插入、弹出、超时清理全部要 Lua 保证原子,实现复杂度陡增。
    唤醒谁?
    – 队列只能让“下一个节点”的客户端去抢锁;如果那个客户端进程已经崩溃,节点就永远卡在那里,需要额外的监听与清理。
    实时性
    – 链表方案只能让客户端轮询“轮到我了吗?”;消息往返 + 空转重试让延迟不可控。
    原子性
    – “入队 + 抢锁”必须是原子事务,否则会出现两个客户端同时认为自己排在队头。
    – 一次 Lua 脚本里既要写队列又要 SETNX,逻辑臃肿且难以维护。

  3. Pub/Sub 的优势正好弥补以上痛点
    天然广播:一条 PUBLISH 立即通知所有订阅端,零轮询、零链表维护。
    无状态:客户端崩溃后频道自动消失,没有残留节点。
    实现简单:Lua 脚本只需 DEL + PUBLISH 两步即可保证原子。
    延迟低:消息由 Redis 服务器主动推送,毫秒级即可唤醒等待方。

结论
AQS 用链表是因为它能在同一进程内共享内存,而 Redisson 运行在跨进程、跨机器的分布式环境中,无法安全、实时、低成本地维护全局链表;Pub/Sub 提供的“广播+无状态”机制正好满足分布式锁释放通知的需求,因此成为最优选。

http://www.xdnf.cn/news/19598.html

相关文章:

  • IPC 进程间通信 interprocess communicate
  • 企业微信AI落地:如何选择企业微信服务商?
  • Axios拦截器:前端通信的交通警察[特殊字符]
  • 搭载AX650N高能效比智能视觉芯片——AX2050系列边缘计算盒,可应用在智慧安防交通仓储教育,人脸识别,明厨亮灶,安全生产,智能机器人等
  • table表格字段明细展示
  • 不透明指针
  • 【iOS】折叠cell
  • 《青衣剑客 · Claude》连载
  • 总线矩阵的原理
  • 如何将多个Excel报表合并为一个汇总文件?
  • N32G43x Bootloader 中 ENV 区的管理与实现
  • 前缀和(优化算法)
  • ClickHouse常见问题——ClickHouseKeeper配置listen_host后不生效
  • 面试 TOP101 动态规划专题题解汇总Java版(BM62 —— BM82)
  • 二、SVN基础命令速查表
  • leetcode 1792. 最大平均通过率 中等
  • 通过 select into outfile / load data infile 进行数据导入导出学习笔记
  • 开源项目_金融分析工具TradingAgents
  • 01数据结构-红黑树
  • python 数据类型【python进阶一】
  • java设计模式一、单例模式
  • 【K8s】整体认识K8s之Configmap、Secret/ResourceQuota资源配额/访问控制
  • Linux应用开发-windows,linux环境下相关工具
  • Adobe Illustrator 2025最新破解教程下载安装教程,Illustrator2025最新版下载
  • AI 安全与伦理:当大模型拥有 “决策能力”,我们该如何建立技术边界与监管框架?
  • 新手向:前端开发中的常见问题
  • NLP大语言模型数据准备
  • 基于 DNA 的原核生物与微小真核生物分类学:分子革命下的范式重构​
  • Shell编程(二):正则表达式
  • FastK v1.1 安装与使用-生信工具59