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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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]) thenreturn 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 {@Beanpublic RedissonClient redissonClient(){//配置类Config config=new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);}
}

Redisson的简单使用

@SpringBootTest
class RedissonApplicationTests {@Resourceprivate RedissonClient redissonClient;@Testpublic 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.mzph.cn/news/193790.shtml

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

相关文章

AR助推制造业智能转型:实时远程协作与可视化引领生产创新

制造商面临着多方面的变革&#xff0c;技术的兴起催生了工业物联网&#xff08;IIoT&#xff09;&#xff0c;改变了现代工厂的外貌、系统和流程。同时&#xff0c;全球竞争压力和不断变化的员工队伍要求采用新的员工培训方法&#xff0c;并重新审视工人在工厂中的角色。尽管如…

Linux(13):例行性工作排程

例行性工程 听谓的排程是将工作安排执行的流程之意。 Linux 排程就是透过 crontab 与 at 这两个东西。 两种工作排程的方式&#xff1a; 一种是例行性的&#xff0c;就是每隔一定的周期要来办的事项&#xff1b; 一种是突发性的&#xff0c;就是这次做完以后就没有的那一种&a…

【算法】希尔排序

目录 1. 说明2. 举个例子3. java代码示例4. java示例截图 1. 说明 1.希尔排序是直接插入排序的一种改进&#xff0c;其本质是一种分组插入排序 2.希尔排序采取了分组排序的方式 3.把待排序的数据元素序列按一定间隔进行分组&#xff0c;然后对每个分组进行直接插入排序 4.随着间…

MySQL 8创建数据库、数据表、插入数据并且查询数据

我使用的数据库是MySQL 8。 创建数据库 create database Bookbought; -- 创建数据库Bookbought use Bookbought; -- 使用数据库Bookbought创建数据表 创建用户表bookuser。 create table ## 往allbook里边插入数据(id INT PRIMARY KEY AUTO_INCREMENT, -- id 为 主键userna…

CCKS2023-面向上市公司主营业务的实体链接评测-亚军方案

赛题分析 大赛地址 https://tianchi.aliyun.com/competition/entrance/532097/information 任务描述 本次任务主要针对上市公司的主营业务进行产品实体链接。需要获得主营业务中的产品实体&#xff0c;将该实体链接到产品数据库中的某一个标准产品实体。产品数据库将发布在竞赛…

机器学习决策树ID3算法

1、先去计算总的信息量 2、根据不同指标分别计算对应的信息增益 3、根据算出的信息增益来选择信息增益最大的作为根结点 4、天气中选择一个继续上述过程 5、决策树划分结束

MySQL索引优化实战二

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

Spring事务管理介绍

文章目录 Spring事务管理1 Spring事务简介【重点】问题导入1.1 Spring事务作用1.2 需求和分析1.3 代码实现【前置工作】环境准备【第一步】在业务层接口上添加Spring事务管理【第二步】设置事务管理器(将事务管理器添加到IOC容器中)【第三步】开启注解式事务驱动【第四步】运行…

智能优化算法应用:基于黄金正弦算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于黄金正弦算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于黄金正弦算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.黄金正弦算法4.实验参数设定5.算法结果6.参考…

Nginx配置反向代理与负载均衡

Nginx配置反向代理与负载均衡 一、代理服务1.正向代理2.反向代理 二、实战场景-反向代理1.修改nginx配置 -> nginx.conf文件2.修改前端路径 三、实战场景-负载均衡1.热备2.轮询3.加权轮询4.ip_hash ​ Nginx (“engine x”) 是一个高性能的HTTP和反向代理服务器&#xff0c;…

10 分钟解释 StyleGAN

一、说明 G在过去的几年里&#xff0c;生成对抗网络一直是生成内容的首选机器学习技术。看似神奇地将随机输入转换为高度详细的输出&#xff0c;它们已在生成图像、生成音乐甚至生成药物方面找到了应用。 StyleGAN是一种真正推动 GAN 最先进技术向前发展的 GAN 类型。当Karras …

力扣题:字符串的反转-11.23

力扣题-11.23 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;557. 反转字符串中的单词 III 解题思想&#xff1a;先读取单词&#xff0c;然后将单词进行翻转即可 class Solution(object):def reverseWords(self, s):""":type s…

2024年AMC8美国初中数学竞赛最后一个月复习指南(附资料)

还有一个半月的时间&#xff0c;2024年AMC8&#xff08;大家默认都直接叫这个比赛的英文名&#xff0c;而不叫中文名美国数学竞赛或美国初中数学竞赛了&#xff09;就要开始了。 有志于在2024年AMC8的比赛中拿到奖项的孩子已经在“磨拳霍霍”了。那么最后一个半月的时间该如何…

LeetCode刷题---反转链表

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏&#xff1a;http://t.csdnimg.cn/ZxuNL http://t.csdnimg.cn/c9twt 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回溯算法&#xff0c;所以下面题目主要也是这些算法做的 我讲述…

Linux 进程状态

操作系统学科的进程状态 新建态&#xff1a;刚刚创建的进程&#xff0c; 操作系统还未把它加入可执行进程组&#xff0c; 它通常是进程控制块已经创建但还未加载到内存中的新进程。就绪态&#xff1a;进程做好了准备&#xff0c;只要有机会就开始执行。阻塞态&#xff1a;进程在…

Qt+ROS+ubuntu18.04配置教程(带界面)

1. 安装ROS Qt Creator Plug-in 首先安装ROS Qt Creator Plug-in&#xff0c;这其实是一个带有ROS插件的Qt Creator&#xff1a;去下面的网址https://ros-qtc-plugin.readthedocs.io/en/latest/_source/How-to-Install-Users.html#qt-installer-procedure&#xff0c;根据自己…

Java数据结构 之 包装类简单认识泛类

生命不息&#xff0c;奋斗不止 目录 1. 什么是包装类&#xff1f; 1.1 装箱和拆箱 1.2 自动装箱和自动拆箱 2. 什么是泛型 3. 引出泛型 3.1 语法 4 泛型类的使用 4.1 语法 4.2 示例 4.3 类型推导(Type Inference) 5. 裸类型(Raw Type) &#xff08;了解&#xff09…

IPv6是趋势!如何在Windows上禁用或启用IPv6?有3种简单的方法

IPv6是IPv4的一个更加安全、可扩展和可靠的继任者。然而&#xff0c;这种较新的互联网协议与IPv4不向后兼容&#xff0c;并且大多数VPN服务提供商不支持IPv6协议。 Microsoft不建议用户禁用IPv6或其组件&#xff0c;除非他们需要解决网络问题。但是&#xff0c;如果你计划禁用…

MATLAB实战 | 求水仙花数

循环结构的基本思想是重复&#xff0c;即利用计算机运算速度快以及能进行逻辑控制的特点&#xff0c;重复执行某些语句&#xff0c;以满足大量的计算要求。虽然每次循环执行的语句相同&#xff0c;但语句中一些变量的值是变化的&#xff0c;而且当循环到一定次数或满足条件后能…

阿里云服务器活动:免费试用ECS,轻松搭建WordPress博客平台,送午睡毯及猫超卡

阿里云服务器免费试用3个月 &#xff0c;搭建WordPress博客平台&#xff0c;还送午睡毯及猫超卡。活动时间截止至12月8日 网址&#xff1a; 阿里云服务器薅羊毛 送午睡毯