深入了解Redission分布式锁原理以及可重入锁的原理

2023-11-07 23:20

本文主要是介绍深入了解Redission分布式锁原理以及可重入锁的原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Redisson是一个基于Redis的Java框架,用于实现各种分布式功能,包括分布式锁。Redisson提供了多种分布式锁的实现,其中包括可重入锁、公平锁、联锁(多个锁同时锁定或释放)、红锁(多个独立Redis节点的分布式锁),以及读写锁等。


基于setnx实现的分布式锁存在以下四个问题

Redisson入门使用教程 

Redisson客户端配置:首先,您需要配置Redisson客户端以连接到Redis服务器。通常,这涉及创建一个Config对象,并使用useSingleServer()或其他方法指定Redis服务器的连接信息。示例代码中的配置是连接到本地Redis服务器的示例。(对了这里不要忘记引入redisson依赖)

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("your word");
return Redisson.create(config)

 Redisson的使用

public class RedissonLockExample {public static void main(String[] args) {// 配置RedissonConfig config = new Config();config.useSingleServer().setAddress("redis://localhost:6379");// 创建Redisson客户端RedissonClient redisson = Redisson.create(config);// 获取锁RLock lock = redisson.getLock("myLock");try {// 尝试加锁,最多等待10秒boolean locked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);if (locked) {// 锁定成功,执行需要加锁的代码System.out.println("获取锁成功,这里来写需要加锁的代码");Thread.sleep(5000); // 模拟锁定后的操作} else {// 锁定失败System.out.println("获取锁失败");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放锁lock.unlock();System.out.println("释放锁");}// 关闭Redisson客户端redisson.shutdown();}

 深入讲解Redisson可重入锁的工作原理

 重入锁原理 

      重入锁(Reentrant Lock)是一种高级的同步工具,它允许同一个线程多次获取同一把锁,而不会发生死锁。这意味着一个线程在持有锁的情况下可以多次进入锁保护的代码块,而不会被自己阻塞

  1. 锁计数器:重入锁内部维护一个锁计数器,用于跟踪锁的持有次数。初始时,锁计数器为0,表示没有线程持有该锁。

  2. 加锁操作:当一个线程首次请求加锁时,锁计数器会增加,同时记录下持有锁的线程。此时,线程获得了锁,并且可以执行锁保护的代码块。

  3. 重入:如果同一个线程再次请求加锁(重复加锁),锁计数器会继续增加,表示锁被持有多次。线程在退出锁保护的代码块之前,可以多次加锁和解锁,而锁计数器会相应地增加和减少。

  4. 解锁操作:每次线程解锁时,锁计数器减少。只有当锁计数器减少为0时,锁才会被完全释放,其他线程才有机会获得锁。

作用:

  1. 避免死锁:重入锁允许同一线程多次获取锁,因此不会因为线程自己持有的锁而导致死锁。这在复杂的多线程场景中非常有用,因为线程可能需要在执行一些递归函数或者多层嵌套的方法时多次获取锁。

  2. 精细控制锁的释放:与传统的synchronized关键字相比,重入锁允许更灵活地控制锁的释放。线程可以在锁保护的代码块内多次获取和释放锁,而不必将整个代码块包裹在同一个synchronized块中。

我们来看一下trylock的底层逻辑:

 通过redis的hash结构来实现锁的重入,如果第一次获取锁就创建,并把value设置为1,再次有线程想要获取锁就再次增加value的值,释放锁时每当一个线程释放时value就减一。直到为0彻底释放完成

调用了tryLockAsync方法并传入了线程id的参数

 由于初始时未填写过期时间等待时间等信息,默认为-1,进而再次调用tryAcquireOnceAsync方法

 <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {this.internalLockLeaseTime = unit.toMillis(leaseTime);return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, 
"if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hincrby', KEYS[1], ARGV[2], 1); 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; end; 
if (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]);",Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));}

可见为了保证获取锁的原子性也即不让其他线程在这个线程获取锁的过程中“插队”执行需要将获取锁的代码写入一个Lua脚本当中。

当==0时表示之前未有线程获取锁创建并赋值。当==1时表示存在,为了实现重入就在value上加一,并设置过期时间。注意 这里返回nil代表成功,失败返回对应的时间毫秒值pttl

之后会释放锁

 protected RFuture<Boolean> unlockInnerAsync(long threadId) {return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end;local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0;else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;",Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));}

每次释放锁都会对数量减一直至0,并且发布释放锁的通知

重试获取锁机制讲解

trylock源码

 public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);if (ttl == null) {return true;} else {time -= System.currentTimeMillis() - current;if (time <= 0L) {this.acquireFailed(waitTime, unit, threadId);return false;} else {current = System.currentTimeMillis();RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {this.unsubscribe(subscribeFuture, threadId);}});}this.acquireFailed(waitTime, unit, threadId);return false;} else {try {time -= System.currentTimeMillis() - current;if (time <= 0L) {this.acquireFailed(waitTime, unit, threadId);boolean var20 = false;return var20;} else {boolean var16;do {long currentTime = System.currentTimeMillis();ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);if (ttl == null) {var16 = true;return var16;}time -= System.currentTimeMillis() - currentTime;if (time <= 0L) {this.acquireFailed(waitTime, unit, threadId);var16 = false;return var16;}currentTime = System.currentTimeMillis();if (ttl >= 0L && ttl < time) {((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}time -= System.currentTimeMillis() - currentTime;} while(time > 0L);this.acquireFailed(waitTime, unit, threadId);var16 = false;return var16;}} finally {this.unsubscribe(subscribeFuture, threadId);}}}}}

可见这里默认ttl也是为-1,注意tryAcquire方法,返回值为ttl,ttl为null即为获得锁成功

这里由于默认存活时间为-1,所以下面参数默认存活时间为

this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

(watchdog看门狗)也就是30s。

如果ttl为null那么返回true代表获取成功

否则用最大等待时间time减去上面的系统时间算出这段代码的耗时,如果为负数说明超过最大等待时长,返回false,如果time大于0,不直接判断,因为此时别的线程获取锁正在执行,假设立马执行只是会浪费cpu资源,所以这里用了subscribe(threadId)方法来订阅锁释放的信息(上面的unlock代码释放锁会发布信息),然后采用计数器进行等待,等待时长为time,假设没等到,返回false,那么使用unsubscribe()方法结束订阅,返回false。

如果等到别的线程释放锁,就再次判断上面代码是否超时,超时返回false,否则再次带哦用tryAcquire方法

如果ttl小于等待时间time,那么就尝试ttl时间,否则就尝试获取锁在time时间内,知道time结束,这就时重试获取锁机制了。


Redisson分布式锁的原理

获取锁

也就是说如果不设置存活时间,那么会利用看门狗执行任务刷新等待

释放锁 

这篇关于深入了解Redission分布式锁原理以及可重入锁的原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/366737

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念

深入理解数据库的 4NF:多值依赖与消除数据异常

在数据库设计中, "范式" 是一个常常被提到的重要概念。许多初学者在学习数据库设计时,经常听到第一范式(1NF)、第二范式(2NF)、第三范式(3NF)以及 BCNF(Boyce-Codd范式)。这些范式都旨在通过消除数据冗余和异常来优化数据库结构。然而,当我们谈到 4NF(第四范式)时,事情变得更加复杂。本文将带你深入了解 多值依赖 和 4NF,帮助你在数据库设计中消除更高级别的异常。 什么是