Redis的分布式锁问题(十)最强分布式锁工具:Redisson

news2024/11/24 16:41:45

Redisson的引入

我们先来看看之前的基于setnx实现的分布式锁存在的问题:

我们之前实现的分布式锁是基于redis的setnx命令的特性的! 

但是,这样子实现起来会有很多弊端!

不可重入

简单的来说就是一旦setnx [key] [value]后,就不能再对这个key做任何操作了(除了删除)

 假设我们在开发中有A和B两个业务,在业务A中,执行了setnx操作,然后在业务A中调用业务B。

然后在业务B中也有setnx的操作(同一个KEY

此时,业务B就会阻塞在这里,等待业务A释放锁

但是,业务A肯定不会释放锁,因为业务A还没有执行完(调B)。故就会发生死锁。 

不可重试 

在我们之前业务逻辑中,尝试获取锁,如果获取不到就直接return了,没有“重来”的机会!也无法提供重试的机制!

超时释放

我们之前,分析过分布式锁被误删的问题。这个问题是已经解决了。

但是,仍然会存在隐患!我们这里是用TTL来控制它。业务执行,时间多少,这是一个未知数,TTL要怎么设置?如何处理业务阻塞?

主从一致性

主节点上获取到了锁,但是主节点突然宕机了,就会从从结点中选出一个节点,作为主节点。

但由于,因为之前的那个主节点宕机了。在新选举出来的这个主节点中是无法获取到之前的锁

所以之前的那个锁相当于失效了!

Redisson 

要解决上述问题并不是那么容易的,如果我们自己实现很有可能会出一些问题!所以最好的办法就是使用市面上的一些框架来解决!

什么是Redisson? 

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redisson使用手册 

0. 项目介绍 - 《Redisson 使用手册》 - 书栈网 · BookStackhttps://www.bookstack.cn/read/redisson-wiki-zh/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D.md里面提到了Redisson可以实现大致如下的分布式锁

Redisson快速入门(Demo) 

(1)导依赖 

<!-- redis-redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

(2)配置Redisson客户端

/**
 * 配置 Redisson
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {

        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.89.128:6379").setPassword("888888");

        // 创建 RedissonClient 对象
        return Redisson.create(config);
    }
}

(3)使用Redisson的分布式锁 

@Test
void testRedisson() throws Exception {
    RLock anyLock = redissonClient.getLock("anyLock");
    boolean isLock = anyLock.tryLock(1, 10, TimeUnit.SECONDS);
    if(isLock) {
        try {
            System.out.println("执行业务");
        } finally {
            anyLock.unlock();
        }
    }
}

测试结果

Redisson实现可重入锁

这里可重入锁的实现 和 Java的 ReentrantLock 类似!

获取锁的时候,先判断是不是同一个对象,是就将 value+1,释放锁的时候就 value-1,当其小于0时就将该key删除!

在Redis中使用 Hash结构 去存储!

Redisson 关于获取锁、释放锁的源码分析 

获取锁 

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }
    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    // 在Lua脚本中起始位是1
    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return nil; " +
                    "end; " +
                    "if (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(getName()), internalLockLeaseTime, getLockName(threadId));
}

上述代码中字符串部分就是Lua脚本,Redisson用其实现可重入锁!  

Redisson 获取锁中的Lua脚本源码解析

-- 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 不存在,获取锁
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    -- 设置有效期
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 锁已经存在,判断是否是自己?!
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 自增+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    -- 重置有效期
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);

释放锁 

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "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]); " +
                    "return 0; " +
                    "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return nil;",
            Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

Redisson 释放锁中的Lua脚本源码解析

-- 判断当前锁是否还是被自己持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    -- 不是就就直接返回
    return nil;
    end;
-- 是自己,则重入次数 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 判断重入次数是否已经为0
if (counter > 0) then
    -- 大于0,说明不能释放,重置有效期即可
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    -- 等于0,说明可以直接删除
    redis.call('del', KEYS[1]);
    -- 发消息
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
    end;
return nil;

测试代码

我们这边模拟一下锁重入的场景。方法A上锁后调方法B,方法B也获取锁(如果是不可重入,这里就会阻塞!)

/**
 * Redisson的单元测试
 */
@SpringBootTest
@Slf4j
public class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() {
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 ... 1");
            return;
        }
        try {
            log.info("获取锁成功 ... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
            log.warn("准备释放锁 ... 1");
            lock.unlock();
        }
    }

    @Test
    void method2() {
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 ... 2");
            return;
        }
        try {
            log.info("获取锁成功 ... 2");
            log.info("开始执行业务 ... 2");
        } finally {
            log.warn("准备释放锁 ... 2");
            lock.unlock();
        }
    }
}

运行结果 

Redis 中值的情况 

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

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

相关文章

这才是图扑数字孪生污水处理厂该有的样子

近年来&#xff0c;智慧水务、数字水务成为水务行业的热点领域。对于污水处理领域&#xff0c;如何贯彻落实双碳战略&#xff0c;积极推进智慧水厂建设&#xff0c;显得尤为关键。 图扑软件依托自主研发的 HT for Web 产品&#xff0c;并结合视频融合、BIM、5G、物联网、云计算…

matplotlib 中子图subplot 绘图时标题重叠解决办法

引言 使用Python的matplotlib库绘制子图发现标题发生了重叠。 原来的代码&#xff1a; plt.rcParams[font.family][SimHei] datayear_genfor i in range(1,11):plt.subplot(5,2,i)typetype_df.index[:][i-1]setplot_TypeTime(i,data,type)plt.show()上网上寻找解决办法。 按照…

Allegro添加渐近线操作指导

Allegro添加渐近线操作指导 Allegro支持添加渐近线,让线宽变化的地方进行圆环的过渡,对于射频信号优化有很大帮助,类似下图 具体操作如下 首先设置参数,route-Gloss-Parameters 点击Fillet and Taper Trace前面的方框 勾选Allowed DRC, Unused Nets 勾选Tapered Trac…

BLUElegend传奇引擎不使用路由器架设单传奇的办法

使用BLUE LEGEND架设传奇私发服单机的朋友&#xff0c;是不是因为找不到路由器而无法架设单机服务端呢&#xff0c;这里介绍一种方法不需要买路由器来架设。 为什么LEG引擎需要路由器才能架设呢&#xff1f; 网上找了很多教程都得不到答案&#xff0c;有些人说是为了固定ip地址…

[附源码]计算机毕业设计SpringBoot网上鲜花购物系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

leetcode-每日一题-1779-找到最近的有相同 X 或 Y 坐标的点(简单,数学思想)

今天这道每日一题很简单&#xff0c;没啥可说的&#xff0c;细心点即可 1779. 找到最近的有相同 X 或 Y 坐标的点 难度简单73收藏分享切换为英文接收动态反馈 给你两个整数 x 和 y &#xff0c;表示你在一个笛卡尔坐标系下的 (x, y) 处。同时&#xff0c;在同一个坐标系下给你一…

基于verdaccio工具搭建npm私服vue组件库

大纲 搭建npm私服的必要性搭建npm私服操作步骤发布私有包的过程 一、搭建npm私服的必要性 下载速度更快便于管理&#xff0c;可以分配权限可以修改第三方包&#xff0c;放入我们得私服可以只在公司局域网中用&#xff0c;不公开 二、搭建npm私服的主要操作 环境准备 确保服…

Google单元测试框架gtest之官方sample笔记4--事件监控之内存泄漏测试

sample 10 使用event listener监控Water类的创建和销毁。在Water类中&#xff0c;有一个静态变量allocated&#xff0c;创建一次值加一&#xff0c;销毁一次值减一。为了实现这个功能&#xff0c;重载了new和delete关键字&#xff0c;然后在new和delete函数中&#xff0c;做all…

Sqoop概述 第1关:Sqoop概述

为了完成本关任务&#xff0c;你需要掌握&#xff1a; 1.Sqoop 概述&#xff1b; 2.Sqoop 基本架构。 Sqoop 概述 设计动机 Sqoop 从工程角度&#xff0c;解决了关系型数据库与 Hadoop 之间的数据传输问题&#xff0c;它构建了两者之间的“桥梁”&#xff0c;使得数据迁移工…

【Linux】ls命令

ls&#xff1a;List Directory Contents&#xff0c;显示目录下内容。 .表示当前目录 …表示上一级目录 .开头文件为隐藏文件 说明&#xff1a; 查看文件大小 ls -asSh ls -al ls -alh fan

门面/外观模式

一、门面模式 1、定义 门面模式&#xff08;Facade Pattern&#xff09;又称作外观模式&#xff0c;是指提供一个统一的接口&#xff0c;用来访问子系统中的一群接口&#xff0c;属于结构型设计模式。 门面模式的主要特征是定义了一个高层接口&#xff0c;让子系统更容易使用。…

PHP基于thinkphp的网上图书管理系统#毕业设计

本论文主要论述了如何使用php语言开发一个网上图书管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;将论述网上图书管理系统的当前背景以及系统开发的目的&#xff0c;后续章节将严格按照软…

基于PHP+MySQL协同办公系统的设计与实现

随着全球经济一体化进程的加快和信息技术的飞速发展&#xff0c;Internet技术及其应用给人们的日常生活和工作等各个方面带来了深刻的影响。网络应用技术的不断提高&#xff0c;企业对于相互之间的通讯能力提出了更高的要求。许多企业都具有一定信息化基础&#xff0c;有一定数…

HTML文本溢出处理

有时在做某些需求布局时,需要处理文本溢出时的情况,如果不处理可能会重新重叠的效果,关于设置文本溢出,主要就是多行/单行的处理,代码如下 1.处理单行文本溢出 /* 设置文本溢出时的处理模式 */text-overflow:ellipsis;overflow: hidden;white-space: nowrap; 2.处理多行文本溢出…

视频播放 (三) 视频列表

1. 配置信息 1.1 AndroidManifest.xml 添加网络权限 <uses-permission android:name"android.permission.INTERNET" /> 1.2 使用 Http 明文设置 android:usesCleartextTraffic"true" 1.3 使用竖屏设置 android:screenOrientation"portrait&q…

FCP第二题:数据库中有一张地区数据统计表,但是并不规则

【题目要求】 数据库中有一张地区数据统计表,但是并不规则 ,记录类似于,225100:02:3:20160725是一串代码,以:分割,第1位为地区代码,第2位为分类代码,第3位为数量,第4位为日期 地区代码含义225100-上海 225200-江苏 225300-浙江 为可能有某些位不存在,缺位时计算规…

很多up主都在使用的Editplus,强大的编辑器-并附有编译执行配置 java编译(新款发放)

趣味拓展 邻居老李家的屋顶为什么有时漏雨&#xff0c;有时不漏雨&#xff1f; (答案在文末) 引言 不晓得你们怎么认识Editplus的&#xff0c;小编最初认识Editplus是在老杜的javaSE视频中 杜老师SE视频中也分享的有Editplus&#xff0c;不过这款是新版本~ 软件介绍 1.EditPl…

【密码加密原则】

目录 1. 什么是密码加密 2. 典型的消息摘要算法 1. 什么是密码加密 用户在使用软件时所提交的密码&#xff0c;不应该被记录下来&#xff0c;如果将用户的密码记录&#xff0c;这是不安全的做法&#xff01; 当用户提交注册信息时&#xff0c;密码必须被记录下来&#xff0…

MYSQL中AS(取别名)

文章目录0 写在前面1 格式2 举例2.1 设置表别名2.2 设置字段别名3 写在末尾0 写在前面 在做业务&#xff0c;在mybatis中手写sql中再多表查询去映射实体时&#xff0c;总会用到AS这个关键字。 或者我们在数据库大量字段测试数据时&#xff0c;很多字段都有相同的前缀&#xff…