架构(十三)动态本地锁

一、引言

        加锁大家都知道,但是目前提供动态锁的基本都是分布式锁,根据订单或者某个收费款项进行加锁。比如这个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里面塞一下很快,可以使用synchronized
        if (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

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

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

# Memory Analyzer (MAT) 在实际开发中的使用

Memory Analyzer (MAT) 在实际开发中的使用 文章目录 Memory Analyzer (MAT) 在实际开发中的使用概述注意点基本使用检查概述获取直方图View the Dominator Tree到GC根的路径 使用示例制作堆dumpHeapDumpOnOutOfMemoryErrorJmap 生成堆Dump Mat打开堆快照HistogramThread Overv…

SpringCloud-Ribbon实现负载均衡

在微服务架构中&#xff0c;负载均衡是一项关键的技术&#xff0c;它可以确保各个服务节点间的负载分布均匀&#xff0c;提高整个系统的稳定性和性能。Spring Cloud 中的 Ribbon 就是一种负载均衡的解决方案&#xff0c;本文将深入探讨 Ribbon 的原理和在微服务中的应用。 一、…

外包干了一个月,技术明显进步。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入南京某软件公司&#xff0c;干了接近2年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了2年的功能测试&…

CSP-202012-1-期末预测之安全指数

CSP-202012-1-期末预测之安全指数 题目很简单&#xff0c;直接上代码 #include <iostream> using namespace std; int main() {int n, sum 0;cin >> n;for (int i 0; i < n; i){int w, score;cin >> w >> score;sum w * score;}if (sum > 0…

STM32——OLED(2)

目录 一、OLED显示屏介绍 引脚说明&#xff1a; 二、OLED驱动 1. 基本认识 2. OLED 驱动原理 及过程 三、SSD1306工作时序 (8080时序&#xff09; 1. 8080并口读/写过程 2. SSD1306工作时序 (8080时序) 四、屏幕显示 1. GRAM 补&#xff1a; 2. 画点原理 3. 显示字…

尚硅谷 Vue3+TypeScript 学习笔记(中)

目录 三、路由 3.1. 【对路由的理解】 3.2. 【基本切换效果】 3.3. 【两个注意点】 3.4.【路由器工作模式】 3.5. 【to的两种写法】 3.6. 【命名路由】 3.7. 【嵌套路由】 3.8. 【路由传参】 query参数 params参数 3.9. 【路由的props配置】 3.10. 【 replace属性…

Java实现快乐贩卖馆管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 搞笑视频模块2.3 视频收藏模块2.4 视频评分模块2.5 视频交易模块2.6 视频好友模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 搞笑视频表3.2.2 视频收藏表3.2.3 视频评分表3.2.4 视频交易表 四、系…

Zustand:简化状态管理的现代React状态库

Zustand&#xff1a;简化状态管理的现代React状态库 Zustand是一个用于管理状态的现代React状态库。它提供了简洁、可扩展和高效的状态管理解决方案&#xff0c;使得在React应用中处理复杂的状态逻辑变得更加容易和直观。本文将介绍Zustand的主要特点、使用方法以及它在React开…

vim常用命令以及配置文件

layout: article title: “vim文本编译器” vim文本编辑器 有三种模式: 命令模式 文本模式, 末行模式 vim命令大全 - 知乎 (zhihu.com) 命令模式 插入 i: 切换到输入模式&#xff0c;在光标当前位置开始输入文本。 a: 进入插入模式&#xff0c;在光标下一个位置开始输入文…

JavaWeb02-MyBatis

目录 一、MyBatis 1.概述 2.JavaEE三层架构简单介绍 &#xff08;1&#xff09;表现层 &#xff08;2&#xff09;业务层 &#xff08;3&#xff09;持久层 3.框架 4.优势 &#xff08;1&#xff09;JDBC的劣势 &#xff08;2&#xff09;MyBatis优化 5.使用 &#…

算法刷题:移动零

移动零 .题目链接详解curdesc算法原理 答案 . 题目链接 移动零 详解 题目要求我们要把数组中所有的零都移动到数组的末尾,且要求其余数字顺序不改变.这道题,我们使用到的是双指针算法: 利用两个指针,将数组分为三个部分, 三个区间分别为 [0,desc][desc1,cur-1][cur,n-1] 在…

HTML 标签

HTML&#xff1a;超文本标记语言 HTML骨架结构&#xff1a; html标签&#xff1a;网页的整体 head标签&#xff1a;网页的头部 body标签&#xff1a;网页的身体 HTML的注释 VS code中&#xff1a;ctrl/ 浏览器不会执行注释 HTML标签的构成&#xff1a; 双标签&#xff1a…

DC-9靶机渗透详细流程

信息收集&#xff1a; 1.存活扫描&#xff1a; arp-scan -I eth0 -l 发现靶机ip&#xff1a;192.168.10.132 └─# arp-scan -I eth0 -l 192.168.10.1 00:50:56:c0:00:08 (Unknown) 192.168.10.2 00:50:56:e5:b1:08 (Unknown) 192.168.10.132 //靶机 00:0c…

物联网和工业4.0

在当今这个快速发展的技术时代&#xff0c;物联网&#xff08;IoT&#xff09;和工业4.0成为了推动全球进入新工业时代的两大驱动力。对于刚入行的人来说&#xff0c;深入理解这两个概念及其背后的技术原理&#xff0c;对于把握未来的职业机会至关重要。 物联网&#xff0c;简…

备战蓝桥杯---动态规划(理论基础)

目录 动态规划的概念&#xff1a; 解决多阶段决策过程最优化的一种方法 阶段&#xff1a; 状态&#xff1a; 决策&#xff1a; 策略&#xff1a; 状态转移方程&#xff1a; 适用的基本条件 1.具有相同的子问题 2.满足最优子结构 3.满足无后效性 动态规划的实现方式…

Go内存优化与垃圾收集

Go提供了自动化的内存管理机制&#xff0c;但在某些情况下需要更精细的微调从而避免发生OOM错误。本文介绍了如何通过微调GOGC和GOMEMLIMIT在性能和内存效率之间取得平衡&#xff0c;并尽量避免OOM的产生。原文: Memory Optimization and Garbage Collector Management in Go 本…

2024-02-08(Flume)

1.Flume 的架构和MQ消息队列有点类似 2.Flume也可以做数据的持久化操作 在Channel部分选择使用File channel组件 3.Flume进行日志文件监控 场景&#xff1a;企业中应用程序部署后会将日志写入到文件中&#xff0c;我们可以使用Flume从各个日志文件将日志收集到日志中心以便…

用bootstrap结合jQuery实现简单的模态对话框

嗨害嗨&#xff0c;我又来了奥。今天呢&#xff0c;给大家分享一个工作中常用到的插件——模态对话框的用法。想必大家在工作中也遇到很多页面&#xff0c;需要用模态对话框进行交互的吧&#xff0c;现在呢&#xff0c;就让我们一起来了解一下它的使用吧。 首先&#xff0c;我…

Dlib检测人脸特征点标号图

dlib进行人脸检测时候&#xff0c;所有人脸的标号图&#xff0c;c检索的时候注意从0索引开始

Infuse通过Alist添加115网盘资源

说明 通过Alist代理管理115网盘&#xff0c;Infuse再添加Alist代理的115网盘的WebDAV 准备一台Linux服务器安装Alist 我这里用的华为云CentOS7&#xff0c;使用Docker容器 安装Alist docker run -d --restartalways -v /etc/alist:/opt/alist/data -p 5244:5244 -e PUID0 …