Redis中分布式锁的使用

在分布式系统中,如果使用JVM中的同步锁在高并发的场景下仍然会产生线程安全问题。首先我们来查看在多个服务器时为什么会产生线程安全问题,有这样一个案例,有一件商品购买规则为一个用户只能购买一次,如果使用同步锁锁住用户id,只能保证在一个服务器中一个用户只能购买一次,在集群模式下,就可能产生并发问题。

为了避免这个问题,我们应该采取一个新的锁监视器,当需要加锁时,所有服务器都需要从外部的锁监视器中查看是否有线程加锁,如果没有则获取互斥锁,如果已经有线程获取到互斥锁,那么就阻塞等待。模型图如下

什么是分布式锁

满足分布式系统或集群模式下多进程可见并互斥的锁。

分布式锁的实现

分布式锁的核心是实现多进程之间的互斥,常见的实现方式有三种

MySQL

Redis

Zookeeper

互斥

利用MySQL本身的互斥锁机制

利用setnx这样的互斥命令

利用节点的唯一性和有序性实现互斥

高可用

高性能

一般

一般

安全性

断开连接,自动释放锁

利用锁超时时间,到期释放

临时节点,断开连接自动释放

这里我们介绍Redis的实现方式,首先是需要实现的两个最基本的方法

获取锁,通过setnx命令,并expire命令设置超时时间。

释放锁,通过del命令,或是宕机后通过超时时间释放。

但是在获取锁时可能会存在一个问题,那就是在setnx时执行成功但是在expire时宕机,没设置到超时时间,为了避免这种情况,我们需要保证两个命令的原子性,可以采用lua脚本又或是采用set方法,指定ex与nx参数,采用set语法如下

set lock 1 nx ex 10 

在Java中实现代码如下

public class SimpleRedisLock{

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

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

    @Override
    public boolean getLock(String key,long timeOut) {
        String id = Thread.currentThread().getId();
        //因为这里Redis会返回一个Boolean类型,但是结果要boolean要进行拆箱,如果没查到的话会返回一个null,直接返回结果容易造成空指针异常
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_Prefix + key, id + "", timeOut, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

   @Override
   public void rmLock(String key) {
       stringRedisTemplate.delete(key_Prefix+key);
   }
}

但是这样又存在一个问题,那就是如果一个获取到锁的线程因为某些原因阻塞,导致已经超过了锁的超时时间还没有执行完毕,此时如果新的线程来获取锁,因为Redis已经将锁删除了,因此可以顺利获取到锁。在第二个线程正在执行业务时,第一个线程执行完毕,开始执行删除锁操作,按照我们所实现的代码,会将第二个线程的锁删除,此时第三个线程它也可以开始获取到新的锁,然后在执行期间锁被第二个线程释放。从而造成并行错误。具体模型图如下

为了解决这个问题,我们需要修改初始代码如下,正确的模型图如下

public class SimpleRedisLock{

    private StringRedisTemplate stringRedisTemplate;

    private static final String key_Prefix="lock:";
    //修改线程标识为UUID,toString方法中的true是为了将UUID中的-去除,我们需要自己在后面拼接一个-
    private static final String ID_Prefix= UUID.randomUUID().toString(true)+"-";

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

    @Override
    public boolean getLock(String key,long timeOut) {
        String id = ID_Prefix+Thread.currentThread().getId();
        //因为这里Redis如果没查到的话会返回一个null类型是Boolean,但是结果要boolean要进行拆箱,boolean只又true以及false,会报空指针异常
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_Prefix + key, id + "", timeOut, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }


   @Override
   public void rmLock(String key) {
       //获取锁标识。如果相同在释放,不同则什么也不做
       String id = ID_Prefix+Thread.currentThread().getId();
       String value = stringRedisTemplate.opsForValue().get(key_Prefix + key);
       if (id.equals(value)) stringRedisTemplate.delete(key_Prefix+key);
   }
}

但是,这样也无法完全解决分布式锁可能产生的问题,接下来我们查看另一种模型图

线程一在判断过Redis中锁标识一样后就在开始执行释放锁时,比如说开始gc垃圾回收或是其他原因导致的暂时阻塞,而在阻塞期间线程一的锁标识超时释放,这时,线程二进行获取锁。线程一阻塞结束,由于要执行删除操作,再次把线程二的锁误删。

这样我们如果通过修改Java代码来解决该问题就过于复杂,需要依赖Redis中的事务机制以及乐观锁实现,因此更推荐使用Lua脚本,来保证获取锁与删除锁的原子性。

简单介绍一下Redis中在Lua语言中提供调用的方法

redis.call('命令名称','key','其他参数')

-- 比如说要执行set name 张三
redis.call('set','name','张三')

-- 如果不想写死需要执行key,value,那么可以通过参数传递
-- key 类型会放在KEYS数组当中。value会放在ARGV数组当中
-- 需要注意的时,Lua语言中数组下标以1开始
redis.call('set','KEYS[1]','ARGV[1]')

RedisTemplate调用lua脚本的API如下

redistemplate.excute(script,keys,args);

再次修改Java代码如下

public class SimpleRedisLock{

    private StringRedisTemplate stringRedisTemplate;

    private static final String key_Prefix="lock:";
    private static final String ID_Prefix= UUID.randomUUID().toString(true)+"-";

    private static final DefaultRedisScript<Long> rmLock_script;
    static {
        rmLock_script=new DefaultRedisScript<>();
        //设置脚本位置
        rmLock_script.setLocation(new ClassPathResource("rmlock.lua"));
        //设置返回类型
        rmLock_script.setResultType(Long.class);
    }
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean getLock(String key,long timeOut) {
        String id = ID_Prefix+Thread.currentThread().getId();
        //因为这里Redis如果没查到的话会返回一个null类型是Boolean,但是结果要boolean要进行拆箱,boolean只又true以及false,会报空指针异常
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_Prefix + key, id + "", timeOut, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void rmLock(String key) {
        //调用lua脚本
        stringRedisTemplate.execute(rmLock_script, Collections.singletonList(key_Prefix + key),ID_Prefix+Thread.currentThread().getId());
    }

}

具体的执行脚本代码如下

if(redis.call('get',KEYS[1]) == ARGV[1]) then
  return redis.call('del',KEYS[1])
end
return 0

这样的实现方式已经可以避免普通的并发问题,但是仍然存在一定问题,比如说存在一个业务需要方法A调用方法B而在这两个方法中需要获取同一把锁,那么就是产生死锁问题,因此我们还需要实现锁可重入。其次我们的实现方式中,如果没有获取到锁会立即返回,但是通常我们需要进行重试,我们还需要实现重试机制。还有主从不一致问题,这些问题让我们实际开发中实际并不现实,因此我们可以选择Redisson来解决以上问题。

Redisson的简单使用

在pom文件引入依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.19.0</version>
</dependency>

配置Redis,我们可以选择yaml文件配置,也可以选择Java配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient(){
        //配置类
        Config config=new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

Redisson的简单使用

@SpringBootTest
class RedissonApplicationTests {

    @Resource
    private RedissonClient redissonClient;

    @Test
    public void test01() throws Exception {
        //获取锁,指定锁名称,可重入
        RLock lock = redissonClient.getLock("lock");
        //三个参数分别是,最大获取锁等待时间(期间会重试),锁自动释放时间,时间单位
        boolean flag = lock.tryLock(1, 10, TimeUnit.SECONDS);
        if (flag){
            try{
                System.out.println("获取锁成功");
            }finally {
                lock.unlock();
            }
        }
    }
}

锁在Redis中存储结构如下,其中value代表的是锁重入次数

 

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

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

相关文章

C# Spire操作Excel数据透视表

一、概述 数据透视表&#xff08;Pivot Table&#xff09;是一种交互式的表&#xff0c;可以进行某些计算&#xff0c;如求和与计数等&#xff0c;可动态地改变透视表版面布置&#xff0c;也可以重新安排行号、列标和页字段。当改变版面布置时&#xff0c;数据透视表也会按照新…

PMP-01

考纲 需要看的书籍 学习计划

奇技淫巧第9期

今天回顾一下 5~12 月所遇到的零碎知识点。 文章目录 歪门邪道优雅删除“学习资料”快速下载 vscode两种硬盘格式zotero在word中插入参考文献markdown 下划线查看 CPU Linux 命令postgres 无法通过 root 用户操作bash 初学者礼包gitwin 11 edge 浏览器0x80190001 报错 python …

Pycharm调用Conda虚拟环境

参考这个链接的评论区回答&#xff1a;Pycharm调用Conda虚拟环境 笑死&#xff0c;我之前也是这样的&#xff0c;不过好像也能用&#xff0c;搞不懂~

蓝桥杯每日一题2023.12.2

题目描述 蓝桥杯大赛历届真题 - C 语言 B 组 - 蓝桥云课 (lanqiao.cn) 题目分析 答案&#xff1a;3598180 由题目分析可以知道&#xff0c;给小明发的牌一共有13种类型&#xff0c;每种类型的牌一共有四张。对于每种牌&#xff0c;我们都有5种选择&#xff0c;不拿、拿一张、…

WebGL笔记:矩阵缩放的数学原理和实现

矩阵缩放的数学原理 和平移一样&#xff0c;以同样的原理&#xff0c;也可以理解缩放矩阵让向量OA基于原点进行缩放 x方向上缩放&#xff1a;sxy方向上缩放&#xff1a;syz方向上缩放&#xff1a;sz 最终得到向量OB 矩阵缩放的应用 比如我要让顶点在x轴向缩放2&#xff0c;y轴…

ArrayList 与 顺序表 (附洗牌算法)!

曾经我也是一枚学霸&#xff0c;直到有一天想去学渣的世界看看&#xff0c;结果就找不到回去的路了。 目录 1. 线性表 2.顺序表 2.1 接口的实现 3. ArrayList简介 4. ArrayList使用 4.1 ArrayList的构造 4.2 ArrayList常见操作 4.3 ArrayList的遍历 4.4 ArrayList的扩…

分享66个焦点幻灯JS特效,总有一款适合您

分享66个焦点幻灯JS特效&#xff0c;总有一款适合您 66个焦点幻灯JS特效下载链接&#xff1a;https://pan.baidu.com/s/10bqe09IAZt_hbsZlXaxkxw?pwd6666 提取码&#xff1a;6666 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;…

万能的视频格式播放器

今天博主给大家带来一款“万能”的视频播放器——VLC Media Player&#xff0c;支持的文件格式非常多&#xff0c;大家快来一起看看吧&#xff01; VLC Media Player 是一款可播放大多数格式&#xff0c;而无需安装编解码器包的媒体播放器。可以播放 MPEG-1、MPEG-2、MPEG-4、D…

Tektronix泰克示波器

一、what’s the oscilloscope&#xff1f; 【ref】https://www.tek.com.cn/blog/what-is-an-oscilloscope 二、基础知识 1、带宽&#xff1a;100Mhz&#xff1b;采样率&#xff1a;2.5GS/s 1GS/s指的是采样率&#xff0c;前面大写的S是sample采样的意思 后面的s是秒 也就是示波…

粉丝提问:岗位与描述不一致,小公司感觉学不到东西,工作内容就是调试,想辞职

0、粉丝问题&#xff1a; 大哥&#xff0c;我毕业已经工作两个月了&#xff0c;在一家小公司&#xff0c;岗位和描述的不一致&#xff0c;感觉就像调试一样&#xff0c;写代码的机会很少也没人带&#xff0c; 我想转嵌入式&#xff0c;您有什么建议的方向吗&#xff0c;或者是…

MathType公式编辑器安装教程

一、下载 MathType7是一款可以帮助用户快速完成数学公式编辑的应用软件&#xff0c;这款软件适合在进行教育教学、科研机构、论文写作的时候使用。我们可以直接通过这款软件来获取到大量数学上使用到的函数、数学符号等内容&#xff0c;然后使用这些内容来完成公式编辑。 …

玩转大数据4:大数据的崛起与应用领域探索

图片来源网络 引言 在当今数字化时代&#xff0c;大数据正以前所未有的速度和规模崛起。大数据的出现不仅改变了企业和组织的经营模式&#xff0c;也对我们的社会生活带来了深刻的影响。Java作为一种广泛使用的编程语言&#xff0c;在大数据领域发挥着重要的作用。本文将重点…

自动驾驶学习笔记(十三)——感知基础

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo Beta宣讲和线下沙龙》免费报名—>传送门 文章目录 前言 传感器 测距原理 坐标系 标定 同…

初识Linux:保姆级教学,让你一秒记住Linux中的常用指令!

文章目录 前言一、LInux的背景及发展史二、Linux下的基本指令1、ls指令2、pwd指令3、cd指令4、touch指令5、mkdir指令&#xff08;重要&#xff09;6、tree指令7、rmdir指令和rm指令&#xff08;重要&#xff09;8、man指令&#xff08;重要&#xff09;9、cp指令&#xff08;重…

操作PDF相关的工具,EPUB转PDF,golang

unipdf 安装依赖 go get github.com/unidoc/unipdf/v3 示例代码 https://github.com/unidoc/unipdf-examples 获取KEY 登录 https://cloud.unidoc.io/ 注册账号&#xff0c;生成 KEY&#xff0c;但是需要收费。 chromedp 使用Golang编写&#xff0c;主要功能是调用浏览器内…

【面试攻略】Oracle中blob和clob的区别及查询修改方法

大家好&#xff0c;我是小米&#xff0c;欢迎来到小米的技术小屋&#xff01;今天我们要一起来聊聊一个在面试中常常被问到的问题——“Oracle中Blob和Clob有啥区别&#xff0c;在代码中怎么查询和修改这两个类型的字段里的内容&#xff1f;”别急&#xff0c;跟着小米一步步揭…

Android11适配已安装应用列表

Android11适配已安装应用列表 之前做过已安装应用列表的适配&#xff0c;最近国内版SDK升级到33和隐私合规遇到很多问题&#xff0c;于是把已安装应用列表记录一下&#xff1a; 1、在Android11及以上的适配&#xff1a; package com.example.requestinsttallapplistdemoimpo…

K210开发板之VSCode开发环境使用中添加或删除文件(编译失败时)需要注意事项

在最初开始接触&#xff0c;将VScode和编译环境搭载好后&#xff0c;就开始运行第一个程序了&#xff0c;为了后续方便开发测试&#xff0c;这里我自己对照官方提供的例子&#xff0c;自己调试&#xff0c;写了一个简单的文件系统 后续&#xff0c;所有关于开发的源文件都在...…

Sun Apr 16 00:00:00 CST 2023格式转换

Date date new Date(); log.info("当前时间为:{}",date); //yyyy-MM-dd HH:mm:ss SimpleDateFormat sdf new SimpleDateFormat(DateUtils.YYYY_MM_DD_HH_MM_SS); String dateTime s…