Redis中分布式锁的使用

news2025/1/18 16:54:01

在分布式系统中,如果使用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代表的是锁重入次数

 

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

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

相关文章

vue3中自定义hook函数

使用Vue3的组合API封装的可复用的功能函数 自定义hook的作用类似于vue2中的mixin技术 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂 案例: 收集用户鼠标点击的页面坐标 hooks/useMousePosition.ts文件代码&#xff1a; import { ref, onMounted, onUnmounted …

【读书笔记】微习惯

周日晚上尝试速读一本书《微习惯》&#xff0c;共七章看了下目录结构并不复杂&#xff0c;计划每章7-8分钟读完&#xff0c; 从20:15-21:00。读的时候&#xff0c;订下闹钟&#xff0c;催促着自己的进度。边读边记了一些要点和微信读书里面的划线。 第六章实践内容最为丰富&…

1949-2021年全国31省铁路里程数据

1949-2021年全国31省铁路里程数据 1、时间&#xff1a;1949-2021年 2、指标&#xff1a;时间、省份、铁路里程 3、范围&#xff1a;包括31省 4、数据缺失情况说明&#xff1a;西藏2005年之前存在缺失&#xff0c;其余30省份1978-2020年无缺失 5、来源&#xff1a;各省统计…

C++实现DFS、BFS、Kruskal算法和Prim算法、拓扑排序、Dijkstra算法

背景&#xff1a; 实现要求&#xff1a; 根据图的抽象数据类型的定义&#xff0c;请采用邻接矩阵来存储图1&#xff0c;采用邻接表来存储图2&#xff0c;并完成如下操作&#xff1a;对图1无向图进行深度优先遍历和广度优先遍历。对图1无向图采用Kruskal算法和Prim算法得出最小…

uni-app 微信小程序之自定义navigationBar顶部导航栏

文章目录 1. 实现效果2. App.vue3. pages.json 配置自定义4. 顶部导航栏 使用 微信小程序自定义 navigationBar 顶部导航栏&#xff0c;兼容适配所有机型 1. 实现效果 2. App.vue 在App.vue 中&#xff0c;设置获取的 StatusBar&#xff0c;CustomBar 高度&#xff08;实现适配…

【云原生Prometheus篇】Prometheus PromQL语句详解 1.0

文章目录 一、前言1.1 Prometheus的时间序列1.1.1 指标名称1.1.2 标签1.1.3 使用的注意事项 1.2 样本数据格式1.3 Prometheus 的聚合函数 二 、PromQL 理论部分2.1 PromQL简介2.2 PromQL的数据类型2.3 时间序列选择器2.3.1 瞬时向量选择器 &#xff08;Instant Vector Selector…

python装饰器解析(关键点:高阶函数、嵌套函数)(参数化装饰器、类装饰器)

文章目录 Python装饰器解析什么是Python装饰器基础理解 如何创建装饰器&#xff08;关键点&#xff1a;高阶函数、嵌套函数&#xff09;创建基础装饰器 使用装饰器使用示例 装饰器的返回值参数化装饰器创建参数化装饰器语法示例使用示例 类装饰器创建类装饰器语法示例使用示例 …

使用postman请求x5接口

x5接口简介 1.接口样例 {"header"{"appid":"bpmnew_fanwei","sign":"C033162E86E4CADE80C7EB44D68A5AD2","sign_type":"md5","url":"https://oa.mioffice.cn/api/bpm/xm/app/show/tod…

MySQL索引优化实战二

分页查询优化 很多时候我们业务中实现分页功能时可能会用如下SQL来实现&#xff1a; select * from employees LIMIT 10000,10表示从表中中区从10001行开始的10行记录&#xff0c;看似只查了10条记录&#xff0c;但是这条SQL是先读取10010条记录&#xff0c;然后抛弃前10000条…

【个人笔记】-python-强化学习-类-在内存中的值

{int} 数值 {int} 200 {float} 数值 {float} 0.9 {narray:维度} 数值 {narray:(1,)} [2.] {bool} True {bool} False {类名} 对象1 {类名} 对象2

LLM 开发模式 RAG,MRKL,Re-Act,Plan-Execute 模式对比

本心、输入输出、结果 文章目录 LLM 开发模式 RAG&#xff0c;MRKL&#xff0c;Re-Act&#xff0c;Plan-Execute 模式对比前言RAG、MRKL、Re-Act和Plan-Execute模式的一些对比花有重开日&#xff0c;人无再少年实践是检验真理的唯一标准 LLM 开发模式 RAG&#xff0c;MRKL&…

MySQL进阶_EXPLAIN重点字段解析

文章目录 第一节.准备1.1 版本信息1.2 准备 第二节.type2.1 system2.2 const2.3 eq_ref2.4 ref2.5 ref_or_null2.6 index_merge2.7 unique_subquery2.8 range2.9 index2.10 all 第三节. Extra3.1 No tables used3.2 No tables used3.3 Using where3.4 No matching min/max row3…

基于springboot实现的垃圾分类管理系统

一、系统架构 前端&#xff1a;html | layer | jquery | css 后端&#xff1a;springboot | mybatis 环境&#xff1a;jdk1.8 | mysql | maven 二、 代码及数据库 三、功能介绍 01. 登录页 02. 系统设置-用户管理 03. 系统设置-页面管理 04. 系统设置-角色管…

Python练习题(二)

&#x1f4d1;前言 本文主要是【Python】——Python练习题的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句&am…

三极管在数字电路中的应用

一、认识三极管 三极管拥有3个引脚&#xff0c;分别对应3个级&#xff1a;基极(Base)、发射极&#xff08;Emitter&#xff09;、集电极(Collector)&#xff0c;如下图所示&#xff1b;下图横向左侧的是基极&#xff0c;带箭头的那个引脚就是发射极&#xff0c;另一个就是集电…

ElementPlus中 使用ElLoading.service, spinner: ‘el-icon-loading‘不生效

let downloadLoadingInstance ElLoading.service({ text: "正在下载数据&#xff0c;请稍候",spinner: el-icon-loading, background: "rgba(0, 0, 0, 0.7)", })使用以上代码时&#xff0c;加载的圆圈出不来&#xff0c;使用f12查看&#xff0c;即使能出…

代理模式介绍(静态代理、jdk动态代理、cglib代理)

一、静态代理 &#xff08;一&#xff09;定义 1、定义 为其他对象提供一种代理以控制对这个对象的访问&#xff1b; 2、涉及到的角色 &#xff08;1&#xff09;抽象主题角色&#xff1a;真实主题和代理主题的共同接口&#xff0c;便于在使用真实主题的地方都可以使用代理…

blue beacon rssi 指纹室内定位数据集

数据集是开展实验的基础&#xff0c;搜集并分享。如果你有关于室内定位的问题&#xff0c;请联系博主。 namedatesetpapercommentBLEBeacon: A Real-Subject Trial Dataset from Mobile Bluetooth Low Energy Beaconshttps://github.com/dimisik/BLEBeacon-Datasethttps://arxi…

React18 入门与进阶

React18 入门与进阶 前言一、核心概念与类组件使用1、虚拟DOM与新的渲染写法2、JSX 与 JSX 的使用3、类组件和函数组件4、类组件与类组件通信5、props详解与注意事项6、类组件中事件的使用7、类组件响应式数据实现与原理8、PureComponent 与 shouldComponentUpdate9、immutable…

Kubernetes(K8s)_15_CNI

Kubernetes&#xff08;K8s&#xff09;_15_CNI CNI网络模型UnderlayMAC VLANIP VLANDirect Route OverlayVXLAN CNI插件FlannelCalico CNI配置内置实现 CNI CNI(Container Network Interface): 实现容器网络连接的规范 Kubernetes将网络通信可分为: Pod内容器、Pod、Pod与Se…