可重入分布式锁有哪些应用场景

news2024/11/28 8:30:30

原文连接:可重入分布式锁有哪些应用场景 https://mp.weixin.qq.com/s/MTPS9V8jn5J91wr-UD4DyA

之前发过的一篇实现Redis分布式锁的8大坑中,有粉丝留言说,分布式锁的可重入特性在工作中有哪些应用场景,那么我们这篇文章就来看一下分布式锁的可重入特性。

实现Redis分布式锁的8大坑

一、可重入场景有哪些?

场景一:创建订单之后,处理其他的逻辑异常了,需要回滚取消订单,此时取消订单的逻辑中需要获取到当前订单的分布式锁,此时也是需要可重入的特性的。

场景二:商城的支付,当第一次对订单进行支付时获取订单的分布式锁,如果此时你退出了,在用另一个客户端对同一个订单进行支付是否还可以呢?如果因为网络异常或者其他原因,当前发起订单的客户端还是可以再次进入支付流程进行支付。

场景三:分布式系统的缓存,缓存在客户端1更新过程中,客户端1发生异常无法继续执行,在客户端1获取的分布式锁还没有过期的这段时间,其他的客户端是无法获取到分布式锁的。假如客户端1在锁过期之前恢复了,再次执行该逻辑时可以继续重入该分布式锁继续执行操作。

场景四:在主线程完成任务的情况下,异步处理另一个任务,此时可以先释放锁,异步任务完成之后再次获取锁。

除了上述描述的四种场景外,只要是涉及到分布式锁的,都是有可能会有可重入的特性了。对于可重入的理解是,在维基百科中是这样描述的。

若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另一段代码,这段代码又使用了该副程序不会出错”,则称其为可重入(reentrant 或 re-entrant)的。即当该副程序正在运作时,执行线程可以再次进入并执行它,仍然可得到符合设计时所预期的结果。与多线程并发执行的线程安全不同,可重入强调对单一线程执行时重新进入同一个子程序仍然是安全的。

可重入概念是在单线程操作系统的时代提出的。一个子程序的重入,可能由于自身原因,如执行了jmp或者call,类似于子程序的递归调用;或者由于作业系统的中断回应。UNIX系统的signal的处理,即子程序被中断处理程序或者signal处理程序调用。所以,可重入也可称作“异步信号安全”。这里的异步是指信号中断可发生在任意时刻。 重入的子程序,按照后进先出线性序依次执行。

所以对于现在的可重入,大部分的场景就是系统异常之后再次执行或者递归调用。

二、Java中有哪些可重入的锁

在Java中,SynchronizedReentrantLock 都是可重入的锁。

1、Synchronized :应用于方法或者代码块。当一个线程持有某个对象的锁时,它可以重复的进入任何其他由该对象保护的Synchronized 方法或者代码块。

package com.zuiyu.client1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SyncDemo {
    public static final Logger log = LoggerFactory.getLogger(SyncDemo.class);

    private int count = 0;

    public synchronized void increment() {
        count++;
        log.info("increment count {}",count);
        decrement(); // 调用自身的另一个 synchronized 方法
    }

    public synchronized void decrement() {
        count--;
        log.info("decrement count {}",count);
    }

    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();
        syncDemo.increment();
    }
}

执行结果如下:

2、ReentrantLock:提供了 lock()unlock() 方法控制锁的获取和释放。与 synchronized 不同的是,ReentrantLock 允许在同一个线程中多次调用 lock() 方法而不被阻塞,只要每次调用 lock() 都有相应的 unlock() 来释放锁就可以。

package com.zuiyu.client1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;


public class ReentrantLockDemo {
    public static final Logger log = LoggerFactory.getLogger(ReentrantLockDemo.class);

    //锁
    private static ReentrantLock lock =  new ReentrantLock();
    public void doSomething(int n){
        //进入递归第一件事:加锁
        try{
            lock.lock();
            log.info("--------lock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
            log.info("--------递归{}次--------",n);
            if(n<=2){
                this.doSomething(++n);
            }else{
                return;
            }
        }finally {
            lock.unlock();
            log.info("--------unlock()执行后,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
        }
    }

    public ReentrantLock getLock(){
        return lock;
    }
    public static void main(String[] args) {
        ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
        reentrantLockDemo.doSomething(1);
        log.info("执行完doSomething方法 是否还持有锁:{}",lock.isLocked());
    }

}

执行结果如下:

三、ReentrantLock 如何实现的可重入

我们通过代码 debug 可以找到 ReentrantLock 代码中的 nonfairTryAcquire 方法。

    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //先判断,c(state)是否等于0,如果等于0,说明没有线程持有锁
            if (c == 0) {
                //通过cas方法把state的值0替换成1,替换成功说明加锁成功
                if (compareAndReentrantLock代码中的SetState(0, acquires)) {
                    //如果加锁成功,设置持有锁的线程是当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//判断当前持有锁的线程是否是当前线程
                //如果是当前线程,则state值加acquires,代表了当前线程加锁了多少次
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

所以 ReentrantLock加锁流程就是:

1、先判断是否有线程持有锁,没有就进行加锁。

2、如果加锁成功,则设置持有锁的线程为当前线程。

3、如果有线程已经持有了锁,则在判断是否是当前线程持有的锁。

4、如果是当前线程持有的锁,则加锁数量+1

5、如果不是当前当前线程持有的锁,返回false,加锁失败。

释放锁的流程如下:

/**
         * 释放锁
         * @param releases
         * @return
         */
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//state-1 减加锁次数
            //如果持有锁的线程,不是当前线程,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();

            boolean free = false;
            if (c == 0) {//如果c==0了说明当前线程,已经要释放锁了
                free = true;
                setExclusiveOwnerThread(null);//设置当前持有锁的线程为null
            }
            setState(c);//设置c的值
            return free;
        }

1、每次释放锁对计数进行减1

2、当c0的时候,说明锁重入的次数为0 了。

3、最终设置当前持有锁的线程为 NULLstate 设置为0,锁也就释放了。

四、Redisson 实现分布式锁

通过上面 ReentrantLock 的加锁释放锁学习,我们已经知道了锁的可重入的原理了,所以使用 Redis 实现分布式锁我们只需要实现如下两点即可。

1、如何保存当前的线程。

2、加锁次数的保存维护。

所以结合上一篇文章中说过的 Redisson 的可重入特性,也就知道如何使用 Redis 来实现一个分布式锁了。

文章地址在这,可以点进去看看,我下面也把关键地方截图放过来。

Redis 实现分布式锁的8大坑

https://mp.weixin.qq.com/s/j69OLgLIo6R2VI80alJF0Q

那么这里在对这些代码在进行一个说明,在对代码说明之前还是先来个demo。

@Service
public class RedissonLockDemo {

    public final Logger log = LoggerFactory.getLogger(getClass());
    private RedissonClient redissonClient;

    String rKey = "lock1";

    public RedissonLockDemo(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public void lock(){
        RLock lock1 = redissonClient.getLock(rKey);
        lock1.lock();
        log.info("thread {} method lock,lock1:{}={}",Thread.currentThread().getName(),lock1.getName(),lock1.getHoldCount());
        lock2();
        lock1.unlock();
    }
    public void lock2(){
        RLock lock2 = redissonClient.getLock(rKey);
        lock2.lock();
        log.info("thread {} method lock,lock2:{}={}",Thread.currentThread().getName(),lock2.getName(),lock2.getHoldCount());

        lock2.unlock();
    }


}

执行结果如下:

通过 debug 代码中lock.lock() 可以看到,发现它最终调用的是
RedissonLock#tryLockInnerAsync


    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.evalWriteSyncedAsync(this.getRawName(), LongCodec.INSTANCE, command, 
        "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]);",
        Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }

加锁流程如下:

1、 判断key是否存在,返回0代表key不存在,代表没有加锁。

2、或者判断field是否在hash中,返回1代表当前线程加进程的ID已经获取到锁了。

3、hincrbykey中的 ARGV[2]1

4、对整个key设置过期时间。

为了校验执行的命令下面截图是 RedissonBaseLock#evalWriteSyncedAsync 。具体如下:

在这个脚本中,用到的命令我们来说一下

  • exists:校验 key 是否存在。

  • hexists:校验 field 是否存在 hash 中。

  • hincrby:将hash中指定的值增加给定的数字。

  • pexpire:设置key的有效期,以毫秒为单位。

  • pttl: 判断key的有效毫秒数。

解锁的代码在 RedissonLock#unlockInnerAsync


  protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
        return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                              "local val = redis.call('get', KEYS[3]); " +
                                    "if val ~= false then " +
                                        "return tonumber(val);" +
                                    "end; " +

                                    "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]); " +
                                        "redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +
                                        "return 0; " +
                                    "else " +
                                        "redis.call('del', KEYS[1]); " +
                                        "redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
                                        "redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +
                                        "return 1; " +
                                    "end; ",
                                Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),
                                LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,
                                getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
    }

解锁的流程如下:

1、if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) 判断锁是否存在。

2、redis.call('hincrby', KEYS[1], ARGV[3], -1) 加锁次数原子自减。

3、if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); 自减后当前线程还持有锁(counter > 0),更新下锁的过期时间。

4、counter < 0else 逻辑解锁完成,删除该锁。

加锁解锁流程相对于上一篇文章中所述有所变化,本文 Redisson 版本为 3.29.0

五、总结

对于工作中用到分布式锁的场景,都要考虑是否可以重入,防止死锁的发生。

锁的可重入,两点需要我们注意,一个是保存当前持有锁的线程,另一个就是锁的加锁次数。

好了本文到这就结束了,如果读完感觉有所收获,欢迎三连。

大家都要一起进步。

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

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

相关文章

pythonsql-随机问答小程序

随机问答-python&sql 智力问答测试&#xff0c;在答题过程中对做对、做错进行实时跟踪&#xff0c;测试完成后能根据玩家的答题情况给出成绩。 1. 设计思路 程序使用了一个SQLite试题库test.db&#xff0c;其中每个智力问答山题目、4个选项*1-1正确答案组成(question, An…

Spring boot环境的常见问题

文章目录 一、启动类无法运行二、包相关问题2.1 默认配置的包无法下载2.2 第三方库的包无法下载2.3 包找不到 三、出现了一个无效的源发行版17四、类文件具有错误的版本 61.0&#xff0c;应为52.0五、控制台乱码 一、启动类无法运行 原因&#xff1a;IDEA 没有把当前项目识别成…

macos安装mysql一直卡在安装成功那个页面选项的解决办法

问题描述&#xff1a; 我安装的是比较新的版本8.0.37&#xff0c;安装过程中一直卡在安装那个选项上&#xff0c;且页面提示安装成功了&#xff0c;但就是死活不往下面的配置选项那一步走。 解决办法&#xff1a; 1.首先清理掉之前的mysql sudo rm -rf /usr/local/mysql2.然…

MATLAB模拟退火算法、遗传算法、蚁群算法、粒子群算法

概况 模拟退火算法、遗传算法、蚁群算法、粒子群算法等算法&#xff0c;都是属于概率算法&#xff0c;不绝对&#xff0c;不迅速&#xff0c;能用其它方式解决的问题&#xff0c;不要用这些相对复杂的算法&#xff0c;比如有明确的线性关系或者非线性对应关系。这里的概率算法…

二氧化碳在饮料汽水中的作用与西奥机电CLRT-01二氧化碳气容量测试仪的重要性

二氧化碳在饮料汽水中的作用与西奥机电CLRT-01二氧化碳气容量测试仪的重要性 一、引言 当我们打开一瓶冰镇的汽水&#xff0c;那独特的口感和清凉感总是让人心旷神怡。而这一切&#xff0c;都离不开其中溶解的二氧化碳。本文将详细解析二氧化碳在饮料汽水中的作用&#xff0c…

Jenkins docker 自动化部署python3后端 centos8.5 运维系列四

1安装expect yum install expect 2 jenkins 新建任务 #cat qysup.sh #!/usr/bin/expect -f set port 22 set user root set host ip set password 密码 set timeout 60 spawn ssh $user$host expect "password:" send "$password\r" expect "]#&qu…

Golang | Leetcode Golang题解之第73题矩阵置零

题目&#xff1a; 题解&#xff1a; func setZeroes(matrix [][]int) {n, m : len(matrix), len(matrix[0])col0 : falsefor _, r : range matrix {if r[0] 0 {col0 true}for j : 1; j < m; j {if r[j] 0 {r[0] 0matrix[0][j] 0}}}for i : n - 1; i > 0; i-- {for …

面试笔记——JVM组成

基本介绍 JVM: Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 使用JVM的好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收机制 JVM的组成及运行流程&#xff1a; 程序计数器 程序计数器&a…

用Rust打印杨辉三角

一、杨辉三角是什么&#xff1f; 杨辉三角是一个著名的数学图形&#xff0c;它展示了二项式系数的排列方式。 杨辉三角是一种将二项式系数以三角形阵列排列的数学图形&#xff0c;具有丰富的历史和数学意义。 杨辉三角的历史起源可以追溯到中国南宋时期&#xff0c;由数学家杨辉…

同创优配正规炒股A股三大指数集体收涨 创指重回1900点关口

查查配5月9日电 周四,A股三大指数震荡上扬。截至收盘,上证指数涨0.83%,报3154.32点;深证成指涨1.55%,报9788.07点;创业板指涨1.87%,报1900.01点。总体上个股涨多跌少,全市场超4200只个股上涨。沪深两市今日成交额9011亿元,较上个交易日放量367亿元。 同创优配是AAA 级诚信经营…

内存卡不小心格式化了怎么办?3个方法解决数据丢失问题!

“很奇怪&#xff0c;我的内存卡不小心中病毒了&#xff0c;刚刚在清理病毒时不小心把内存卡格式化了。我保存了很多重要的数据在里面&#xff0c;还有方法可以恢复这些数据吗&#xff1f;” 在数字设备日益普及的今天&#xff0c;内存卡已成为我们存储和传输数据的重要工具。但…

竖排文字识别原理与实践操作方法

在当今数字化时代&#xff0c;OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;技术已经广泛应用于各个领域&#xff0c;特别是在文档处理方面&#xff0c;OCR软件能够帮助用户快速将纸质文档转化为可编辑的电子文档。然而&#xff0c;对于竖…

OpenSearch 与 Elasticsearch:7 个主要差异及如何选择

OpenSearch 与 Elasticsearch&#xff1a;7 个主要差异及如何选择 1. 什么是 Elasticsearch&#xff1f; Elasticsearch 是一个基于 Apache Lucene 构建的开源、RESTful、分布式搜索和分析引擎。它旨在处理大量数据&#xff0c;使其成为日志和事件数据管理的流行选择。 Elasti…

顺序表的实现(迈入数据结构的大门)(2)

目录 顺序表的头插(SLPushFront) 此时&#xff1a;我们有两个思路&#xff08;数组移位&#xff09; 顺序表的头删(学会思维的变换)(SLPopFront) 顺序表的尾插(SLPushBack) 有尾插就有尾删 既然头与尾部的插入与删除都有&#xff0c;那必然少不了指定位置的插入删除 查找…

Python嵌套绘图并为条形图添加自定义标注

论文绘图时经常需要多图嵌套&#xff0c;正好最近绘图用到了&#xff0c;记录一下使用Python实现多图嵌套的过程。 首先&#xff0c;实现 Seaborn 分别绘制折线图和柱状图。 绘制折线图import seaborn as snsimport matplotlib.pyplot as pltimport warningswarnings.filterw…

【日常开发之插件篇】IDEA plugins 神器助我!!

文章目录 Tabnine 代码自动补全图例 Rainbow Brackets图例 Translation 翻译配置 LombokAlibaba Java Coding Guidelines 阿里巴巴的编码规约检查插件 今早因为老代码的一些bug让我突然觉得Idea的一些插件特别好用&#xff0c;我准备将我平时所用到的一些插件做个推荐以及记录。…

全国智能网联汽车行业产教融合共同体成立, 经纬恒润当选常务理事单位

近日&#xff0c;全国智能网联汽车行业产教融合共同体&#xff08;以下简称“共同体”&#xff09;全体成员大会在湖南株洲隆重召开。湖南省教育厅职成处副处长殷劭&#xff0c;国家智能网联汽车创新中心常务副主任、国汽&#xff08;北京&#xff09;智能网联汽车研究院有限公…

可以录屏的软件推荐3款,让你的录制更高效!

在信息时代&#xff0c;数字化学习、远程办公等场景越来越普及&#xff0c;录屏软件已经成为了人们生活中不可或缺的一部分。一款好的录屏软件可以帮助用户轻松录制屏幕内容&#xff0c;以便随时回顾或与他人分享。本文将详细介绍三款可以录屏的软件&#xff0c;帮助读者了解它…

百度百科怎么创建自己

百度百科是一个向所有互联网用户开放的平台&#xff0c;人人都可以创建词条。以下是创建自己百度百科的步骤和注意事项。 创建步骤 注册百度账号&#xff1a;首先&#xff0c;你需要注册一个百度账号。如果已经有百度账号&#xff0c;可以直接登录。 选择百科类型&#xff1a;…

FPGA+海思ARM方案,可同时接收HDMI/VGA 两种信号,远程控制

FPGA海思ARM方案&#xff0c;可同时接收HDMI/VGA 两种信号&#xff0c;通过配置输出任一图像或者拼接后的图像 客户应用&#xff1a;无线远程控制 主要特性&#xff1a; 1.支持2K以下任意分辨率格式 2.支持H264压缩图像 3.支持WIFI/4G无线传输 4.支持自适应输入图像分辨率 …