字节二面:Spring Boot Redis 可重入分布式锁实现原理?

应用开发2025-11-05 13:11:521

我是字节码哥,可以叫我靓仔。可重

书接上回,入分码哥上一篇《纠正误区:这才是布式 SpringBoot Redis 分布式锁的正确实现方式》分享了分布式锁如何从错误到残缺,再到青铜版本的锁实高性能 Redis 分布式锁代码实战,让你一飞冲天。现原

这是字节我们最常用的分布式锁方案,今天码哥给你来一个进阶。可重

Chaya:「码哥,入分上次的布式分布式锁版本虽然好,但是锁实不支持可重入获取锁,还差一点点意思。现原」

Chaya 别急,字节今日码哥给你带来一个高性能可重入 Redis 分布式锁解决方案,可重直捣黄龙,入分一笑破苍穹。

什么是可重入锁

当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的代码,可重入性就就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行。高防服务器

复制public synchronized void a() { b(); } public synchronized void b() { // doWork }1.2.3.4.5.6.

假设 X 线程在 a 方法获取锁之后,继续执行 b 方法,如果此时不可重入,线程就必须等待锁释放,再次争抢锁。

锁明明是被 X 线程拥有,却还需要等待自己释放锁,然后再去抢锁,这看起来就很奇怪,我释放我自己~

可重入锁实现原理

Chaya:「Redis String 数据结构无法满足可重入锁,key 表示锁定的资源,value 是客户端唯一标识,可重入没地方放了。」

我们可以使用 Redis hash 结构实现,key 表示被锁的共享资源, hash 结构的 fieldKey 存储客户端唯一标识,fieldKey 的 value 则保存加锁的次数。

加锁原理

可重入锁加锁的过程中有以下场景需要考虑。

锁已经被 A 客户端获取,客户端 B 获取锁失败。锁已经被客户端 A 获取,客户端 A 多次执行获取锁操作。锁没有被其他客户端获取,那么此刻获取锁的客户端可以获取成功。b2b信息网

按照之前的经验,多个操作的原子性可以用 lua 脚本实现。可重入锁加锁 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]);1.2.3.4.5.6.7. KEYS[1]是 lockKey 表示获取的锁资源,比如 lock:168。ARGV[1] 表示表示锁的有效时间(单位毫秒)。ARGV[2] 表示客户端唯一标识,在 Redisson 中使用 UUID:ThreadID。

下面我来接下是这段脚本的逻辑。

锁不存在或者锁存在且值与客户端唯一标识匹配,则执行 hincrby 和 pexpire指令,接着 return nil。表示的含义就是锁不存在就设置锁并设置锁重入计数值为 1,设置过期时间;锁存在且唯一标识匹配表明当前加锁请求是锁重入请求,锁从如计数 +1,重新锁超时时间。

redis.call(exists, KEYS[1]) == 0判断锁是否存在,0 表示不存在。redis.call(hexists, KEYS[1], ARGV[2]) == 1)锁存在的话,判断 hash 结构中 fieldKey 与客户端的唯一标识是站群服务器否相等。相等表示当前加锁请求是锁重入。redis.call(hincrby, KEYS[1], ARGV[2], 1)将存储在 hash 结构的 ARGV[2] 的值 +1,不存在则支持成 1。redis.call(pexpire, KEYS[1], ARGV[1])对 KEYS[1] 设置超时时间。

锁存在,但是唯一标识不匹配,表明锁被其他线程持有,调用 pttl返回锁剩余的过期时间。

Chaya:「“脚本执行结果返回 nil、锁剩余过期时间有什么目的?”」

当且仅当返回 nil才表示加锁成功;客户端需要感知锁是否成功的结果。

解锁原理

解锁逻辑复杂一些,不仅要保证不能删除别人的锁。还要确保,重入次数为 0 才能解锁。

解锁代码执行方式与加锁类似,三个返回值含义如下。

1 代表解锁成功,锁被释放。0 代表可重入次数被减 1。nil 代表其他线程尝试解锁,解锁失败。 复制if (redis.call(hexists, KEYS[1], ARGV[2]) == 0) then return nil; end; local counter = redis.call(hincrby, KEYS[1], ARGV[2], -1); if (counter > 0) then redis.call(pexpire, KEYS[1], ARGV[1]); return 0; else redis.call(del, KEYS[1]); return 1; end; return nil;1.2.3.4.5.6.7.8.9.10.11.12. KEYS[1]是 lockKey,表示锁的资源,比如 lock:order:pay。ARGV[1],锁的超时时间。ARGV[2],Hash 表的 FieldKey。

首先使用 hexists 判断 Redis 的 Hash 表是否存在 fileKey,如果不存在则直接返回 nil解锁失败。

若存在的情况下,且唯一标识匹配,使用 hincrby 对 fileKey 的值 -1,然后判断计算之后可重入次数。当前值 > 0 表示持有的锁存在重入情况,重新设置超时时间,返回值 1;

若值小于等于 0,表明锁释放了,执行 del释放锁。

Chaya:“可重入锁很好,依然存在的一个问题是:加锁后,业务逻辑执行耗时超过了 lockKey 的过期时间,lockKey 会被 Reids 删除。”

这个时间不能瞎写,一般要根据在测试环境多次测试,然后压测多轮之后,比如计算出接口平均执行时间 200 ms。那么锁的超时时间就放大为平均执行时间的 3~5 倍。

Chaya:“锁的超时时间怎么计算合适呢?”

这个时间不能瞎写,一般要根据在测试环境多次测试,然后压测多轮之后,比如计算出接口平均执行时间 200 ms。那么锁的超时时间就放大为平均执行时间的 3~5 倍。

Chaya:“为啥要放大呢?”

因为如果锁的操作逻辑中有网络 IO 操作、JVM FullGC 等,线上的网络不会总一帆风顺,我们要给网络抖动留有缓冲时间。

Chaya:“有没有完美的方案呢?不管时间怎么设置都不大合适。”

我们可以让获得锁的线程开启一个守护线程,用来给当前客户端快要过期的锁续航,续命的前提是,得判断是不是当前进程持有的锁,如果不是就不进行续。

如果快要过期,但是业务逻辑还没执行完成,自动对这个锁进行续期,重新设置过期时间。

这就是下一篇我要说的超神方案,加入看门狗机制实现锁自动续期。不过锁自动续期比较复杂,今天的 Redis 可重入分布式锁王者方案已经可以让你称霸武林,接下来上实战。

可重入分布式锁实战

关于 Spring Boot 的环境搭建以及普通分布式锁实战详见上一篇《纠正误区:这才是 SpringBoot Redis 分布式锁的正确实现方式》。今天直接上可重入锁核心代码。

ReentrantDistributedLock

可重入锁由ReentrantDistributedLock标识,它实现 Lock接口,构造方法实现 resourceName 和 StringRedisTemplate 的属性设置。

客户端唯一标识使用uuid:threadId 组成。

复制public class ReentrantDistributedLock implements Lock { /** * 锁超时时间,默认 30 秒 */ protected long internalLockLeaseTime = 30000; /** * 标识 id */ private final String id = UUID.randomUUID().toString(); /** * 资源名称 */ private final String resourceName; private final List<String> keys = new ArrayList<>(1); /** * Redis 客户端 */ private final StringRedisTemplate redisTemplate; public ReentrantDistributedLock(String resourceName, StringRedisTemplate redisTemplate) { this.resourceName = resourceName; this.redisTemplate = redisTemplate; keys.add(resourceName); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.

加锁 tryLock、lock

tryLock 以阻塞等待 waitTime 时间的方式来尝试获取锁。获取成功则返回 true,反之 false。

与 tryLock不同的是, lock 一直尝试自旋阻塞等待获取分布式锁,直到获取成功为止。而 tryLock 只会阻塞等待 waitTime 时间。

复制@Override 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(); // lua 脚本获取锁 Long ttl = tryAcquire(leaseTime, unit, threadId); // lock acquired if (ttl == null) { return true; } time -= System.currentTimeMillis() - current; // 等待时间用完,获取锁失败 if (time <= 0) { return false; } // 自旋获取锁 while (true) { long currentTime = System.currentTimeMillis(); ttl = tryAcquire(leaseTime, unit, threadId); // lock acquired if (ttl == null) { return true; } time -= System.currentTimeMillis() - currentTime; if (time <= 0) { return false; } } } @Override public void lock(long leaseTime, TimeUnit unit) { long threadId = Thread.currentThread().getId(); Long ttl = tryAcquire(leaseTime, unit, threadId); // lock acquired if (ttl == null) { return; } do { ttl = tryAcquire(leaseTime, unit, threadId); // lock acquired } while (ttl != null); } private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) { // 执行 lua 脚本 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LuaScript.reentrantLockScript(), Long.class); return redisTemplate.execute(redisScript, keys, String.valueOf(unit.toMillis(leaseTime)), getRequestId(threadId)); } private String getRequestId(long threadId) { return id + ":" + threadId; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.

解锁 unlock

复制public void unlock() { long threadId = Thread.currentThread().getId(); // 执行 lua 脚本 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LuaScript.reentrantUnlockScript(), Long.class); Long opStatus = redisTemplate.execute(redisScript, keys, String.valueOf(internalLockLeaseTime), getRequestId(threadId)); if (opStatus == null) { throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + id + " thread-id: " + threadId); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.

LuaScript

这个脚本就是在讲解可重入分布式锁原理具体逻辑已经解释过,这里就不再重复分析。

复制public class LuaScript { private LuaScript() { } /** * 可重入分布式锁加锁脚本 * * @return 当且仅当返回 `nil`才表示加锁成功;返回锁剩余过期时间是让客户端感知锁是否成功。 */ public static String reentrantLockScript() { return "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]);"; } /** * 可重入分布式锁解锁脚本 * * @return 当且仅当返回 `nil`才表示解锁成功; */ public static String reentrantUnlockScript() { return "if (redis.call(hexists, KEYS[1], ARGV[2]) == 0) then " + "return nil;" + "end; " + "local counter = redis.call(hincrby, KEYS[1], ARGV[2], -1); " + "if (counter > 0) then " + "redis.call(pexpire, KEYS[1], ARGV[1]); " + "return 0; " + "else " + "redis.call(del, KEYS[1]); " + "return 1; " + "end; " + "return nil;"; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.

RedisLockClient

最后,还需要提供一个客户端给方便使用。

复制@Component public class RedisLockClient { @Autowired private StringRedisTemplate redisTemplate; /** * 获取可重入分布式锁 * @param name * @return */ public Lock getReentrantLock(String name) { return new ReentrantDistributedLock(name, redisTemplate); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.

单元测试走一个,验证下分布式锁是否支持可重入。

复制@Slf4j @SpringBootTest(classes = RedisApplication.class) public class RedisLockTest { @Autowired private RedisLockClient redisLockClient; @Test public void testTryReentrantLockSuccess() throws InterruptedException { Lock lock = redisLockClient.getReentrantLock("order:pay"); try { boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS); if (!isLock) { log.warn("加锁失败"); return; } // 重复加锁 reentrant(lock); log.info("业务逻辑执行完成"); } finally { lock.unlock(); } } private void reentrant(Lock lock) throws InterruptedException { try { boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS); if (!isLock) { log.warn("加锁失败"); return; } log.info("业务逻辑执行完成"); } finally { lock.unlock(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.

有两个点需要注意。

释放锁的代码一定要放在 finally{} 块中。否则一旦执行业务逻辑过程中抛出异常,程序就无法执行释放锁的流程。只能干等着锁超时释放。加锁的代码应该写在 try {} 代码中,放在 try 外面的话,如果执行加锁异常(客户端网络连接超时),但是实际指令已经发送到服务端并执行,就会导致没有机会执行解锁的代码。

CHaya:“码哥,这个方案确实很王者,大开眼界,接下来的超神版可以实现看门狗自动续期么?”

鉴于篇幅有限,今天就跟大家介绍 Redis 可重入分布式锁王者方案,关注我,下一篇给你分享、超神版分布式锁解决方案。

本文地址:http://bhae.cn/html/148a6299789.html
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

全站热门

简单网络管理协议(SNMP)是用于IP网络设备管理的标准协议。典型的支持SNMP协议的设备有路由器、交换机、服务器、工作站、打印机及数据机柜等等。SNMP一般被网络管理系统用于按照管理员设定的条件来监视网络附加设备。SNMP是因特网协议套件中的一个组成部分,它由IETF机构定义。它包含一系列的网络管理标准,其中有一个应用层协议,一个数据库架构以及一组数据对象。SNMP将管理数据以变量的形式展示出来,这些变量描述了系统配置。同时这些变量可以被用于管理的应用查询(或者被设置)。为什么需要使用SNMPv3 尽管SNMPv3所增加的加密功能并不影响协议层面,但是新的文本惯例、概念及术语使得它看起来很不一样。SNMPv3在SNMP的基础之上增强了安全性以及远程配置功能。最初,SNMP最大的缺点就是安全性弱。SNMP的第一与第二个版本中,身份验证仅仅是在管理员与代理间传送一个明文的密码而已。目前每一个SNMPv3的信息都包含了被编码成8进制的安全参数。这些安全参数的具体意义由所选用的安全模型决定。SNMPv3提供了重要的安全特性:保密性 —— 加密数据包以防止未经授权的源监听。完整性 —— 数据的完整性特性确保数据在传输的时候没有被干扰,并且包含了可选的数据响应保护机制。身份验证 —— 检查数据是否来自一个合法的源。在ubuntu中安装SNMP服务器及客户端 打开终端运行下列命令sudo apt-get install snmpd snmp安装完成后需要做如下改变。配置SNMPv3 获得从外部守护进程访问的权限默认的安装仅提供本地的访问权限,假如想要获得外部访问权限,打开文件 /etc/default/snmpd。sudo vi /etc/default/snmpd改变下列内容将SNMPDOPTS=-Lsd -Lf /dev/null -u snmp -g snmp -I -smux,mteTrigger,mteTriggerConf -p /var/run/snmpd.pid改为SNMPDOPTS=-Lsd -Lf /dev/null -u snmp -I -smux -p /var/run/snmpd.pid -c /etc/snmp/snmpd.conf最后重启 snmpdsudo /etc/init.d/snmpd restart定义 SNMPv3 用户,身份验证以及加密参数 “securityLevel”参数使得SNMPv3有多种不同的用途。noAuthNoPriv —— 没有授权,加密以及任何安全保护!authNoPriv —— 需要身份认证,但是不对通过网络发送的数据进行加密。autoPriv —— 最可靠模式。需要身份认证而且数据会被加密。snmpd 的配置以及设置都保存在文件 /etc/snmp/snmpd.conf。使用编辑器编辑文件:sudo vi /etc/snmp/snmpd.conf在文件末尾添加以下内容:#createUser user1createUser user2 MD5 user2passwordcreateUser user3 MD5 user3password DES user3encryption#rouser user1 noauth 1.3.6.1.2.1.1rouser user2 auth 1.3.6.1.2.1rwuser user3 priv 1.3.6.1.2.1注:假如你需要使用自己的用户名/密码对的话,请注意密码及加密短语的最小长度是8个字符。同时,你需要做如下的配置以便snmp可以监听来自任何接口的连接请求。将#agentAddress udp:161,udp6:[::1]:161改为agentAddress udp:161,udp6:[::1]:161保存改变后的snmpd.conf文件并且重启守护进程:sudo /etc/init.d/snmpd restart

xyz域名怎么样?xyz域名怎么注册?

创意域名有什么定义和价值?新手必知一些诀窍

如何投资域名?投资域名有什么技巧?

本教程是向大家介绍Ubuntu14.4下Sublime Text 3无法输入中文解决方法,不是很难,方法很实用,同时感谢原创作者LunnLew的分享,希望这篇教程对大家有所帮助!工具/原料Ubuntu14.04搜狗输入法 for LinuxSublime text 3已知前置条件1、本经验目前在Ubuntu14.04环境下,已有搜狗输入法 for Linux和Sublime Text 3的情况下安装成功。END解决方法步骤21、保存下面的代码到文件sublime_imfix.c(位于~目录)复制代码代码如下:#include GdkWindow *window){ GtkIMContextClass *klass; g_return_if_fail (GTK_IS_IM_CONTEXT (context)); klass = GTK_IM_CONTEXT_GET_CLASS (context); if (klass->set_client_window) klass->set_client_window (context, window); g_object_set_data(G_OBJECT(context),window,window); if(!GDK_IS_WINDOW (window)) return; int width = gdk_window_get_width(window); int height = gdk_window_get_height(window); if(width != 0 && height !=0) gtk_im_context_focus_in(context);}2、将上一步的代码编译成共享库>libsublime-imfix.so,命令cd ~gcc -shared -o libsublime-imfix.so sublime_imfix.c  `pkg-config --libs --cflags gtk+-2.0` -fPIC3、然后将>libsublime-imfix.so拷贝到>sublime_text所在文件夹sudo mv libsublime-imfix.so /opt/sublime_text/4、修改文件/usr/bin/subl的内容>sudo gedit /usr/bin/subl将#!/bin/shexec /opt/sublime_text/sublime_text $@修改为>#!/bin/sh>LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so exec /opt/sublime_text/sublime_text $@此时,在命令中执行>subl 将可以使用搜狗for linux的中文输入>5、为了使用鼠标右键打开文件时能够使用中文输入,还需要修改文件sublime_text.desktop的内容。命令>sudo gedit /usr/share/applications/sublime_text.desktop将[Desktop Entry]中的字符串Exec=/opt/sublime_text/sublime_text %F修改为Exec=bash -c LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so exec /opt/sublime_text/sublime_text %F将[Desktop Action Window]中的字符串Exec=/opt/sublime_text/sublime_text -n修改为Exec=bash -c LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so exec /opt/sublime_text/sublime_text -n将[Desktop Action Document]中的字符串Exec=/opt/sublime_text/sublime_text --command new_file修改为Exec=bash -c LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so exec /opt/sublime_text/sublime_text --command new_file注意:修改时请注意双引号>,否则会导致不能打开带有空格文件名的文件。此处仅修改了>/usr/share/applications/sublime-text.desktop,但可以正常使用了。opt/sublime_text/目录下的>sublime-text.desktop可以修改,也可不修改。6、经过以上步骤我们能在Sublime中输入中文了。END以上就是Ubuntu14.4下Sublime Text 3无法输入中文的解决方法,希望这篇教程能帮助到大家!谢谢大家阅读该文教程!

域名如何与网站绑定?需要怎么做?

域名后缀vip代表什么?vip域名后缀怎么样?

js基础之setTimeout与setInterval原理分析

友情链接

滇ICP备2023000592号-9