架构(十三)动态本地锁

一、引言

        加锁大家都知道,但是目前提供动态锁的基本都是分布式锁,根据订单或者某个收费款项进行加锁。比如这个1订单要收刷卡费用,那就OREDER_1做为key丢到redis进行分布式加锁。这也是当下分布式锁最流行的方式。

        但是对于平台项目或者一些并发程度低的场景,分布式锁就没有必要了,本地锁更加方便。但是本地锁只有synchronized、ReentrantLock之类的方式,想动态的加锁只用他们是实现不了的。

二、实现

        那么如果要实现动态的本地锁怎么做呢?

        先看看redis的分布式锁是怎么做的,他其实就是利用redis的单线程往里面存一个值,如果已经有线程存了,并发的线程就存不进去,只能等。只不过各个redis的客户端还要考虑删除的并发性、锁超时删除、加锁等待这些问题。

1、ConcurrentHashMap

        借鉴这个方案,本地也可以加个存储,为了并发的可读性使用ConcurrentHashMap,这里可以有效的避免其他线程解锁删除缓存

private static final ConcurrentHashMap<String, String> map = 
new ConcurrentHashMap<>();

        加锁就把OREDER_1塞到map里面塞的过程需要防止并发,所以使用synchronized之类的就可以,因为map塞数据可比业务执行的加锁时间短多了

private synchronized static boolean getLock(String key) {// map里面塞一下很快,可以使用synchronizedif (map.containsKey(key)) {// 懒汉模式,再判断一遍,免得两个线程一起通过了外层的判断return false;}map.put(key,key);return true;}

        加锁的方法就是先判断一下有没有已经占了位置的,没有就往map里面占位置

public static boolean tryLock(String key, long timeout, TimeUnit unit) {if (map.containsKey(key)) {return false;}return getLock(key);}

        解锁就是直接删除

public static void unLock(String key) {if (!map.containsKey(key)) {return;}// 释放锁map.remove(key);}

        这是最简单的做法,那么上面的实现有什么问题呢?最大的问题就是删除的时候可能被其他线程给删了,毕竟不会所有人都按照预想的去使用工具,安全是架构应该考虑的。

        还有锁的超时、等待多长时间没有锁就失败两个功能点

2、优化解锁、锁超时

       要优化这个实现就可以结合ReentrantLock,他有判断是否本线程加锁和等待多长时间进行加锁的api,如果自己实现相当于把他里面的线程存储和睡眠唤醒给重复做一遍,没有必要。

        那么怎么用它呢,map的value存储一个lock,相当于一个key一个lock,生成和删除的时候可以使用synchronized,这个和刚刚说的一样,map里面塞一个删一个是很快的,new一个lock和lock.unlock也是很快的,主要的时间都在业务处理和同一场景下不同单号之间的阻塞

public class LockKeyUtil {private static final ConcurrentHashMap<String, ReentrantLock> map = new ConcurrentHashMap<>();/*** 从map里获取锁 如果存在则返回 不存在则创建** @param key key*/private synchronized static ReentrantLock getReentrantLock(String key) {if (map.containsKey(key)) {return map.get(key);}return map.compute(key, (k, lock) -> {lock = new ReentrantLock();return lock;});}/*** * @param key* @param waitTimeout* @param unit* @return*/public static boolean tryLock(String key, long waitTimeout, TimeUnit unit) {ReentrantLock lock = getReentrantLock(key);boolean res;try {res = lock.tryLock(waitTimeout, unit);} catch (InterruptedException e) {unLock(key);res = false;}return res;}/*** 释放锁** @param key key*/public static synchronized void unLock(String key) {if (!map.containsKey(key)) {return;}ReentrantLock lock = map.get(key);// 释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();map.remove(key);}}}

3、优化加锁队列

        上面的实现可以看到还有一个问题,如果在tryLock的时候,多个线程进入了,那么第一个线程解锁的时候把他移除map就有问题了,所以unlock还可以根据加锁队列优化一下

        通过getQueueLength知道有没有在等待加锁的线程,当然了这三行代码不是原子性的,所以是有可能刚刚取完队列还没删除map之前就有线程去加锁了,但是这种情况并发几率可以说是万分之一不到,可以不考虑

public synchronized static void unLock(String key) {if (!map.containsKey(key)) {return;}ReentrantLock lock = map.get(key);// 释放锁if (lock.isHeldByCurrentThread()) {int wait = lock.getQueueLength();lock.unlock();if (wait == 0) {map.remove(key);}}}

4、执行超时自动解锁

        这里还有一个执行超时自动解锁的功能,其实感觉没必要,用的时候一般都是在finally里面去unlock,所以几乎不会有不解锁的情况

String key = LOCK + request.getId();boolean locked = LockKeyUtil.tryLock(key, 3, TimeUnit.SECONDS);if (!locked) {throw new OrderException("LOCK_FAIL");}try {//业务处理} finally {LockKeyUtil.unLock(key);}

       

三、测试

1、相同key测试代码

        这段代码主要是让线程1或者3先拿到锁,那么其中一个和3就一定处于等待状态,如果是3在等,他到最后也抢不到锁

public static void main(String[] args) {String key = "order_1666";Thread thread1 = new Thread(() -> {LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread1:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key, 2, TimeUnit.SECONDS);System.out.println("thread1:" + Thread.currentThread().getName() + " lock res:" + res);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}LockKeyUtil.unLock(key, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread1:" + Thread.currentThread().getName() + " end:" + formattedDateTime);});Thread thread2 = new Thread(() -> {try {Thread.sleep(1);LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread2:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key, 5, TimeUnit.SECONDS);System.out.println("thread2:" + Thread.currentThread().getName() + " lock res:" + res);Thread.sleep(5000);LockKeyUtil.unLock(key, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread2:" + Thread.currentThread().getName() + " end:" + formattedDateTime);} catch (InterruptedException e) {e.printStackTrace();}});Thread thread3 = new Thread(() -> {LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread3:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key, 1, TimeUnit.SECONDS);System.out.println("thread3:" + Thread.currentThread().getName() + " lock res:" + res);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}LockKeyUtil.unLock(key, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread3:" + Thread.currentThread().getName() + " end:" + formattedDateTime);});thread1.start();thread2.start();thread3.start();}

 

2、相同key测试结果

        结果和预期相符,线程1先抢到了,执行完之后线程2 抢到,线程3在过程中就失败了

        整个执行链路也给打出来了

thread1:Thread-0 start:2024-02-06 13:59:12
thread3:Thread-2 start:2024-02-06 13:59:12
thread2:Thread-1 start:2024-02-06 13:59:12
thread:Thread-0 tryLock key:order_1666 start
thread:Thread-2 tryLock key:order_1666 start
thread:Thread-1 tryLock key:order_1666 start
thread:Thread-0 getReentrantLock key:order_1666 start
thread:Thread-0 getReentrantLock key:order_1666 new ReentrantLock()
thread:Thread-1 getReentrantLock key:order_1666 start
thread:Thread-0 tryLock key:order_1666res:true
thread1:Thread-0 lock res:true
thread:Thread-1 getReentrantLock key:order_1666 containsKey
thread:Thread-2 getReentrantLock key:order_1666 start
thread:Thread-2 getReentrantLock key:order_1666 containsKey
thread:Thread-2 tryLock key:order_1666res:false
thread3:Thread-2 lock res:false
thread:Thread-0 unLock key:order_1666 unlock success
thread:Thread-2 unLock key:order_1666 not isHeldByCurrentThread
thread:Thread-1 tryLock key:order_1666res:true
thread2:Thread-1 lock res:true
thread1:Thread-0 end:2024-02-06 13:59:14
thread3:Thread-2 end:2024-02-06 13:59:14
thread:Thread-1 unLock key:order_1666 unlock success
thread:Thread-1 unLock key:order_1666 map remove
thread2:Thread-1 end:2024-02-06 13:59:19

3、不同key测试

        就是多加一个key去加锁解锁,看能不能同时加同时解        

public static void main(String[] args) {String key = "order_1666";Thread thread1 = new Thread(() -> {LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread1:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key, 2, TimeUnit.SECONDS);System.out.println("thread1:" + Thread.currentThread().getName() + " lock res:" + res);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}LockKeyUtil.unLock(key, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread1:" + Thread.currentThread().getName() + " end:" + formattedDateTime);});Thread thread2 = new Thread(() -> {try {Thread.sleep(1);LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread2:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key, 5, TimeUnit.SECONDS);System.out.println("thread2:" + Thread.currentThread().getName() + " lock res:" + res);Thread.sleep(5000);LockKeyUtil.unLock(key, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread2:" + Thread.currentThread().getName() + " end:" + formattedDateTime);} catch (InterruptedException e) {e.printStackTrace();}});String key3 = "order_1999";Thread thread3 = new Thread(() -> {LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String formattedDateTime = now.format(formatter);System.out.println("thread3:" + Thread.currentThread().getName() + " start:" + formattedDateTime);boolean res = LockKeyUtil.tryLock(key3, 1, TimeUnit.SECONDS);System.out.println("thread3:" + Thread.currentThread().getName() + " lock res:" + res);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}LockKeyUtil.unLock(key3, true);now = LocalDateTime.now();formattedDateTime = now.format(formatter);System.out.println("thread3:" + Thread.currentThread().getName() + " end:" + formattedDateTime);});thread1.start();thread2.start();thread3.start();}

        结果是不同的key可以同时加锁的,这就实现了锁的动态性

thread2:Thread-1 start:2024-02-06 14:06:28
thread1:Thread-0 start:2024-02-06 14:06:28
thread3:Thread-2 start:2024-02-06 14:06:28
thread3:Thread-2 lock res:true
thread2:Thread-1 lock res:true
thread3:Thread-2 end:2024-02-06 14:06:29
thread1:Thread-0 lock res:false
thread1:Thread-0 end:2024-02-06 14:06:32
thread2:Thread-1 end:2024-02-06 14:06:33

四、总结        

        最后的实现就是这样了

f6d008a35cc144f89dec2df89714664d.png

        看起来很简单的功能,要考虑的东西其实很多,这种实现也不是唯一的,有兴趣可以跟作者讨论其他的实现方案

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2779741.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

C++ | string类按位赋值小技巧

一切的起因是string类的谜之初始化。 在写代码的时候&#xff0c;我发现即使没有用字符串初始化string对象&#xff0c;也可以对string对象进行下标操作&#xff0c;就像这样&#xff1a; #include<iostream> #include<string> using namespace std; int main() {…

读书笔记之《运动改造大脑》:运动是最佳的健脑丸

《运动改造大脑》的作者是约翰•瑞迪&#xff08;John Ratey&#xff09; / 埃里克•哈格曼&#xff08;Eric Hagerman&#xff09;&#xff0c;原著名称为&#xff1a;Spark&#xff1a;the revolutionary new science of exercise and the brain&#xff0c;于 2013年出版约翰…

Activiti7(流程引擎)简单笔记,附带作者执行的Demo代码文件

文章目录 一、Activiti7流程基础1、最简单的流程2、流程值表达式3、方法表达式4、节点监听器5、流程变量6、候选人7、候选人组8、流程网关排他网关并行网关包容网关事件网关 二、Activiti7流程事件1、定时器事件定时器开始事件定时器中间事件定时器边界事件 2、消息事件消息开始…

【EAI 015】CLIPort: What and Where Pathways for Robotic Manipulation

论文标题&#xff1a;CLIPort: What and Where Pathways for Robotic Manipulation 论文作者&#xff1a;Mohit Shridhar1, Lucas Manuelli, Dieter Fox1 作者单位&#xff1a;University of Washington, NVIDIA 论文原文&#xff1a;https://arxiv.org/abs/2109.12098 论文出处…

问题:重热现象可使多级汽轮机的理想焓降增加,重热系数越大,多级汽轮机的内效率就越低。 #学习方法#微信#媒体

问题&#xff1a;重热现象可使多级汽轮机的理想焓降增加&#xff0c;重热系数越大&#xff0c;多级汽轮机的内效率就越低。 参考答案如图所示

FL Studio如何改变轨道颜色 FL Studio波形颜色如何自定义 flstudio21中文版下载 FL Studio 设置颜色

FL Studio如何改变轨道颜色&#xff1f;FL Studio的轨道颜色可以在播放列表或混音台界面进行修改。FL Studio波形颜色如何自定义&#xff1f;FL Studio的波形文件颜色、名称、图标等信息都是可以自定义的&#xff0c;下文将给大家详细讲述。 一、FL Studio如何改变轨道颜色 在…

05-编码篇-H264文件分析

通过前面的分析&#xff0c;我们可以看出常规情况下&#xff0c;是将视频以帧的单位进行处理&#xff0c;比如I帧&#xff0c;P帧&#xff0c;B帧等。 但是这些帧是如何以文件方式保存的呢&#xff0c;这节我们主要对H264的保存方式作一个了解。 一帧图片通过编码后&#xff0c…

【教3妹学编程-算法题】价值和小于等于 K 的最大数字

3妹&#xff1a;2哥&#xff0c;新年好鸭~ 2哥 : 新年好&#xff0c;3妹这么早啊 3妹&#xff1a;是啊&#xff0c;新年第一天要起早&#xff0c;这样就可以起早一整年 2哥 :得&#xff0c;我还不了解你&#xff0c;每天晒到日上三竿 3妹&#xff1a;嘿嘿嘿嘿&#xff0c;一年是…

CTFshow-WEB入门-信息搜集

web1&#xff08;查看注释1&#xff09; wp 右键查看源代码即可找到flag web2&#xff08;查看注释2&#xff09; wp 【CtrlU】快捷键查看源代码即可找到flag web3&#xff08;抓包与重发包&#xff09; wp 抓包后重新发包&#xff0c;在响应包中找到flag web4&#xff08;robo…

OpenAI---提示词工程的6大原则

OpenAI在官方的文档里上线了Prompt engineering&#xff0c;也就是提示词工程指南&#xff0c;其中OpenAI有提到写提示词的6条大的原则&#xff0c;它们分别是&#xff1a; &#xff08;1&#xff09;Write clear instructions&#xff08;写出清晰的指令&#xff09; &#xf…

基于PHP网上图书销售商城系统qo85w

软件体系结构方案&#xff1a;由于本系统需要在不同设备上都能运行&#xff0c;而且电脑配置要求也要越低越好&#xff0c;为了实现这一要求&#xff0c;经过考虑B/S结构成为最佳之选。使用B/S结构的系统可以几乎在任何电脑上运行&#xff0c;只要浏览器可以正常工作就可以正常…

锐捷(十九)锐捷设备的接入安全

1、PC1的IP地址和mac地址做全局静态ARP绑定; 全局下&#xff1a;address-bind 192.168.1.1 mac&#xff08;pc1&#xff09; G0/2:ip verify source port-securityarp-check 2、PC2的IP地址和MAC地址做全局IPMAC绑定&#xff1a; Address-bind 192.168.1.2 0050.7966.6807Ad…

MVVM模型

MVVM模型M模型&#xff08;Model&#xff09;对应data中的数据&#xff08;普通的JS对象&#xff09;V视图&#xff08;View&#xff09;对应模板&#xff08;Vue的模板经过解析形成的页面&#xff0c;页面生成的DOM结构&#xff09;VMVue实例对象&#xff08;ViewModel&#x…

SpringBoot3整合Knife4j

前置&#xff1a; 官网&#xff1a;快速开始 | Knife4j gitee&#xff1a;swagger-bootstrap-ui-demo: knife4j 以及swagger-bootstrap-ui 集成框架示例项目 - Gitee.com 1.依赖引入&#xff1a; ps&#xff1a;json处理需要引入相关包 <dependency><groupId>c…

基于Java (spring-boot)的电子商城管理系统

一、项目介绍 &#xff08;1&#xff09;商品管理模块&#xff1a;实现了商品的基本信息录入、图片上传、状态管理等相关功能。 &#xff08;2&#xff09;商品分类模块&#xff1a;实现了分类的增删改查、分类层级管理、商品分类的关联等功能。 &#xff08;3&#xff09;订…

【Unity】实用功能开发(一)实现在UI中用RawImage实时展示3D模型(背景透明,并通过UI防止3D场景遮挡)并可以通过分层完成:游戏中的人物状态展示界面,小地图,人物实时头像状态等功能

有时由于项目效果需要&#xff0c;部分功能的实现受到阻碍&#xff0c;这里收集一些已实现的思路和方法&#xff0c;每次会记录大致需求和遇到的问题&#xff0c;如果有更好的想法&#xff0c;欢迎评论区讨论&#xff01;&#xff01;&#xff01; 目录 功能描述&#xff1a;…

Mac 版 Excel 和 Windows 版 Excel的区别

Excel是一款由微软公司开发的电子表格程序&#xff0c;广泛应用于数据处理、分析和可视化等领域。它提供了丰富的功能和工具&#xff0c;包括公式、函数、图表和数据透视表等&#xff0c;帮助用户高效地处理和管理大量数据。同时&#xff0c;Excel还支持与其他Office应用程序的…

C#实现矩阵乘法

目录 一、使用的方法 1.矩阵 2.矩阵的乘法原理 二、实例 1.源码 2.生成效果 一、使用的方法 矩阵相当于一个数组&#xff0c;主要用来存储一系列数&#xff0c;例如&#xff0c;mn矩阵是排列在m行和n列中的一系列数&#xff0c;mn矩阵可与一个np矩阵相乘&#xff0c;结果…

Java:集合以及集合进阶 --黑马笔记

一、集合概述和分类 1.1 集合的分类 除了ArrayList集合&#xff0c;Java还提供了很多种其他的集合&#xff0c;如下图所示&#xff1a; 我想你的第一感觉是这些集合好多呀&#xff01;但是&#xff0c;我们学习时会对这些集合进行分类学习&#xff0c;如下图所示&#xff1a;…

中科大计网学习记录笔记(九):DNS

前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面的部分发现信…