Redis 篇-深入了解基于 Redis 实现分布式锁(解决多线程安全问题、锁误删问题和确保锁的原子性问题)

news2024/11/10 5:49:30

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 分布式锁概述

        1.1 Redis 分布式锁实现思路

        1.2 实现基本的分布式锁

        2.0 Redis 分布式锁误删问题

        2.1 解决 Redis 分布式锁误删问题

        3.0 Redis 分布式锁原子性问题

        3.1 Lua 脚本解决多条命令原子性问题

        4.0 基本 Redis 实现的分布式锁代码


        1.0 分布式锁概述

        分布式锁是一种用于在分布式系统中控制对共享资源的访问的机制。它确保在同一时间只有一个进程或线程能够访问特定的资源,从而避免数据冲突和不一致性。

        当项目部署到集群中,如果只用 sychronized 锁是不足以在集群环境中确保线程安全,简单的说一下原因:在集群中,有多个 JVM ,就会有多个字符串常量池,锁的作用域仅限于当前 JVM 的对象或类。当多个 JVM 访问同一个资源时,每个 JVM 都会有自己的锁,导致无法实现对共享资源的有效控制。所以出现锁不住资源的情况。

        因此需要用分布式锁来完成。

常见的实现方式:

        1)数据库锁:利用数据库的事务机制来实现锁定。

        2)Redis 锁:使用 Redis 的 setnx 命令来实现分布式锁。

        3)Zookeeper 锁:利用 Zookeeper 的临时节点和顺序节点来实现分布式锁。

        1.1 Redis 分布式锁实现思路

        使用 Redis 的 SETNX 命令来实现分布式锁。

        首先,先介绍 setnx 的特性,一旦使用 setnx 设置某一个字段时,当设置成功之后再使用 setnx 设置重复字段,则会出现失败情况。Redis 分布式锁就是利用该特性来实现锁。当然,这只是大概的情况,还有很多细节需要注意。

        1)尝试获取锁的思路:

        先使用 setnx 设置某一个字段,如果返回值为成功,则获取锁成功;如果返回值为失败,则获取锁失败,那么获取锁失败可以根据具体业务情况来安排,比如可以先等待一段时间,接着再去尝试获取锁、还可以直接抛出异常等。

        还要考虑一种情况,当出现锁忘记释放了,则该字段就会一直存在缓存中,随着时间积累,缓存空间就会慢慢的减少,因此,给该字段设置 TTL ,超时时间。

        2)释放锁的思路:

        一般来说,直接用 del 命令,删除某一个字段即可。

        以上获取锁和释放锁都是最基础的形态,还有很多情况需要考虑,因此还不能在实战中使用。

        1.2 实现基本的分布式锁

尝试获取锁:

import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock{

    private final StringRedisTemplate stringRedisTemplate;
    private final String name;
    private static final String KEY_PREFIX = "lock:";

    public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    /**
     * 尝试获取锁
     * @param time
     * @param unit
     * @return
     */
    public boolean tryLock(long time,TimeUnit unit){

        long threadId = Thread.currentThread().getId();
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", time, unit);
        return BooleanUtil.isTrue(b);
    }

    
}

        在创建 RedisLock 对象的时候,需要转递 StringRedisTemplate 类型对象,还有业务名称 name 作为锁绑定的具体对象,且在设置 setnx 的时候,value 设置为当前线程 id ,有助于查看当前锁被那一个线程获取了。最后需要注意,不可直接将类型 Boolean 类型的对象直接返回,因为由 Boolea 会自动拆箱 boolean 基本类型对象,在拆箱过程中容易出现空指针异常。

释放锁:

import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock{

    private final StringRedisTemplate stringRedisTemplate;
    private final String name;
    private static final String KEY_PREFIX = "lock:";

    public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    /**
     * 尝试获取锁
     * @param time
     * @param unit
     * @return
     */
    public boolean tryLock(long time,TimeUnit unit){

        long threadId = Thread.currentThread().getId();
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", time, unit);
        return BooleanUtil.isTrue(b);
    }


    /**
     * 释放锁
     */
    public void unLock(){
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }

}

        2.0 Redis 分布式锁误删问题

        在获取锁之后,正常执行完逻辑任务,再释放锁。这一过程按理来说,不会出现分布式锁被误删的情况,但是再考虑到一下情况:

        假设线程一正常获取锁之后,执行任务,但是该任务出现了阻塞情况,等待的时间较久,此时当锁到过期时间之后,就会自动被释放了,当时此时线程一还不知道当前锁被释放了,就在这时候,线程二来正常的获取锁,因为锁已经被释放了,所以线程二是可以获取锁成功的,接着,线程二获取锁之后,就开始执行任务了,此刻线程一任务执行完之后,会直接释放锁,这就出现线程一误删了线程二的锁问题。

如图:

        出现误删问题,就有可能出现多个线程获取锁的情况发生,从而出现线程安全问题,所以需要解决该问题。

        2.1 解决 Redis 分布式锁误删问题

        为了解决 Redis 分布式锁被误删的问题,可以想到的办法是:在释放锁之前,判断当前的锁 “是否” 是自己之前获取的锁,如果是,则可以直接释放锁;如果不是,则什么都不用做。

        具体如何判断当前锁 “是否” 是自己之前获取的锁呢?

        之前我们在设置 setnx 的时候,将 value 设置为线程 id ,那么就可以在释放锁的时候通过判断当前线程 id 与获取锁的时候设置 value 的线程 id 值两者是否一致。

        但是如果在集群环境中只判断线程 id 是否相同还不足以确保不会出现误删的情况发生,因为在集群环境中,有多个 JVM ,则非常有可能出现线程 id 相同的情况,所以还需要加上 UUID 来设置前缀,确保每一个 JVM 的前缀都是不一样的,结合起来就可以解决该情况了。

代码如下:

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock{

    private final StringRedisTemplate stringRedisTemplate;
    private final String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String VAL_PREFIX = UUID.randomUUID().toString(true) + "-";

    public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    /**
     * 尝试获取锁
     * @param time
     * @param unit
     * @return
     */
    public boolean tryLock(long time,TimeUnit unit){

        String value = VAL_PREFIX + Thread.currentThread().getId();
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, time, unit);
        return BooleanUtil.isTrue(b);
    }


    /**
     * 释放锁
     */
    public void unLock(){
        //先获取value值
        String newValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        String oldValue = VAL_PREFIX+Thread.currentThread().getId();
        //再判断两者是否相同
        if (oldValue.equals(newValue)){

            //相同情况,直接删除即可
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
        //不相同情况,什么都不做
    }

}

        3.0 Redis 分布式锁原子性问题

        由于在释放锁之前加上了,判断当前锁 "是否" 是自己的代码,从而有可能出现了原子性问题,当判断完之后,出现线程阻塞,导致释放锁时机延长,直到超过了过期时间,则锁就会被自动释放,当线程阻塞完毕之后,再来释放锁,此时有可能出现误删锁。

如图:

        因此需要保证判断锁和释放锁具有原子性,要么一起执行,要么都不执行。

        3.1 Lua 脚本解决多条命令原子性问题

        Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性。 基本语法可以参数网站:Lua 教程 | 菜鸟教程 (runoob.com)

        使用 Lua 脚本语言编写 Redis 多条命令,先根据 key 来查询 value ,再判断 value 与当前线程标识是否相同,如果相同,则进行删除缓存;如果不相同,则什么都不需要做。

Lua 脚本如下:

-- 这里的 KEYS[1] 就是锁的Key,这里的ARGV[1]就是当前线程标识
-- 获取锁中的标识,判断是否与当前线程标识一致
if(redis.call('GET',KEYS[1]) == ARGV[1]) then
    -- 一致,则删除锁
    return redis.call('DEL',KEYS[1])
end
-- 不一致,则直接返回
return 0

        在 Java 中使用 StringRedisTemplate 对象来调用 execute 方法从而调用 Lua 脚本。

        需要传的参数:

        1)DefaultRedisScript 类型对象,该对象主要用来将读取 Lua 脚本。

        2)List<K> keys 数组对象,主要是传入 key 的实参,因为在 Lua 脚本中设置是形参,因此根据实际情况来传入实参。

        3)Object... args 任意对象,根据实际情况来传入除了 KEY 以外的实参。

        Lua 中的形参 KEYS[1] 对应的实参为 List<K> keys 数组对象,而形参 ARGV[1] 对应的实参为 Object... args 任意对象。

        在 Lua 中使用 redis.call() 方法,可以理解成调用该方法来实现对 Redis 操作。 

具体代码实现:

    private static final DefaultRedisScript<Long> defaultRedisScript;

    static {
        defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setLocation(new ClassPathResource("unlock.lua"));
        defaultRedisScript.setResultType(Long.class);
    }

    /**
     * 释放锁
     */
    public void unLock(){
        stringRedisTemplate.execute(defaultRedisScript,
                Collections.singletonList(KEY_PREFIX + name),
                VAL_PREFIX + Thread.currentThread().getId());
    }

        最后,使用 Lua 脚本实现对 Redis 多条命令的操作,再由 Java 读取操作 Lua 脚本语言,从而实现解决原子性问题。

        4.0 基本 Redis 实现的分布式锁代码

        解决了在集群环境下,确保线程安全问题,且解决了误删锁问题和解决原子性问题。

代码如下:

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class RedisLock{

    private final StringRedisTemplate stringRedisTemplate;
    private final String name;
    private static final String KEY_PREFIX = "lock:";
    private static final String VAL_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> defaultRedisScript;
    static {
        defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setLocation(new ClassPathResource("unlock.lua"));
        defaultRedisScript.setResultType(Long.class);
    }

    public RedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    /**
     * 尝试获取锁
     * @param time
     * @param unit
     * @return
     */
    public boolean tryLock(long time,TimeUnit unit){

        String value = VAL_PREFIX + Thread.currentThread().getId();
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value, time, unit);
        return BooleanUtil.isTrue(b);
    }


    /**
     * 释放锁
     */
    public void unLock(){
        stringRedisTemplate.execute(defaultRedisScript,
                Collections.singletonList(KEY_PREFIX + name),
                VAL_PREFIX + Thread.currentThread().getId());
    }

/*    public void unLock(){
        //先获取value值
        String newValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        String oldValue = VAL_PREFIX+Thread.currentThread().getId();
        //再判断两者是否相同
        if (oldValue.equals(newValue)){

            //相同情况,直接删除即可
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
        //不相同情况,什么都不做
    }*/

}

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

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

相关文章

高级法医视频分析技术 2024

高级法医视频分析技术 2024 如今&#xff0c;法医视频分析是数字取证的重要组成部分 &#xff0c;因为它可以帮助特工了解视频证据很重要的案件的重要信息。到 2024 年&#xff0c;该领域使用的工具和方法将以前所未有的速度发生变化。在这个领域工作的人需要了解这些变化。在本…

ctfshow-nodejs

什么是nodejs Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境。可以说nodejs是一个运行环境&#xff0c;或者说是一个 JS 语言解释器 Nodejs 是基于 Chrome 的 V8 引擎开发的一个 C 程序&#xff0c;目的是提供一个 JS 的运行环境。最早 Nodejs 主要是安装在服务器…

线性代数 第六讲 特征值和特征向量_相似对角化_实对称矩阵_重点题型总结详细解析

文章目录 1.特征值和特征向量1.1 特征值和特征向量的定义1.2 特征值和特征向量的求法1.3 特征值特征向量的主要结论 2.相似2.1 相似的定义2.2 相似的性质2.3 相似的结论 3.相似对角化4.实对称矩阵4.1 实对称矩阵的基本性质4.2 施密特正交化 5.重难点题型总结5.1 判断矩阵能否相…

Flutter集成Firebase中的 A/B Testing

前提 完成Flutter集成Firebase中的远程配置流程 A/B Test的使用流程 我们先通过远程配置设置变量&#xff0c;应用程序根据变量值展示不同的界面创建一个A/B Test实验&#xff0c;在实验中创建满足条件的用户才能加入到这个实验中&#xff0c;并且在A/B 实验中修改远程配置变…

【网络安全】服务基础第二阶段——第二节:Linux系统管理基础----Linux统计,高阶命令

目录 一、Linux高阶命令 1.1 管道符的基本原理 1.2 重定向 1.2.1 输出重定向 1.2.2 输入重定向 1.2.3 wc命令基本用法 1.3 别名 1.3.1 which命令基本语法 1.3.2 alias命令基本语法 1.4 压缩归档tar 1.4.1 第一种&#xff1a;gzip压缩 1.4.2 第二种&#xff1a;bzip…

多款式随身WiFi如何挑选,USB随身WiFi、无线电池随身WiFi、充电宝随身WiFi哪个好?优缺点分析!

市面上的随身WiFi款式多样琳琅满目&#xff0c;最具代表性的就是USB插电款、无线款和充电宝款。今天就来用一篇文章分析一下这三种款式的优缺点。 USB插电款 优点&#xff1a;便宜&#xff0c;无需充电&#xff0c;在有电源的地方可以随时随地插电使用&#xff0c;比如中兴的U…

交换两个变量数值的3种方法

前言&#xff1a;交换两个数值可不是"a b&#xff0c;b a"。这样做的话&#xff0c;a先等于了b的值&#xff1b;当“b a”后&#xff0c;因为此时a已经等于b的值了&#xff0c;这个语句就相当于执行了b b。最终的数值关系就成了a b&#xff0c;b b。 下面教给大…

综合DHCP、ACL、NAT、Telnet和PPPoE进行网络设计练习

描述&#xff1a;企业内网和运营商网络如上图所示。 公网IP段&#xff1a;12.1.1.0/24。 内网IP段&#xff1a;192.168.1.0/24。 公网口PPPOE 拨号采用CHAP认证&#xff0c;用户名:admin 密码:Admin123 财务PC 配置静态IP&#xff1a;192.168.1.8 R1使用模拟器中的AR201型…

重生奇迹MU老大哥剑士职业宝刀未老

重生奇迹MU中&#xff0c;老大哥剑士职业一直以来备受玩家们的喜爱。这个职业不仅拥有强大的攻击力、防御力和战斗技巧&#xff0c;而且还能够通过使用各种宝刀来增强自身的战斗能力。即便经过了多年的沉淀&#xff0c;老大哥剑士依然是一名宝刀未老的男人&#xff0c;仍然能够…

[羊城杯 2021]Ez_android-快坚持不下去的第五天

找到mainactivity函数 1. 用户名和密码的检查 2. 密码的加密然后 - 1 的操作 for (int i 0; i < bArr.length; i) {bArr[i] (byte) (bArr[i] - 1); } 这段代码通过遍历字节数组中的每个元素&#xff0c;将每个元素的值减去 1&#xff0c;并更新数组。这里的 byte 强制转…

怎么给U盘加密来防止数据泄密?总结了五个管用方法

U盘里的数据通常涉及课件、报表、演讲稿等&#xff0c;非常重要&#xff0c;最好能给U盘加密&#xff0c;这样即使丢了也无法看到里面的内容&#xff0c;那么如何给U盘加密呢&#xff1f; 一、使用Windows BitLocker To Go&#xff08;仅限Windows用户&#xff09; 操作步骤&a…

CentOS7.9虚拟机安装

一、下载CentOS7.9镜像文件 链接: https://pan.baidu.com/s/11eY8sS5mXWwQlW6yO9jokA 提取码: jrm6 二、创建CentOS7.9虚拟机 1、打开vmware,选择新建虚拟机 2、这里选择自定义 3、单击“下一步”。 4、先选择稍后安装 5、这里选择Liunx&#xff0c;然后找到对应的版本 6、…

强调重点元素、弱化辅助元素、去掉无关元素,工控HMI还能好不了

HMI设计&#xff0c;尤其工控领域的HMI设计&#xff0c;千万不要走极端&#xff0c;把界面搞得花哨&#xff0c;或者所谓的美观&#xff0c;切记实现功能才是第一位的。 在人机界面&#xff08;HMI&#xff09;设计中&#xff0c;强调重点元素、弱化辅助元素、去掉无关元素是非…

Winfrom中解决图像、文字模糊的方法

1.添加清单 2.将清单中的下面内容取消注释

python图像处理的图像几何变换

一.图像几何变换 图像几何变换不改变图像的像素值&#xff0c;在图像平面上进行像素变换。适当的几何变换可以最大程度地消除由于成像角度、透视关系乃至镜头自身原因所造成的几何失真所产生的负面影响。几何变换常常作为图像处理应用的预处理步骤&#xff0c;是图像归一化的核…

电力设计院10大排行榜!这个大院屠榜!

今天晚上阅读了中国电力规划设计协会《2022年度电力勘测设计行业统计分析报告》&#xff0c;这本报告是依据协会会员企业统计报表数据进行编制分析的。报告共收集了167家勘测设计企业上报的数据信息&#xff0c;统计的企业数量较2021 年166家企业增加1 家。 按业务板块划分为&…

【全网最全】2024年数学建模国赛A题30页完整建模文档+17页成品论文+保奖matla代码+可视化图表等(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 【全网最全】2024年数学建模国赛A题30页完整建模文档17页成品论文保奖matla代码可视化图表等&#xff08;后续会更新&#xff09;「首先来看看目前已有的资…

14份网络安全意识培训ppt

14份网络安全意识培训ppthttp://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485750&idx1&sn5abd05087334dcf68b1f76aa73011c41&chksmc0e4706af793f97c1bac979ad6a40c54c442a24b5f191162848bf2b06443a5968697ca45ecee#rd 网络安全周就要来了&#xff0c;…

用Boot写mybatis的增删改查

一、总览 项目结构&#xff1a; 图一 1、JavaBean文件 2、数据库操作 3、Java测试 4、SpringBoot启动类 5、SpringBoot数据库配置 二、配置数据库 在项目资源包中新建名为application.yml的文件&#xff0c;如图一。 建好文件我们就要开始写…

鸿蒙自动化发布测试版本app

创建API客户端 API客户端是AppGallery Connect用于管理用户访问AppGallery Connect API的身份凭据&#xff0c;您可以给不同角色创建不同的API客户端&#xff0c;使不同角色可以访问对应权限的AppGallery Connect API。在访问某个API前&#xff0c;必须创建有权访问该API的API…