【Redis】Redis 的学习教程(十一)之使用 Redis 实现分布式锁

1. 分布式锁概念

在多线程环境下,为了保证数据的线程安全,锁保证同一时刻,只有一个可以访问和更新共享数据。在单机系统我们可以使用 synchronized 锁、Lock 锁保证线程安全。

synchronized 锁是 Java 提供的一种内置锁,在单个 JVM 进程中提供线程之间的锁定机制,控制多线程并发。只适用于单机环境下的并发控制。

想要在多个节点中提供锁定,在分布式系统并发控制共享资源,确保同一时刻只有一个访问可以调用,避免多个调用者竞争调用和数据不一致问题,保证数据的一致性,就需要分布式锁

分布式锁:控制分布式系统不同进程访问共享资源的一种锁的机制。不同进程之间调用需要保持互斥性,任意时刻,只有一个客户端能持有锁。

共享资源包含:

  • 数据库
  • 文件硬盘
  • 共享内存

分布式锁特性:

  • 互斥性:锁只能被持有的客户端删除,不能被其他客户端删除
  • 锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁
  • 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效
  • 安全性:锁只能被持有的客户端删除,不能被其他客户端删除

模拟并发环境下单

①:添加 Redis 依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

②:添加配置:

spring:redis:host: localhostport: 6379password:timeout: 2000s# 配置文件中添加 lettuce.pool 相关配置,则会使用到lettuce连接池lettuce:pool:max-active: 8  # 连接池最大连接数(使用负值表示没有限制) 默认为8max-wait: -1ms # 接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1msmax-idle: 8    # 连接池中的最大空闲连接 默认为8min-idle: 0    # 连接池中的最小空闲连接 默认为 0

③:Redis 配置类:

@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// json 序列化配置Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);jackson2JsonRedisSerializer.setObjectMapper(om);// String 序列化StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// 所有的 key 采用 string 的序列化template.setKeySerializer(stringRedisSerializer);// 所有的 value 采用 jackson 的序列化template.setValueSerializer(jackson2JsonRedisSerializer);// hash 的 key 采用 string 的序列化template.setHashKeySerializer(stringRedisSerializer);// hash 的 value 采用 jackson 的序列化template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}}

Redis 工具类:

@Component
public class RedisUtil {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 普通缓存获取public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}// 普通缓存放入public boolean set(String key, Object value) {try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}public boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit));}public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {//springboot2.4后用法redisTemplate.delete(Arrays.asList(key));}}}}

④:添加下单接口

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate RedisUtil redisUtil;@GetMapping("/initProductStock")public void initProductStock() {redisUtil.set("stock", 100);}@GetMapping("/create_order")public void createOrder() {// 获取当前库存int stock = (Integer) redisUtil.get("stock");if (stock > 0) {// 减库存int realStock = stock - 1;redisUtil.set("stock", realStock);// TODO 添加订单记录log.info("扣减成功,剩余库存:" + realStock);return;}log.error("扣减失败,库存不足");}}

接口说明:

  • /order/initProductStock:先向 Redis 中初始化一个库存
  • /order/create_order:下单接口:先从缓存获取库存,如果库存大于 0,则库存减 1

⑤:并发测试

使用 JMeter 进行并发环境测试,10 个线程,循环 5 次。

在这里插入图片描述

⑥:测试结果,打印日志如下:

在这里插入图片描述
使用 JMeter 调用了 50 次接口后,按照正常情况下,库存应该为:50 = 100 - 50。

但通过日志显示,最终库存为:95。

这是因为在并发环境下,多个线程下单操作,前面的线程还未更新库存,后面的线程已经请求进来,并获取到了未更新的库存,后续扣减库存都不是扣减最近的库存。线程越多,扣减的库存越少。这就是在高并发场景下发生的超卖问题。

很明显,上述问题是出现了线程安全的问题,我们首先能想到的肯定是给它加 synchronized 锁。

是的,没问题,但是我们知道,synchronized 锁是属于JVM 级别的,也就是我们所谓的“单机锁”,如果是多机部署的环境中,还能保证数据的一致性吗?

答案肯定是不能的。这个时候,就需要用到了我们 Redis 分布式锁

用 Redis 实现分布式锁的几种方案,都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况

SETNX 是SET IF NOT EXISTS的简写。命令格式:SETNX key value,如果 key不存在,则 SETNX 成功返回 1,如果这个 key 已经存在了,则返回 0

setIfAbsent() 是 setnx + expire 的合并命令

2. Redis分布式锁方案一:SETNX + EXPIRE

问题:为什么要加过期时间

如果在释放锁之前 Redis 宕机了,就会造成一直死锁。

setnx 命令 和 expire 命令一定要是原子操作。

伪代码如下:

if(jedis.setnx(key_resource_id,lock_value) == 1{ //加锁expire(key_resource_id,100; //设置过期时间try {do something  //业务请求}catch(){}finally {jedis.del(key_resource_id); //释放锁}
}

setnxexpire 两个命令分开了,「不是原子操作」。如果执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程 crash 或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」

@GetMapping("/create_order")
public void createOrder() {String key = "lock_key";// 1.加锁boolean lock = tryLock(key, 1, 60L, TimeUnit.SECONDS);if (lock) {try {// 获取当前库存int stock = (Integer) redisUtil.get("stock");if (stock > 0) {// 减库存int realStock = stock - 1;redisUtil.set("stock", realStock);// TODO 添加订单记录log.info("扣减成功,剩余库存:" + realStock);return;}log.error("扣减失败,库存不足");} catch (Exception e) {log.error("扣减库存失败");} finally {// 3.解锁unlock(key);}} else {log.info("未获取到锁...");}
}public boolean tryLock(String key, Object value, long timeout, TimeUnit unit) {return redisUtil.setIfAbsent(key, value, timeout, unit);
}public void unlock(String key) {redisUtil.del(key);
}

使用 JMeter 运行后,结果如下:

在这里插入图片描述

获取到锁的线程已成功扣除库存,没有获取到锁的线程只打印日志。

3. Redis分布式锁方案二:SETNX + EXPIRE + 校验唯一随机值

方案一还是有一定的缺陷的:假设线程 A 获取锁成功,一直在执行业务逻辑,但是 60s 过去了,还没执行完。但是,此时,锁已经过期了。线程 B 又请求过来了,显然,线程 B 也可以获取锁成功,也开始执行业务逻辑代码。那么问题就来了:在线程 B 执行过程中,线程 A 已经执行完了,就会把线程 B 的锁给释放掉!

既然锁可能被别的线程误删,那我们给 value 值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,就OK了

@GetMapping("/create_order")
public void createOrder() {String key = "lock_key";String value = "ID_PREFIX" + Thread.currentThread().getId();// 1.加锁boolean lock = tryLock(key, value, 60L, TimeUnit.SECONDS);if (lock) {// ...
}public void unlock(String key, String value) {String currentValue = (String)redisUtil.get(key);if (StringUtils.hasText(currentValue) && currentValue.equals(value)) {redisUtil.del(key);}
}

这里需要注意的是:释放锁时,先 get 再删除,这并不是原子操作,无法保证进程安全。为了更严谨,这里用 lua 脚本代替

lua 脚本

Lua脚本是redis已经内置的一种轻量小巧语言,其执行是通过 redis 的 eval/evalsha 命令来运行,把操作封装成一个 Lua 脚本,如论如何都是一次执行的原子操作

lockDel.lua如下:resources/lua/lockDel.lua

if redis.call('get', KEYS[1]) == ARGV[1]then-- 执行删除操作return redis.call('del', KEYS[1])else-- 不成功,返回0return 0
end
public void unlock(String key, String value) {// 解锁脚本DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>();unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/lockDel.lua")));unlockScript.setResultType(Long.class);// 执行lua脚本解锁Long execute = redisTemplate.execute(unlockScript, Collections.singletonList(key), value);
}

或者:

public void unlock(String key, String value) {// 解锁脚本String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(key), value);
}

4. Redis分布式锁方案三:Redisson

方案二还存在问题:「锁过期释放,业务没执行完」。 如果设置的超时时间比较短,而业务执行的时间比较长。比如超时时间设置5s,而业务执行需要10s,此时业务还未执行完,其他请求就会获取到锁,两个请求同时请求业务数据,不满足分布式锁的互斥性,无法保证线程的安全

4.1 Redisson 概念

其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。当前开源框架 Redisson 解决了这个问题。Redisson 底层原理图如下:

在这里插入图片描述

只要线程一加锁成功,就会启动一个 watch dog 看门狗,它是一个后台线程,会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断的延长锁 key 的生存时间。因此,Redisson 就是使用 watch dog 解决了「锁过期释放,业务没执行完」问题

Redis 虽然作为分布式锁来说,性能是最好的。但是也是最复杂的。上面总结 Redis 主要有下面几个问题:

  • 未设置过期时间,会死锁
  • 设置过期时间
    • 锁误删
    • 业务还继续执行,导致多个线程并发执行

线上都是用 Redission 实现分布式锁,Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。Redisson 是基于 netty 通信框架实现的,所以支持非阻塞通信,性能优于 Jedis。

Redisson 分布式锁四层保护:

  • 防死锁
  • 防误删
  • 可重入(一个线程可以在获取锁之后再次获取同一个锁,而不需要等待锁释放)
  • 自动续期

Redisson 实现 Redis 分布式锁,支持单机和集群模式

4.2 Redisson 实现

使用 Redission 分布式锁,分成三个步骤:

  1. 获取锁 redissonClient.getLock("lock")
  2. 加锁 rLock.lock()
  3. 解锁 rLock.unlock()

引入依赖:

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

Redisson 配置类:

@Configuration
public class RedissionConfig {@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.password}")private String password;private int port = 6379;@Beanpublic RedissonClient getRedisson() {Config config = new Config();// 单机版config.useSingleServer().setAddress("redis://" + redisHost + ":" + port);//.setPassword(password);config.setCodec(new JsonJacksonCodec());return Redisson.create(config);}}

集群版:

@Bean
public RedissonClient getRedisson() {Config config = new Config();config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒//可以用"rediss://"来启用SSL连接.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");return Redisson.create(config);
}

下单接口:

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate RedissonClient redissonClient;@GetMapping("/create_order")public void createOrder() {String key = "lock_key";RLock rLock = redissonClient.getLock(key);// 1.加锁rLock.lock();try {// 获取当前库存int stock = (Integer) redisUtil.get("stock");if (stock > 0) {// 减库存int realStock = stock - 1;redisUtil.set("stock", realStock);// TODO 添加订单记录log.info("扣减成功,剩余库存:" + realStock);return;}log.error("扣减失败,库存不足");} catch (Exception e) {log.error("扣减库存失败");} finally {// 3.解锁rLock.unlock();}}
}

使用 JMeter 并发运行后:

在这里插入图片描述

Redission 实现的分布式锁,直接调用,不需要锁异常、超时并发、锁删除等问题,它把处理上面的问题的代码都封装好了,直接调用即可

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

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

相关文章

jenkins自动化脚本集成时钉钉消息未发送

在进行jenkins自动化脚本集成时&#xff0c;需要配置钉钉发送消息。钉钉的配置正确&#xff0c;测试钉钉消息发送成功&#xff0c;但是当构建项目时&#xff0c;却没有收到钉钉消息&#xff0c;报错如下&#xff1a; [钉钉插件]发送消息时报错: java.lang.NullPointerExceptio…

大转盘抽奖活动制作流程,让你轻松打造火爆营销活动

抽奖活动一直是商家吸引顾客、推广产品的利器之一。而如何让抽奖活动更加顺利、高效地进行呢&#xff1f;今天我们就要介绍的就是乔拓云平台&#xff0c;通过它&#xff0c;商家可以轻松地制作、发布抽奖活动&#xff0c;让您的营销更加便捷、迅速&#xff01;以下是具体操作步…

【智能电表数据接入物联网平台实践】

智能电表数据接入物联网平台实践 设备接线准备设备调试代码实现Modbus TCP Client 读取电表数据读取寄存器数据转成32bit Float格式然后使用modbusTCP Client 读取数据 使用mqtt协议接入物联网平台最终代码实现 设备接线准备 设备调试 代码实现 Modbus TCP Client 读取电表数…

音乐随行,公网畅享,群辉Audiostation给你带来听歌新体验!

文章目录 本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是本教程使用环境&#xff1a;1 群晖系统安装audiostation套件2 下载移动端app3 内网穿透&#xff0c;映射至公网 很多老铁想在上班路上听点喜欢的歌或者相声解解闷儿&#xff0c;于是打开手…

Go 多版本管理工具

Go 多版本管理工具 文章目录 Go 多版本管理工具一、go get 命令1.1 使用方法&#xff1a; 二、Goenv三、GVM (Go Version Manager)四、voidint/g4.1 安装4.2 冲突4.3 使用 在平时开发中&#xff0c;本地新旧项目并行开发的过程中&#xff0c;你大概率会遇到一个令人头疼的问题&…

开发者福利!李彦宏将在百度世界大会手把手教你做AI原生应用

目录 一、写在前面 二、大模型社区 2.1 加入频道 2.2 创建应用 一、写在前面 1. “把最先进的技术用到极致&#xff0c;把最先进的应用做到极致。” 2. “每个产品都在热火朝天地重构&#xff0c;不断加深对AI原生应用的理解。” 3. “这就是真正的AI原生应用&#xff0c;这…

9月21日作业

登录代码&#xff1a; widget.h #ifndef REGISTER_H #define REGISTER_H#include <QWidget> #include <QDebug> #include <QSqlDatabase> #include <QSqlQuery> #include <QMessageBox>namespace Ui { class Register; }class Register : publ…

Linux开发工具之编辑器-vim

vim简单来说就是一款文本编辑器&#xff0c;用于写代码&#xff0c;更是一款多模式编辑器 vim的基本概念 vim有许多种模式&#xff0c;但是铁三角是以下三种模式&#xff1a;命令模式&#xff0c;插入模式&#xff0c;底行模式 1 正常/普通/命令模式&#xff08;默认打开&…

Docker容器启动失败:找不到映像

Docker容器启动失败&#xff1a;找不到映像 Docker容器启动失败&#xff1a;找不到映像摘要 &#x1f615;引言 &#x1f62e;正文 &#x1f913;为什么会找不到映像&#xff1f; &#x1f615;1. 映像不存在2. 映像标签错误3. 映像不兼容 如何预防和解决问题&#xff1f; &…

权限提升WIN篇(腾讯云,CS,MSF)

溢出漏洞 信息收集 操作系统版本ver&#xff0c;systeminfo漏洞补丁信息systeminfo操作系统位数systeminfo杀软防护tasklist /svc网络netstat -ano,ipconfig当前权限whoami 筛选EXP 根据前面的信息收集中的系统版本&#xff0c;位数和补丁情况筛选出合适的EXP 提权 根据EX…

喜报 | 亮相2023数博会,摘得首届数智金融创新大赛优秀奖

河北正定&#xff0c;千年古城&#xff0c;这里不仅有一幕幕刀光剑影&#xff0c;鼓角争鸣的故事&#xff0c;还有驰名中外的人“一寺四塔”&#xff0c;有宜人的气候&#xff0c;也有汇聚高科技的天下英雄会。 图源于网络 2023年9月6日&#xff0c;河北正定&#xff0c;中国国…

JavaWeb开发-07-MySQL(二)

一.数据库操作-DQL -- 准备测试数据 INSERT INTO tb_emp (id, username, password, name, gender, image, job, entrydate, create_time, update_time) VALUES (1, jinyong, 123456, 金庸, 1, 1.jpg, 4, 2000-01-01, 2022-10-27 16:35:33, 2022-10-27 16:35:35), (2, zhangwuji…

搭建GraphQL服务

js版 GraphQL在 NodeJS 服务端中使用最多 安装graphql-yoga: npm install graphql-yoga 新建index.js: const {GraphQLServer} require("graphql-yoga")const server new GraphQLServer({ typeDefs: type Query { hello(name:String):String! …

Linux CentOS7 tree命令

tree就是树&#xff0c;是文件或文件名输出到控制台的一种显示形式。 tree命令作用&#xff1a;以树状图列出目录的内容&#xff0c;包括文件、子目录及子目录中的文件和目录等。 我们使用ll命令显示只能显示一个层级的普通文件和目录的名称。而使用tree则可以树的形式将指定…

打架识别相关开源数据集资源汇总(附下载链接)

更多数据集分类资源汇总&#xff1a;https://www.cvmart.net/dataSets 监控摄像头下的打架检测 数据集下载链接&#xff1a;http://suo.nz/39IbxQ 该数据集是从包含打架实例的 Youtube 视频中收集的。此外&#xff0c;还包括一些来自常规监控摄像机视频的非打架序列。 总共有…

Linux -- 使用多张gpu卡进行深度学习任务(以tensorflow为例)

在linux系统上进行多gpu卡的深度学习任务 确保已安装最新的 TensorFlow GPU 版本。 import tensorflow as tf print("Num GPUs Available: ", len(tf.config.list_physical_devices(GPU)))1、确保你已经正确安装了tensorflow和相关的GPU驱动&#xff0c;这里可以通…

Mac电脑安装Zulu Open JDK 8 使用 spring-kafka 消费不到Kafka Partition中的消息

一、现象描述 使用Mac电脑本地启动spring-kakfa消费不到Kafka的消息&#xff0c;监控消费组的消息偏移量发现存在Lag的消息&#xff0c;但是本地客户端就是拉取不到&#xff0c;通过部署到公司k8s容器上消息却能正常消费&#xff01; 本地启动的服务消费组监控 公司k8s容器服…

9.2.3.1 【MySQL】XDES Entry链表

当段中数据较少的时候&#xff0c;首先会查看表空间中是否有状态为 FREE_FRAG 的区&#xff0c;也就是找还有空闲空间的碎片区&#xff0c;如果找到了&#xff0c;那么从该区中取一些零碎的页把数据插进去&#xff1b;否则到表空间下申请一个状态为 FREE 的区&#xff0c;也就是…

关于时空数据的培训 GAN:实用指南(第 02/3 部分)

一、说明 在本系列关于训练 GAN 实用指南的第 1 部分中&#xff0c;我们讨论了 a&#xff09; 鉴别器 &#xff08;D&#xff09; 和生成器 &#xff08;G&#xff09; 训练之间的不平衡如何导致模式崩溃和由于梯度消失而导致静音学习&#xff0c;以及 b&#xff09; GAN 对超参…

CTF —— 网络安全大赛(这不比王者好玩吗?)

前言 随着大数据、人工智能的发展&#xff0c;人们步入了新的时代&#xff0c;逐渐走上科技的巅峰。 \ ⚔科技是一把双刃剑&#xff0c;网络安全不容忽视&#xff0c;人们的隐私在大数据面前暴露无遗&#xff0c;账户被盗、资金损失、网络诈骗、隐私泄露&#xff0c;种种迹象…