redis 极简分布式锁实现

写在前面


  • 工作中遇到,整理 reids 做简单分布式锁的思考
  • 博文适合刚接触 redis 的小伙伴
  • 理解不足小伙伴帮忙指正

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》


假设现在有这样一个需求,需要做排队预约住宿的功能,当前宿舍住满了,有新的同学需要来入住,可以进行排队预约,排队编号通过累加的方式生成

我们设计这样一张数据表

CREATE TABLE `ams_student_queue_check_in_sync` (`queue_check_in_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '学生队列ID',`student_name` VARCHAR(50) NOT NULL COMMENT '学生姓名' COLLATE 'utf8mb4_general_ci',`student_uid` VARCHAR(50) NULL DEFAULT NULL COMMENT '学生uid' COLLATE 'utf8mb4_general_ci',`student_card` VARCHAR(30) NULL DEFAULT NULL COMMENT '学生身份证号' COLLATE 'utf8mb4_general_ci',`student_contact_number` VARCHAR(20) NOT NULL COMMENT '学生联系电话' COLLATE 'utf8mb4_general_ci',`student_email` VARCHAR(50) NULL DEFAULT NULL COMMENT '学生电子邮件地址' COLLATE 'utf8mb4_general_ci',`student_gender` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '学生性别',`student_emergency_contact_name` VARCHAR(100) NULL DEFAULT NULL COMMENT '第二联系人姓名' COLLATE 'utf8mb4_general_ci',`student_emergency_contact_number` VARCHAR(20) NULL DEFAULT NULL COMMENT '第二联系人电话' COLLATE 'utf8mb4_general_ci',`student_status` TINYINT(4) NULL DEFAULT '1' COMMENT '学生排队状态(1.待入住,2.以入住 3.以取消)',`arrival_dates` DATETIME NULL DEFAULT NULL COMMENT '预计入住时间',`departure_dates` DATETIME NULL DEFAULT NULL COMMENT '预计离开日期',`queue_position` INT(11) NULL DEFAULT NULL COMMENT '学生在排队中的位置',`check_in_remark` TEXT NULL DEFAULT NULL COMMENT '备注' COLLATE 'utf8mb4_general_ci',`extended1` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段1' COLLATE 'utf8mb4_general_ci',`extended2` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段2' COLLATE 'utf8mb4_general_ci',`extended3` VARCHAR(50) NULL DEFAULT NULL COMMENT '扩展字段3' COLLATE 'utf8mb4_general_ci',`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`queue_check_in_id`) USING BTREE,INDEX `student_uid` (`student_uid`) USING BTREE
)
COMMENT='入住排队表'
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1363
;

queue_position 为每一位同学的排队编号,需要根据当前的学生编号最大来累加

下面为实现的基础代码

    @ApiOperation("入住排队接口")@PostMapping("/checkInQueue")@Transactionalpublic   AjaxResult checkInQueue( @RequestHeader("UID") String uid, @RequestBody AmsStudentQueueCheckIn amsStudentQueueCheckIn){if (Objects.isNull(uid)){return AjaxResult.error("Uid 为空");}if (Objects.isNull(amsStudentQueueCheckIn.getStudentEmergencyContactNumber())){return AjaxResult.error("电话号为空");}StringBuilder stringBuilder = new StringBuilder();String studentContactNumber = amsStudentQueueCheckIn.getStudentContactNumber();List<AmsStudentQueueCheckIn> amsStudentQueueCheckIns1 = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInList(new AmsStudentQueueCheckIn().setStudentContactNumber(studentContactNumber));Integer count = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListCount(amsStudentQueueCheckIn.getStudentGender());if (Objects.nonNull(amsStudentQueueCheckIns1) && amsStudentQueueCheckIns1.size() !=0 ){stringBuilder.append("已经排队预约啦,请耐心等待 ^_^").append(", 预约编号为 " ).append(amsStudentQueueCheckIns1.get(0).getQueuePosition()).append(", 前面还有 ").append(count - 1).append( " 人");return  AjaxResult.success(stringBuilder.toString(),ImmutableMap.of("queuePosition",amsStudentQueueCheckIns1.get(0).getQueuePosition(),"beforePeopleBumber",count -1 ));}AmsStudentQueueCheckIn amsStudentQueueCheckIns = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListMax(amsStudentQueueCheckIn.getStudentGender());Long queuePosition = 0L;if (Objects.nonNull(amsStudentQueueCheckIns)){queuePosition = amsStudentQueueCheckIns.getQueuePosition()}amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(queuePosition + 1L).setStudentUid(uid);amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(amsStudentQueueCheckIns.getQueuePosition() + 1L).setStudentUid(uid);int i = amsStudentQueueCheckInService.insertAmsStudentQueueCheckIn(amsStudentQueueCheckIn);if (i != 1){return AjaxResult.error("排队预约失败!");}stringBuilder.append("排队预约成功").append(", 预约编号为 " ).append(amsStudentQueueCheckIn.getQueuePosition()).append(", 前面还有 ").append(count).append( " 人");return  AjaxResult.success(stringBuilder.toString(),ImmutableMap.of("queuePosition",amsStudentQueueCheckIn.getQueuePosition(),"beforePeopleBumber",count));}

逻辑比较简单,拿到数据,获取编号最大值累加,数据落表,但是上面的代码存在一个问题,因为是 Springboot 项目,使用 tomcat 部署,Spring Boot 嵌入的 Tomcat 默认启用 Http11NioProtocol,可以切换日志级别为 Debug 可看到

Http11NioProtocol 表示多线程非阻塞模式的HTTP协议的通信(web 服务端网络IO处理模型包括:单(多)线程阻塞(非阻塞)IO模型)。

# 日志级别 Debug
# 日志配置
logging:level:root: debug
11:42:51.810 [restartedMain] INFO  o.a.c.h.Http11NioProtocol - [log,173] - Initializing ProtocolHandler ["http-nio-8080"]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [Connector[HTTP/1.1-8080]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [INITIALIZED]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [STARTING_PREP]
11:42:51.811 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardServer[-1]] to [STARTING]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTING_PREP]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTING]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [org.apache.catalina.deploy.NamingResourcesImpl@1dc49001] to [STARTED]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [STARTING_PREP]
11:42:51.812 [restartedMain] INFO  o.a.c.c.StandardService - [log,173] - Starting service [Tomcat]
11:42:51.812 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardService[Tomcat]] to [STARTING]
11:42:51.813 [restartedMain] DEBUG o.a.c.u.LifecycleBase - [log,173] - Setting state for [StandardEngine[Tomcat]] to [STARTING_PREP]
11:42:51.813 [restartedMain] INFO  o.a.c.c.StandardEngine - [log,173] - Starting Servlet engine: [Apache Tomcat/9.0.75]

可以看到 spring-boot-starter-web 嵌入的 9.0.75 版本的 tomcat ,Tomcat 从 8.5 版本开始移除了 BIO,默认启用 NIO

下图为从套接字连接接收、处理请求、响应客户端的整个过程

《Tomcat内核设计剖析》

所以当多个排队请求并发调用接口时,不同的线程会分别进入方法,这个时候有可能会从数据库获取相同的排队编号进行累加,同时生成相同新编号,所以这里需要考虑方法线程安全,

最简单的方式是使用同步方法,保证只有一个线程获取锁,但是这不是最优的方式,这里不做考虑

 public  synchronized  AjaxResult checkInQueue( @RequestHeader("UID") String uid, @RequestBody AmsStudentQueueCheckIn amsStudentQueueCheckIn){....................

使用同步方法的方式解决了上面的问题,但是如果当前项目是在 k8s 集群上面部署,以分布式的方式,就需要考虑多个 Pod 的数据同步问题。

假设两个排队请求被负载到两个不同的 Pod,这个时候同时查询数据会获取相同的最大编号,生成相同的编号,考虑使用分布式锁。下面为对方法的改进,这里如果使用分布式锁的方式,那么上面的同步方法即可以去掉了,因为获取锁的方法是原子操作。

分布式锁实现很简单,就是进来一个线程先占位,当别的线城进来操作时,发现已经有人占位了,就会放弃或者稍后再试。这里的占位状态是全局的,相对整个集群而言,代码如下

        String token = UUID.randomUUID().toString();// 添加分布式锁if (redisCache.tryAcquireLock("checkInQueue", token, 2, 10)){AmsStudentQueueCheckIn amsStudentQueueCheckIns = amsStudentQueueCheckInService.selectAmsStudentQueueCheckInListMax(amsStudentQueueCheckIn.getStudentGender());Long queuePosition = 0L;if (Objects.nonNull(amsStudentQueueCheckIns)){queuePosition = amsStudentQueueCheckIns.getQueuePosition();}amsStudentQueueCheckIn.setStudentStatus(1).setQueuePosition(queuePosition + 1L).setStudentUid(uid);int i = amsStudentQueueCheckInService.insertAmsStudentQueueCheckIn(amsStudentQueueCheckIn);// 释放分布式锁redisCache.unlock("checkInQueue", token);if (i != 1){return AjaxResult.error("排队预约失败!请重新填写");}}else {return AjaxResult.error("系统繁忙,请稍后提交!");}

tryAcquireLocktryLock 以及 unlock 的方法实现

public class RedisCache
{private static final Logger log = LoggerFactory.getLogger(RedisCache.class);private static final String REDIS_UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";@Autowiredpublic RedisTemplate redisTemplate;/*** 获取分布式锁** @param key* @param token* @param expireInSeconds 锁超时时间* @return*/public boolean tryLock(String key, String token, long expireInSeconds) {Boolean res = redisTemplate.opsForValue().setIfAbsent(key, token, expireInSeconds, TimeUnit.SECONDS);log.info("获取分布式锁:"+ key + ":" + token);return Objects.equals(res, true);}/*** 分布式锁 unlock,使用lua脚本保证事务** @param key* @param token lock时的token值,只有token一致才能解锁* @return*/public void unlock(String key, String token) {try {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(REDIS_UNLOCK_SCRIPT, Long.class);Long res = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), token);log.info("释放分布式锁:"+ key + ":" + token);if (!Objects.equals(res, 1L)) {log.warn("redis unlock wrong:key=[{}],token=[{}],res=[{}]", key, token, res);}} catch (Exception e) {log.error("redis unlock error:key=[{}],token=[{}]", key, token, e);}}/*** @param key* @param token* @param lockTimeout  锁的超时时间* @param acquireTimeout  获取锁的截止时间* @return*/public boolean tryAcquireLock(String key, String token, long lockTimeout, long acquireTimeout) {try {long end = System.currentTimeMillis() + acquireTimeout;while (System.currentTimeMillis() < end) {Boolean res = redisTemplate.opsForValue().setIfAbsent(key, token, lockTimeout, TimeUnit.MILLISECONDS);if (Boolean.TRUE.equals(res)) {log.info("获取分布式锁:"+ key + ":" + token);return true;}try {Thread.sleep(100);} catch (Exception e) {log.error("thread sleep error", e);Thread.currentThread().interrupt();}}} catch (Exception e) {log.error("try acquire lock error, ", e);}return false;}
}    

tryAcquireLocktryLock 都用于获取分布式锁,unlock 用于释放分布式锁,逻辑简单,这里不做说明,关注以下几点:

  • tryAcquireLocktryLock 的区别在于,前者在没有获取到锁之后会在限定的时间进行重复尝试获取,后者只尝试获取一次。
  • 防止业务代码在执行的时候抛出异常,每一个锁添加了一个超时时间,超时之后,锁会被自动释放,考虑获取锁和设置过期时间之间如果服务器突然挂掉了,这个时候锁被占用,无法及时得到释放,也会造成死锁所以,所以要保证这个操作是原子的,所以使用 Redis 提供的原子操作 setIfAbsent(检查指定的键是否存在,如果不存在则设置键值对)
  • 如果当前线程执行业务较耗时,超时时间会自动释放锁,其他线程会获取锁,当前线程执行完释放锁或释放到其他线程的锁,会出现混乱,所以需要锁相对线程唯一,自己的锁只能自己释放,使用 key+token 的机制
  • 使用 key+token 的机制,每次释放锁都要判断 value, 一致才释放,但是这样的话,要去查看锁的 value,比较 value 的值是否正确,释放锁, 多个操作不保证原子性,所以unlock 需要引入 lua脚本,Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令

上面的实现是最简单的 redis 实现分布式锁,如果要进一步增强分布式锁的可靠性和性能,可以考虑使用更复杂的方案,如 RedLock 算法(redis 集群)、基于 Redis 的 Pub/Sub 机制等。这些方案可以提供更强的分布式锁功能,并解决一些特殊情况下的竞态条件和故障恢复问题。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知,这是一个开源项目,如果你认可它,不要吝啬星星哦 😃


https://liruilong.blog.csdn.net/article/details/107076223

http://www.gxitsky.com/2022/02/12/SpringBoot-60-tomcat-nio/


© 2018-2024 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

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

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

相关文章

Flutter 开发3:创建第一个Flutter应用

Step 1: 安装Flutter 1.1 下载Flutter SDK 首先&#xff0c;你需要访问Flutter官方网站下载最新的Flutter SDK。选择适合你操作系统的安装包。 $ cd ~/development $ unzip ~/Downloads/flutter_macos_2.2.3-stable.zip1.2 更新环境变量 接下来&#xff0c;你需要将Flutter…

Git 介绍 与 配置

Git 介绍 Git是一个分布式版本控制系统&#xff0c;用于跟踪文件的更改和协作开发。它可以管理项目的版本历史记录&#xff0c;并允许多个开发者在同一时间进行并行开发。 解决上图产生的问题就出现了git 分布式版本控制系统 看下图 Git 配置 Git的基本配置包括用户名和电子邮…

人脸识别 FaceNet人脸识别(一种人脸识别与聚类的统一嵌入表示)

人脸识别 FaceNet人脸识别&#xff08;一种人脸识别与聚类的统一嵌入表示&#xff09; FaceNet的简介Facenet的实现思路训练部分 FaceNet的简介 Facenet的实现思路 import torch.nn as nndef conv_bn(inp, oup, stride 1):return nn.Sequential(nn.Conv2d(inp, oup, 3, stride…

Netty源码二:服务端创建NioEventLoopGroup

示例 还是拿之前启动源码的示例&#xff0c;来分析NioEventLoopGroup源码 NioEventLoopGroup构造函数 这里能看到会调到父类的MultiThread EventLoopGroup的构造方法 MultiThreadEventLoopGroup 这里我们能看到&#xff0c;如果传入的线程数目为0&#xff0c;那么就会设置2倍…

JDBC 结构优化2

JDBC 结构优化2 文章目录 JDBC 结构优化2结构优化2 - ATM系统(存,取,转,查)1 Service2 事务3 ThreadLocal4 事务的封装 结构优化2 - ATM系统(存,取,转,查) 1 Service 什么是业务? 代表用户完成的一个业务功能&#xff0c;可以由一个或多个DAO的调用组成。软件所提供的一个功…

[NCTF2019]Fake XML cookbook(特详解)

先试了一下弱口令&#xff0c;哈哈习惯了 查看页面源码发现xml function doLogin(){var username $("#username").val();var password $("#password").val();if(username "" || password ""){alert("Please enter the usern…

数据据库八之 视图、触发器、事务

【零】准备数据 【1】创建表 &#xff08;1&#xff09;部门表 d_id是部门的编号d_name是部门的名字 # 确保表不存在 drop table if exists department; # 创建表 create table department( d_id int auto_increment primary key, d_name varchar(6) )auto_increment 501 …

腾讯云部署vue+node项目

文章目录 一、安装宝塔二、vue项目部署三、node项目部署 前言: 关于项目部署,一开始也是找了很多资料,费了点时间,所以记录一下。希望能对各位有所帮助。 一、安装宝塔 1.首先在控制台,进入云服务器的终端界面 2.输入命令和密码获取权限,并且安装宝塔界面 yum install -y w…

如何把png改成jpg格式?分享5个好用的方法!

你是否经常遇到需要将PNG格式的图片转换成JPG格式的情况&#xff1f;无论是因为工作需要&#xff0c;还是为了适应不同的平台和设备&#xff0c;格式转换都是我们经常要面对的问题。那么&#xff0c;如何快速、简单地完成这个任务呢&#xff1f;今天&#xff0c;我们就来为你揭…

计算机网络——IP协议

前言 网络层的主要负责地址分配和路由选择,ip负责在网络中进行数据包的路由和传输。 IPv4报文组成&#xff08;了解&#xff09; IPv4首部&#xff1a;IPv4首部包含了用于路由和传输数据的控制信息&#xff0c;其长度为20个字节&#xff08;固定长度&#xff09;。 版本&#…

图灵之旅--ArrayList顺序表LinkedList链表栈Stack队列Queue

目录 线性表顺序表ArrayList简介ArrayList使用ArrayList的构造ArrayList常见操作ArrayList的遍历ArrayList的扩容机制利用ArrayList洗牌ArrayList的优缺点 链表链表的实现双向链表的实现 LinkedListLinkedList引入LinkedList的使用LinkedList的构造LinkedList的常用方法介绍Lin…

Power ModeII 插件的下载与使用-----idea

下载 Marketplace里面搜索下载即可 使用 下载后重启软件就可以用了 下面是一些关于Power ModeII &#xff0c;我的个性化设置截图 以及相关设置解释 插件或扩展的设置面板【用于给代码编辑器或集成开发环境&#xff08;IDE&#xff09;添加视觉效果】 主要设置 ENTER POWE…

Maven安装,学习笔记,详细整理maven的一些配置

Maven 1. 初识Maven 2. Maven概述 Maven模型介绍 Maven仓库介绍 Maven安装与配置 3. IDEA集成Maven 4. 依赖管理 01. Maven课程介绍 1.1 课程安排 学习完前端Web开发技术后&#xff0c;我们即将开始学习后端Web开发技术。做为一名Java开发工程师&#xff0c;后端 Web开发技术…

STM32——USART

一、通信 1.1通信是什么&#xff1b; 通信是将一个设备的数据发送到另一个设备中&#xff0c;从而实现硬件的扩展&#xff1b; 1.2通信的目的是什么&#xff1b; 实现硬件的扩展-在STM32中集成了很多功能&#xff0c;例如PWM输出&#xff0c;AD采集&#xff0c;定时器等&am…

数据结构和算法笔记5:堆和优先队列

今天来讲一下堆&#xff0c;在网上看到一个很好的文章&#xff0c;不过它实现堆是用Golang写的&#xff0c;我这里打算用C实现一下&#xff1a; Golang: Heap data structure 1. 基本概念 满二叉树&#xff08;二叉树每层节点都是满的&#xff09;&#xff1a; 完全二叉树&a…

STM32标准库——(5)EXTI外部中断

1.中断系统 中断&#xff1a;在主程序运行过程中&#xff0c;出现了特定的中断触发条件&#xff08;中断源&#xff09;&#xff0c;使得CPU暂停当前正在运行的程序&#xff0c;转而去处理中断程序&#xff0c;处理完成后又返回原来被暂停的位置继续运行 中断优先级&#xff…

【QT+QGIS跨平台编译】之十一:【libzip+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文章目录 一、libzip介绍二、文件下载三、文件分析四、pro文件五、编译实践一、libzip介绍 libzip是一个开源C库,用于读取,创建和修改zip文件。 libzip可以从数据缓冲区,文件或直接从其他zip归档文件直接复制的压缩数据中添加文件。在不关闭存档的情况下所做的更改可以还原…

uniapp微信小程序-input默认字的样式

需要的是这样的 问题 正常是在input框上面写样式就行&#xff0c;但是uniapp不起作用 解决 直接在input上写placeholder-style"color就解决了 <input class"findInput" type"text" placeholder"关键词查询"placeholder-style"co…

Phoncent博客,探索Rie Kudan的GPT创作之举

近日&#xff0c;大家都在谈论日本作家Rie Kudan&#xff0c;她凭借其小说《东京共鸣塔》&#xff08;"Tokyo-to Dojo-to"&#xff09;荣获了日本极具声望的芥川奖。这本小说引起了广泛的讨论和思考&#xff0c;因为令人惊讶的是&#xff0c;Kudan在其中直接引用了人…

2023美赛A题之Lotka-Volterra【完整思路+代码】

这是2023年的成功&#xff0c;考虑到曾经付费用户的负责&#xff0c;2024年可以发出来了。去年我辅导队伍数量&#xff1a;15&#xff0c;获奖M为主&#xff0c;个别F&#xff0c;H&#xff0c;零S。言归正传&#xff0c;这里我开始分享去年的方案。由于时间久远&#xff0c;我…