Redis实战(2)——互斥命令用于构建分布式锁

news2024/9/20 18:35:27

1 什么是分布式锁

在单体应用中,线程锁是可以让多个线程串行执行一段代码逻辑的。不过在集群环境或者是分布式的环境下,线程锁无法保证线程串行运行,从而出现线程安全的问题。

根本的原因在于,在 集群分布式环境下 \textcolor{red}{集群分布式环境下} 集群分布式环境下,用于确保线程串行运行的线程监视器有多个。因为服务如果是分布式的部署,那么一定是在多个JVM中运行的。每个JVM中都将维护自己的堆栈空间。线程监视器同样如此。每个线程监视器都有可能被线程键入, 所以集群、分布式环境下线程锁无线确保线程安全 \textcolor{blue}{所以集群、分布式环境下线程锁无线确保线程安全} 所以集群、分布式环境下线程锁无线确保线程安全

在这里插入图片描述

所以在这种情况下,需要使用分布式锁。

在这里插入图片描述
分布式锁是在集群或者分布式环境下,多进程【线程】可见且互斥的锁。具有以下几个特点。

  • 高可用的获取锁与释放锁;
  • 高性能的获取锁与释放锁;
  • 具备锁失效机制,防止死锁;
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

2 Redis互斥指令实现分布式锁

实现分布式锁有多种方案,如关系型数据库。可在数据库表中维护一张业务锁信息。但是其性能受到数据库性能的影响。而redis数据库是基于内存的数据库,且可集群部署,所以满足高性能和高可用。在互斥与可见方面,可利用setnx这种互斥指令来实现。
另外为了避免服务宕机导致死锁的问题,可以利用redis 的key ttl 时间,超时时可自动删除,防止死锁。总的来说。选用Redis数据库来实现分布式锁是一个有效的方案。
在这里插入图片描述
从该流程可看出,redis实现分布式锁分为两步:获得锁和释放锁。初步实现如下

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;
    //具体业务实现锁时,构造函数传入StringRedisTemplate 操作redis数据库
    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //set nx ex指令。set key成功表示获得锁成功
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, key, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //释放锁
        stringRedisTemplate.delete(key);
    }

2.1 Redis分布式锁超时被误删除问题

上述分布式锁是否完美了呢。显然是可以待优化的。因为上面的锁实现会出现业务超时,线程误删除锁的问题。线程时序图如下
在这里插入图片描述

由于在获得锁的时候,设置key 的过期时间。如果在执行业务时阻塞超时,甚至超过了设置的key过期时间,在业务还没执行完成时,由于redis的过期机制,锁自动就释放了。那么其他的线程就可以获得这个锁。同样的当前一个线程业务执行完成后,在释放锁的时候可能会释放其他线程持有的锁,这样会出现在集群环境下分布式锁带来的线程安全问题。

这里主要问题就是线程会将其他线程持有的锁给删除掉。这里可以在获得锁的时候存入线程标识,删除前判定线程标识是否为当前持有锁的线程。减少锁误删的问题。优化锁的逻辑如下:
在这里插入图片描述

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;

    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+":";

    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        long threadId=Thread.currentThread().getId();
        //线程标识 线程标识为UUID+线程ID 的组合
        String threadMark=ID_PREFIX+threadId;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, threadMark, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        //获得上锁时存入的线程标识信息
        String value=stringRedisTemplate.opsForValue().get(key);
        //判断是否与存入的线程标识一致
        String myThreadMark=ID_PREFIX+Thread.currentThread().getName();
        if(value.equals(myThreadMark)){
            //标识一致时,释放线程的锁,避免误释放其他的锁
            stringRedisTemplate.delete(key);
        }
    }
}

2.2 Redis分布式锁因为非原子性被误删除

优化后,还会出现线程不安全的问题吗。在某些极端情况下,还是会出现线程锁误删,线程不安全的情形。分析时序图如下:

在这里插入图片描述

因为线程一在业务完成后删除锁的阶段中,在判断属于自己的锁后准备删除锁时发生了阻塞,导致原本持有的锁释放。线程二从而抢到锁执行业务逻辑,不过在此阶段中线程一抢到了时间片,继续执行删除锁的逻辑,从而删除了本该线程二持有的锁。此后线程三抢到锁,出现了线程二和线程三并行执行的情况,线程不安全。
经过分析可知,出现锁误删的源头在于,极端情形下,判断锁与删除锁的步骤不是原子操作的,那么就有可能出现线程并行的问题。这里采用的解决方案是在删除锁时将多个命令写在Lua脚本中,然后通过redis 执行Lua脚本。因为lua脚本能保证命令执行的原子性。

删除锁的 L u a 脚本通常可放在 x x x . l u a 文件中,如下所示为 u n l o c k . l u a ,该文件可放在 c l a s s p a t h 路径下: \textcolor{red}{删除锁的Lua脚本通常可放在xxx.lua文件中,如下所示为unlock.lua,该文件可放在classpath 路径下:} 删除锁的Lua脚本通常可放在xxx.lua文件中,如下所示为unlock.lua,该文件可放在classpath路径下:

-- 释放锁的lua脚本
-- 成功释放返回1,没有释放返回0
-- 判断线程锁标识是否一致
if(redis.call('get','KEYS[1]')==ARGV[1])
    --一致则删除锁数据
then redis.call('del',KEYS[1])
end
return 0

优化代码如下:

public class DistributeLock {

    private StringRedisTemplate stringRedisTemplate;

    public DistributeLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+":";

    //释放锁的一个脚本
    private static DefaultRedisScript<Integer> redisScript;

    //初始化删除锁脚本,避免每次调用Lua脚本都要去读取IO产生的延迟问题,
    // static静态代码块只加载一次
    static {
        redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Integer.class);//返回类型是Int
        //lua文件存放在resources目录下的redis文件夹内
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/unlock.lua")));
    }


    /**
     * 获得分布式锁 [基于不同的业务]
     * @param serviceName 业务名称
     * @param time 安全机制 设置过期时间,避免服务宕机时出现死锁问题
     * @param unit 过期时间单位
     * @return
     */
    public boolean getLock(String serviceName, long time, TimeUnit unit){
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        long threadId=Thread.currentThread().getId();
        //线程标识
        String threadMark=ID_PREFIX+threadId;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, threadMark, time, unit);
        //避免拆箱时 空指针问题
        return Boolean.TRUE.equals(aBoolean);

    }
    /**
     * 释放分布式锁  [基于不同的业务]
     * @param serviceName 业务名称
     * @return
     */
    public void delLock(String serviceName ){
       //执行LUA脚本
        //泛型T为脚本返回的类型
        //<T> T execute(RedisScript<T> var1, List<K> var2, Object... var3);
        //var1lua 脚本
        //var2 keys
        //vars value 信息
        String key=RedisConstants.DISTRIBUTE_LOCK+serviceName;
        String myThreadMark=ID_PREFIX+Thread.currentThread().getName();
        Integer result = stringRedisTemplate.execute(redisScript, Arrays.asList(key), myThreadMark);
    }


}

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

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

相关文章

F12 浏览器调试模式页面刷新 network 日志刷新消失的解决办法

每次请求刷新后都把之前的请求记录刷新掉了&#xff0c;把preserve log勾选上后&#xff0c;所有的请求都会保留&#xff0c;再也不怕抓不到记录了。

配置 gitlab https 访问

文章目录 1. 备份2. 生成SSL证书3. 配置文件4. 重启5. 访问 1. 备份 docker exec -ti gitlab-ce gitlab-rake gitlab:backup:create2. 生成SSL证书 yum install openssl openssl-devel -y mkdir /data/gitlab/config/ssl ; cd /data/gitlab/config/ssl### 生成证书 openssl…

郑州Sectigo DV通配符SSL证书

我们在浏览器访问网页时或许不会注意到网站是http还是https链接&#xff0c;但是一定能注意到浏览器给我们展示的“不安全”警告&#xff0c;警告访问者网站未加密&#xff0c;访问网站会有泄露隐私的危险。SSL证书能将网站链接由http转为https&#xff0c;对网站传输数据加密&…

数据科学复现

片段 线性回归建模步骤.py 用sklearn做简单的一元线性回归.py 红用逻辑回归实现红酒数据分类py 高斯朴素贝叶斯分类py

uni-app如何生成正式的APK

第一步&#xff1a; 进入dcloud官网https://dcloud.io/&#xff0c;点击开发者后台进入登录注册页面 第二步&#xff1a;登录之后跳到项目列表&#xff0c;选择自己想要打包的项目 点击进去如果没有生成证书&#xff0c;点击生成证书&#xff0c;如果显示证书已生成就不用管了…

什么样的程序员在35岁后仍然保持竞争力?

作为程序员&#xff0c;大家肯定都听说过程序员的35岁危机&#xff0c;有的人刚入职程序员这个岗位就开始为自己的未来担忧&#xff0c;然而&#xff0c;与其担心自己35岁以后被优化掉&#xff0c;不如现在想想&#xff0c;怎么让自己到了35岁以后在这个岗位上依旧有竞争力。今…

【Python】在PyCharm中安装 ChatGPT 插件,让 AI 帮助我们写代码,从此代码再无报错,小白也能轻易上手!!!

前言 ChatGPT是目前最强大的AI&#xff0c;不仅能够聊天、写小说&#xff0c;甚至码代码也不在话下。 但是在国内要使用chatgpt很麻烦&#xff0c;国内一家团队开发了一款idea插件NexChatGPT&#xff0c;用数据代理的方式&#xff0c;让我们在国内也能轻松的使用chatgpt。 没…

【图像去噪】基于进化算法——自组织迁移算法(SOMA)的图像去噪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

亚马逊云科技联合霞光社发布《2013~2023中国企业全球化发展报告》

中国企业正处于全球聚光灯下。当企业全球化成为时代发展下的必然趋势&#xff0c;出海也从“可选项”变为“必选项”。中国急速扩大的经济规模&#xff0c;不断升级的研发和制造能力&#xff0c;都在推动中国企业不断拓宽在全球各行业的疆域。 过去十年&#xff0c;是中国企业…

Unity XML1——XML基本语法

一、XML 概述 ​ 全称&#xff1a;可拓展标记语言&#xff08;EXtensible Markup Language&#xff09; ​ XML 是国际通用的&#xff0c;它是被设计来用于传输和存储数据的一种文本特殊格式&#xff0c;文件后缀一般为 .xml ​ 我们在游戏中可以把游戏数据按照 XML 的格式标…

SOLIDWORKS 运行缓慢?了解如何诊断SOLIDWORKS大型装配性能问题?

在打开装配时&#xff0c;您的SOLIDWORKS运行缓慢吗&#xff1f;或者不仅打开时间慢&#xff0c;每次点击、旋转或缩放都会产生延迟。 好消息是&#xff0c;我们是可以改善装配加载性能的&#xff0c;困难的部分是需要知道从哪里入手。本文中&#xff0c;我们将研究如何诊断SO…

这几个习惯,让我成为了高阶项目经理

大家好&#xff0c;我是老原。 每个大佬都有每个大佬不同的习惯。但是优秀的大佬之间是有共性的。 当我们把他们的共性当做一种“习惯”来训练自己&#xff0c;有没有可能也把自己培养成大佬 就像你一开始不知道怎么预设风险、沟通和团队协调不够好&#xff1b; 有的人就可…

提升团队协作效率的秘诀!项目管理系统来帮忙!

在疫情的影响下&#xff0c;许多企业受到了经济的影响。企业产值和人员的减少需要提高现有员工的工作效率。项目中的团队合作是提高企业产值的重要组成部分。如何提高项目管理中员工的团队合作效率已成为困扰许多管理者的难题。 许多管理者在领导团队进行项目管理时无法有效地管…

Json-Server模拟服务端接口数据

vue2创建项目&#xff1a; 进入空文件夹 shift右键 进入PowerShell 执行命令&#xff1a; vue init webpack "项目名" 安装json-server npm install -g json-server 查看版本号 json-server -v 创建json数据&#xff0c;并在任意一个文件夹中执行命令 jso…

更安全,更省心丨DolphinDB 数据库权限管理系统使用指南

在数据库产品使用过程中&#xff0c;为保证数据不被窃取、不遭破坏&#xff0c;我们需要通过用户权限来限制用户对数据库、数据表、视图等功能的操作范围&#xff0c;以保证数据库安全性。为此&#xff0c;DolphinDB 提供了具备以下主要功能的权限管理系统&#xff1a; 提供用户…

【主成分分析(PCA)- 鸢尾花】

主成分分析&#xff08;PCA&#xff09; 摘要 在现代数据科学中&#xff0c;维度灾难常常是数据处理与分析的一大难题。主成分分析&#xff08;PCA&#xff09;是一种广泛使用的数据降维技术&#xff0c;它通过将原始数据转换为新的低维空间&#xff0c;保留最重要的信息&…

25岁的Java工程师,6个月顺利转行人工智能

曾经我是一名Java开发者&#xff0c;在过去的日子里&#xff0c;经历了夜以继日的加班、浑浑噩噩的摆烂。 如今&#xff0c;作为一名从博学谷毕业的人工智能从业者&#xff0c;职业生涯再度焕发活力&#xff0c;生活也变得非常愉快。 接下来&#xff0c;我将转变前后的这段经…

Mac-Charles抓包安卓ios证书安装教程

写在前面 鉴于每次给新电脑和新手机安装Charles证书时总会出现这样那样的问题&#xff0c;把上次成功安装证书并且成功抓包的过程记录一下。 电脑信任Charles证书 如果是新安装的Charles&#xff0c;电脑之前没有信任过Charles钥匙串证书的&#xff0c;需要先将Charles的证书信…

RabbitMQ 集群部署

RabbiMQ 是用 Erlang 开发的,集群非常方便,因为 Erlang 天生就是一门分布式语言,但其本身并不支持负载均衡。 RabbitMQ 的集群节点包括内存节点、磁盘节点。RabbitMQ 支持消息的持久化,也就是数据写在磁盘上,最合适的方案就是既有内存节点,又有磁盘节点。 RabbitMQ 模式大…