最新要闻
- 世界微动态丨网友偶遇眼镜王蛇求助 博物杂志:务必远离、打输住院打赢坐牢
- 世界今亮点!Vtuber因直播《霍格沃茨之遗》被骚扰 宣布毕业
- 天天讯息:委员建议研究生招生规模动态扩大:缓解考研难
- 速看:兰博基尼领衔 今年值得期待的7款跑车 买不起还不能看看?
- 女子试用期被辞退 现场给HR普法:金句频出网友点赞称解气
- 每日聚焦:靠ChatGPT年入百万!合法还不限学历专业:一般人我不告诉他(doge)
- 全球新消息丨韩系车日子不好过!起亚狮铂拓界限时优惠:降3万还给大礼包
- 播报:LG:三星QD OLED电视更容易烧屏
- 世界聚焦:掏耳朵怎么就这么爽!
- 今日视点:不只全面屏!努比亚Z50 Ultra后摄惊艳:黄金镜皇组合
- 男子月薪3千相亲角“反向相亲”气到大妈 大爷理解:靠颜值吃饭
- 每日热文:吴京+杰森斯坦森主演!《巨齿鲨2》暑期上映 国内有望同步
- 浙大揭秘吃鱼为什么会变聪明 网友:告诉老默 我想吃鱼了
- 上海消保委提醒谨慎购买威马汽车:经营异常、消极应对投诉
- 特斯拉将放弃稀土材料 中国公司无惧:目前没有东西替代
- 即时:B站两款自研游戏将上线 CEO陈睿:能挣钱的游戏只剩下两种
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
环球快资讯丨Redis分布式锁常见坑点分析
日常开发中,基于 Redis 天然支持分布式锁,大家在线上分布式项目中都使用过 Redis 锁。本文主要针对日常开发中加锁过程中某些异常场景进行讲解与分析。本文讲解示例代码都在 https://github.com/wayn111/newbee-mall-pro项目 test
目录下 RedisLockTest
类中。
(相关资料图)
版本声明:
Spring Boot
版本 3.0.2- 演示项目地址:https://github.com/wayn111/newbee-mall-pro
- github地址:http://github.com/wayn111欢迎大家关注,点个star
一、任务超时,锁已经过期
这个异常场景说实话发生概率很低,大部分情况下加锁时任务执行都会很快,锁还没到期,任务自己就会删除锁。除非说任务调用第三方接口不稳定导致超时、数据库查询突然变得非常慢就可能会产生这个异常场景。
那怎么处理这个异常嘞?大部分人可能都会回答添加一个定时任务,在定时任务内检测锁快过期时,进行续期操作。OK,这么做好像是可以解决这个异常,那么博主在这里给出自己的见解。
1.1 先说一个暴论:如果料想到有这类异常产生,为什么不在加锁时,就把加锁过期时间设置大一点
不管所续期还是增大加锁时长,都会导致一个问题,其他线程会迟迟获取不到锁,一直被阻塞。那结果都一样,为什么不直接增大加锁时间?
想法是好的,但是实际上,加锁时间的设置是我们主观臆断的,我们无法保证这个加锁代码的执行时间一定在我们的锁过期时间内。作为一个严谨的程序员,我们需要对我们的代码有客观认知,任务执行可能几千上亿万次都是正常,但就是那么一次它执行超时了,可能由于外部依赖、当前运行环境的异常导致。
1.2 直接不设置过期时间,任务不执行完,不释放锁
如果在加锁时就不设置过期时间的话,理论上好像是可以解决这个问题,任务不执行完,锁就不会释放。但是作为程序员,总觉得哪里怪怪的,任务不执行完,锁就不会释放!
仔细想想,我们一般在 try 中进行加锁 在 finally 进行锁释放,这个好像也没毛病哦。但是实际针对一些极端异常场景下,如果任务执行过程中,服务器宕机、程序突然被杀掉、网络断连等都可能造成这个锁释放不了,另一个任务就一直获取不到锁。
这个方案程序正常的情况下,可以满足我们的要求,但是一旦发生异常将导致锁无法释放的后果,也就是说只要我们解决这个锁在异常场景下无法释放的问题,这个方案还是OK的。博主这里直接给出方案:
在不设置过期时间的加锁操作成功时,给一个默认过期时间比如三十秒,同时启动一个定时任务,给我们的锁进行自动续期,每隔 默认过期时间 / 3
秒后执行一次续期操作,发生锁剩余时长小于 默认过期时间 / 2
就重新赋值过期时长为三十秒。这样的话,可以保证锁必须由任务执行完才能释放,当程序异常发生时,仍然能保证锁会在三十秒内释放。
1.3 设置过期时间,任务不执行完,不释放锁
这个方案本质上与方案二的解决方案相同,还是启动定时任务进行续期操作,流程这里不做多余讲述。需要注意的就是加锁指定过期时间会比较符合我们的客观认知。实际上他的底层逻辑跟方案二相同,无非就是定时任务执行间隔,锁剩余时长续期判断要根据过期时间来计算。
综合来看:方案三会最合适,符合我们的客观认知,跟我们之前对 Redis 的使用逻辑较为相近。
二、线程B加锁执行中未释放锁,线程A释放了线程B的锁
说实话我仔细思考了一下这个异常场景,发现这个异常是个伪命题,如果线程 B 正在执行时,线程 A 怎么能获取到线程B的锁!线程 A 获取不到线程 B 的锁,谈何来去释放线程 B 的锁!如果线程 A 能获取到线程 B 的锁那么这个分布式锁的代码一开始就已经错了。
这里回到这个异常场景本身,我们可以给每个线程设置请求ID,加锁成功将请求ID设置为加锁 key 的对应 value,线程释放锁时需要判断当前线程的请求ID与加锁 key 的对应 value 是否相同,相同则可以释放锁,不相同则不允许释放。
三、线程加锁成功后继续申请加锁
这个场景主要发生在加锁代码内部调用栈过深,比如说加锁成功执行方法 a,在方法 a 内又重复申请了同一把锁,导致线程把自己锁住了,这个业界的主流叫法是叫锁的可重入性。
解决方式有两种,一是修改方法内的加锁逻辑,不要加同一把锁,修改方法 a 内的加锁 key 名称。二是针对加锁逻辑做修改,实现可重入性。
这里简单介绍如何实现可重入性,给每个线程设置请求ID,加锁成功将请求ID设置为加锁 key 的对应 value,针对同一个线程的重复加锁,判断当前线程已存在请求ID的情况下,请求ID直接与加锁 key 的对应 value 相比较,相同则直接返回加锁成功。
四、 代码实践
4.1 加锁自动续期实践
设置锁过期时间为10秒,然后该任务执行15秒,代码如下:
ps: 以下代码都可以在 https://github.com/wayn111/newbee-mall-pro项目
test
目录下RedisLockTest
类中找到
@Slf4j@SpringBootTest@RunWith(SpringRunner.class)public class RedisLockTest { @Autowired private RedisLock redisLock; @Test @Test public void redisLockNeNewTest() { String key = "test"; try { log.info("---申请加锁"); if (redisLock.lock(key, 10)) { // 模拟任务执行15秒 log.info("---加锁成功"); Thread.sleep(15000); log.info("---执行完毕"); } } catch (Exception e) { log.error(e.getMessage(), e); } finally { redisLock.unLock(key); } }}
执行如下:
可以看出就算任务执行超过过期时间也能通过自动续期让代码正常执行。
4.2 多线程下其他线程无法共同申请到同一把锁实践
启动两个线程,线程 A 先加锁, 线程 B 后枷锁
@Testpublic void redisLockReleaseSelfTest() throws IOException { new Thread(() -> { String key = "test"; try { log.info("---申请加锁"); if (redisLock.lock(key, 10)) { // 模拟任务执行15秒 log.info("---加锁成功"); Thread.sleep(15000); log.info("---执行完毕"); } else { log.info("---加锁失败"); } } catch (Exception e) { log.error(e.getMessage(), e); } finally { redisLock.unLock(key); } }, "thread-A").start(); new Thread(() -> { String key = "test"; try { Thread.sleep(100L); log.info("---申请加锁"); if (redisLock.lock(key, 10)) { // 模拟任务执行15秒 log.info("---加锁成功"); Thread.sleep(15000); log.info("---执行完毕"); } else { log.info("---加锁失败"); } } catch (Exception e) { log.error(e.getMessage(), e); } finally { redisLock.unLock(key); } }, "thread-B").start(); System.in.read();}
结果如下:
可以看到,线程 A 先申请到锁,线程 B 后申请锁,结果线程 B 申请加锁失败。
4.3 锁得可重入性实践
当前线程加锁成功后,在线程执行中继续申请同一把锁,代码如下:
@Testpublic void redisLockReEntryTest() { String key = "test"; try { log.info("---申请加锁"); if (redisLock.lock(key, 10)) { // 模拟任务执行15秒 log.info("---加锁第一次成功"); if (redisLock.lock(key, 10)) { // 模拟任务执行15秒 log.info("---加锁第二次成功"); Thread.sleep(15000); log.info("---加锁第二次执行完毕"); } else { log.info("---加锁第二次失败"); } Thread.sleep(15000); log.info("---加锁第一次执行完毕"); } else { log.info("---加锁第一次失败"); } } catch (Exception e) { log.error(e.getMessage(), e); } finally { redisLock.unLock(key); }}
结果如下:
4.4 加锁逻辑讲解
直接贴出本文最核心 RedisLock 类全部代码:
@Slf4j@Componentpublic class RedisLock { @Autowired public RedisTemplate redisTemplate; /** * 默认锁过期时间20秒 */ public static final Integer DEFAULT_TIME_OUT = 30; /** * 保存线程id-ThreadLocal */ private ThreadLocal stringThreadLocal = new ThreadLocal<>(); /** * 保存定时任务(watch-dog)-ThreadLocal */ private ThreadLocal executorServiceThreadLocal = new ThreadLocal<>(); /** * 加锁,不指定过期时间 * * @param key key名称 * @return boolean */ public boolean lock(String key) { return lock(key, null); } /** * 加锁 * * @param key key名称 * @param timeout 过期时间 * @return boolean */ public boolean lock(String key, Integer timeout) { Integer timeoutTmp; if (timeout == null) { timeoutTmp = DEFAULT_TIME_OUT; } else { timeoutTmp = timeout; } String nanoId; if (stringThreadLocal.get() != null) { nanoId = stringThreadLocal.get(); } else { nanoId = IdUtil.nanoId(); stringThreadLocal.set(nanoId); } RedisScript redisScript = new DefaultRedisScript<>(buildLuaLockScript(), Long.class); Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId, timeoutTmp); boolean flag = execute != null && execute == 1; if (flag) { ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); executorServiceThreadLocal.set(scheduledExecutorService); scheduledExecutorService.scheduleWithFixedDelay(() -> { RedisScript renewRedisScript = new DefaultRedisScript<>(buildLuaRenewScript(), Long.class); Long result = (Long) redisTemplate.execute(renewRedisScript, Collections.singletonList(key), nanoId, timeoutTmp); if (result != null && result == 2) { ThreadUtil.shutdownAndAwaitTermination(scheduledExecutorService); } }, 0, timeoutTmp / 3, TimeUnit.SECONDS); } return flag; } /** * 释放锁 * * @param key key名称 * @return boolean */ public boolean unLock(final String key) { String nanoId = stringThreadLocal.get(); RedisScript redisScript = new DefaultRedisScript<>(buildLuaUnLockScript(), Long.class); Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), nanoId); boolean flag = execute != null && execute == 1; if (flag) { if (executorServiceThreadLocal.get() != null) { ThreadUtil.shutdownAndAwaitTermination(executorServiceThreadLocal.get()); } } return flag; } private String buildLuaLockScript() { return """ local key = KEYS[1] local value = ARGV[1] local time_out = ARGV[2] local result = redis.call("get", key) if result == value then return 1; end local lock_result = redis.call("setnx", key, value) if tonumber(lock_result) == 1 then redis.call("expire", key, time_out) return 1; else return 0; end """; } private String buildLuaUnLockScript() { return """ local key = KEYS[1] local value = ARGV[1] local result = redis.call("get", key) if result ~= value then return 0; else redis.call("del", key) end return 1; """; } private String buildLuaRenewScript() { return """ local key = KEYS[1] local value = ARGV[1] local timeout = ARGV[2] local result = redis.call("get", key) if result ~= value then return 2; end local ttl = redis.call("ttl", key) if tonumber(ttl) < tonumber(timeout) / 2 then redis.call("expire", key, timeout) return 1; else return 0; end """; }}
加锁逻辑:这里我把加锁逻辑分解成三步展示给大家
- 加锁前:先判断当前线程是否存在请求ID,不存在则生成,存在就直接使用
- 加锁中:通过 lua 脚本执行原子加锁操作,加锁时先判断当前线程ID与加锁 key 得 value 是否相等,相等则是同一个线程的锁重入,直接返加锁成功。不相等则设置加锁 value 为请求ID以及过期时间。
- 加锁后:启动一个定时任务,每隔
过期时间 / 3
秒后执行一次续期操作,发现锁剩余时间不足过期时间 / 2
秒后,通过 lua 脚本进行续期操作。
解锁逻辑:这里我把解锁逻辑分解成两步展示给大家
- 解锁中:通过 lua 脚本执行解锁操作,先判断加锁 key 的 value 是否与自身请求ID相同,相同则让解锁,不相同则不让解锁。
- 解锁后:删除定时任务。
五、总结
其实本文得核心逻辑有许多都是参考 Redission 客户端而写,对于这些常见得坑点,博主结合自身思考,业界知识总结并自己实现一个分布式锁得工具类。希望大家看了有所收获,对日常业务中 Redis 分布式锁的使用能有更深的理解。
环球快资讯丨Redis分布式锁常见坑点分析
世界今日讯!eas里客户端保存,提交里增加校验规则和必填
访问者模式
世界微动态丨网友偶遇眼镜王蛇求助 博物杂志:务必远离、打输住院打赢坐牢
世界今亮点!Vtuber因直播《霍格沃茨之遗》被骚扰 宣布毕业
天天讯息:委员建议研究生招生规模动态扩大:缓解考研难
全球聚焦:收个滴滴Offer:从小伙三面经历,看看需要学点啥?
环球热资讯!Study for Go! Chapter one - Type
环球最新:手写模拟Spring底层原理-Bean的创建与获取
速看:兰博基尼领衔 今年值得期待的7款跑车 买不起还不能看看?
女子试用期被辞退 现场给HR普法:金句频出网友点赞称解气
每日聚焦:靠ChatGPT年入百万!合法还不限学历专业:一般人我不告诉他(doge)
全球新消息丨韩系车日子不好过!起亚狮铂拓界限时优惠:降3万还给大礼包
zip文件结构
头条:与时俱进推动智慧城市建设,智慧管网监测加强城市治理能力
全球视讯!Java项目集成工作流activiti,会签
简单介绍Python中如何给字典设置默认值
播报:LG:三星QD OLED电视更容易烧屏
世界聚焦:掏耳朵怎么就这么爽!
今日视点:不只全面屏!努比亚Z50 Ultra后摄惊艳:黄金镜皇组合
男子月薪3千相亲角“反向相亲”气到大妈 大爷理解:靠颜值吃饭
每日热文:吴京+杰森斯坦森主演!《巨齿鲨2》暑期上映 国内有望同步
环球视点!ffmpeg视频上传及压缩Linux配置篇下
世界快资讯:【Avalonia】【跨平台】关于Prism项目模块化在Linux下路径问题
浙大揭秘吃鱼为什么会变聪明 网友:告诉老默 我想吃鱼了
上海消保委提醒谨慎购买威马汽车:经营异常、消极应对投诉
特斯拉将放弃稀土材料 中国公司无惧:目前没有东西替代
即时:B站两款自研游戏将上线 CEO陈睿:能挣钱的游戏只剩下两种
《生化危机4:重制版》新演示/截图 里昂拯救黑丝碍事梨
焦点日报:配置资源管理Secret和ConfigMap
环球视点!Windows故障转移群集 和 SQLServer AlwaysOn 搭建教程
(数据库系统概论|王珊)第九章关系查询处理和关系优化-第一节:查询处理
全球速递!视频上传及压缩SpringBoot篇上
世界热门:el-input 使用 回车键会刷新页面的问题
全球最强!传音260W快充手机将亮相:10分钟内充满
性能对标奔驰大G 比亚迪“F品牌”首车曝光:够硬够强
世界快消息!传欧盟准备批准微软收购动视-暴雪
当前报道:女司机“神操作”:100来公里高速连撞4次 竟甩锅路太窄
世界视讯!又一大作优化翻车!《卧龙:苍天陨落》RTX 4090依旧闪退
12GB+256GB到手仅2699元!Redmi K60正式开启降价
温州特斯拉事故驾驶员家属发声:记不清车辆失控场景 妻子去世自责
环球时讯:中国航天员遇到外星人怎么办?载人航天总师:积极交流 星际合作
焦点要闻:漫威等好莱坞大片中国市场遇冷:大家不爱看了 不符合国人审美、文化观
550元 富士发布Instax Mini 12拍立得相机 支持APP存照片
环球即时:卷成白菜价!致态TiPlus 7100固态硬盘新史低:1TB仅549元
环球头条:马斯克10万亿美元“改造地球”背景下!特斯拉电机要完全不用稀土:专家回应有可能
读Java性能权威指南(第2版)笔记07_即时编译器上
今头条!天问二号任务已获得国家批准立项:要从小行星2016 HO3采样返回
实现 Vue 折叠面板组件
委员:996制度是导致就业难、生育率低的重大原因
当前播报:电商价格战开打!京东百亿补贴上线:全场包邮 买贵双倍赔
特斯拉未来要狂暴降价:就靠这改变世界?其实都被骗了!
世界快资讯:豆瓣8.9分!韩国拼体格真人秀在欧美爆红
暗黑三国风!《卧龙:苍天陨落》正式上线:298元 GTX 1650就能玩
3.3 数据结构 时间复杂度 和空间复杂度 计算
环球简讯:004. html篇之《标签分类和嵌套》
天天动态:星巴克国内最大对手!瑞幸咖啡财报:年收入首次突破百亿
硬件狗狗3.3新版发布:跑分排行 实时PK
【环球热闻】使用ansible部署服务到k8s
专家:双休制度很难被改变 可以试试“做四休三”
【当前独家】同样是PCIe 5.0 SSD:Intel、AMD跑分竟不一样!差距达30%
世界观焦点:大熊猫为什么近期扎堆回国?美日等国养不起“国宝”了吗?完全是误解
当前速讯:女子称因准点下班试用期第3天被辞退:还被领导一顿痛骂
全球聚焦:R数据分析:做量性研究的必备“家伙什”-furniture包介绍
003. html篇之《表单》
全球即时:Codeforces 1774 G Segment Covering 题解 (观察性质,倍增)
【全球播资讯】Feign踩坑源码分析 -- 请求参数分号变逗号
【全球快播报】火箭弹电子版领取处>>
视讯!完美还原!玩家用虚幻5复刻《狂飙》高启强老家:桌上还有孙子兵法
《龙马精神》4月上映!69岁成龙再跳120米高摩天轮 本人直言小事情
天天快资讯:C++面经(持续更新)
今亮点!实验楼(规则)怪谈
热推荐:电视剧《三体》豆瓣评分上涨 于和伟:《三体》涨分像涨工资
热门看点:《卧龙:苍天陨落》今晚零点正式解锁!乱世三国冒险即将开启
前沿资讯!口感醇正!熊猫精酿好时光皮尔森啤酒好价:2.8元/听
即时:中国科技公司:让老外开眼了
完爆H.265!优酷用上H.266编解码:最便宜手机放视频也丝般顺滑
世界今头条!搭建两台web服务器基于HAProxy实现负载均衡
焦点短讯!路飞-day5——git 多分支开发、git远程仓库、ssh方式连接远程仓库、协同开发、冲突解决、线上分支合并、远程仓库回滚
简讯:(数据库系统概论|王珊)第七章数据库设计:习题
全球时讯:我国网民规模达10.67亿!短视频用户首次突破10亿:你每天刷多久?
B站发布2022年Q4及全年财报:全年营收219亿元 Q4日活用户达9280万
零排放、低噪音!国内首列氢燃料混合动力铰接轻轨车下线
环球微头条丨003 jmeter连接数据库及jmeter关联提取器
git-git、gitee使用介绍
面试官:从 MySQL 读取 100w 数据进行处理,应该怎么做?问倒一大遍!
天天快播:常用的Prestosql
python3和scrapy使用亿牛云隧道代理问题以及代码
为何近半数安卓用户想换苹果?背后原因揭开
天天快资讯:国人也买不动了!1月iPhone全球销量大跌11% 苹果会降价刺激销量吗?
当前速看:纯电飞行250公里 国产厂商创电动载人飞行器新纪录
【世界新要闻】公司招聘会计要求一定是A型血 网友:很奇葩
每日快讯!中国空间站成功首次“点火”!高速相机拍下神奇一幕
今日观点!Pod控制器
创建型:构造器模式
天天最新:Python类和对象的绑定方法及非绑定方法
答菲洗脸巾80片到手6.9元:干湿两用 不掉毛絮
全球视点!男子犯困竟在高速行车道睡觉30分钟 科普:连续开车不应超4小时
甄子丹谈好莱坞对亚裔的刻板印象:怎么都这么老套?
特斯拉减少75%碳化硅用量 马斯克一句话干崩第三代半导体 上市公司回应