【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
一、什么是分布式锁
我们在上篇文章中实现了单机模式下的秒杀业务。其中采用了synchronized加锁来解决各种线程安全问题。而synchronized关键字是依赖于单机的JVM,在集群模式下,每个服务器都有独立的JVM,如果此时还采用synchronized关键字加锁,就会导致不同服务器间出现线程安全问题:
这时我们就需要一个不依赖JVM的独立的锁,去统一地对多台服务器进行加锁操作:
像这样独立出来的锁就被称之为分布式锁。
一个优秀的分布式锁需要满足以下特性:
- 多进程可见
- 互斥
- 高可用
- 高性能
- 安全性
- ……
二、分布式锁的实现
分布式锁的核心是实现多进程之间的互斥,常见的有三种实现方式:
相比之下redis在性能上是最好的,本篇文章就主要来探讨用redis来实现一个分布式锁。
Redis实现简单的分布式锁
这里的锁最重要的一个特点就是需要具备互斥性。redis中的setnx操作,在key不存在的时候可以写入,key如果存在就将无法写入,就很符合互斥性的特点。
这样我们就可以尝试用setnx操作来自己实现一个互斥锁SimpleRedisLock:
加锁时会执行setnx操作向Redis中添加锁信息。由于setnx的互斥性,只有第一个执行setnx操作的线程才能成功执行写入操作,并返回成功信息,则表示着加锁成功;此时其它线程尝试获取锁去执行setnx操作就会失败,返回错误信息,则表示加锁失败,进入阻塞等待。这样就实现了一个简单的锁的功能。
同时为了避免某个线程在获取锁后执行时间过长导致大量线程长时间处于阻塞态,甚至出现死锁问题。因此我们可以通过Redis的expire操作对锁添加过期时间
代码实现:
public class SimpleRedisLock implements ILock{//锁的名称private String name;//传入redis的方法private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name=name;this.stringRedisTemplate=stringRedisTemplate;}//锁的前缀private static final String KEY_PREFIX="lock:";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标识long threadId = Thread.currentThread().getId();//尝试获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);//判断获取锁是否成功return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//释放锁,即删除redis中对应的锁对象stringRedisTemplate.delete(KEY_PREFIX+name);}
}
运行流程:
接着就可以用它来代替synchronized为我们上篇文章中的代码进行加锁操作了:
Long userId = UserHolder.getUser().getId();//创建锁对象SimpleRedisLock lock=new SimpleRedisLock("order:"+userId,new StringRedisTemplate());//尝试获取锁boolean isLock = lock.tryLock(1200);//判断获取锁是否成功if(!isLock){//获取锁失败,返回错误或重试return Result.fail("不许重复下单!");}try{//获取当前对象的代理对象IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}finally {lock.unlock();}
误删问题
但是上述实现的锁在一些情况下是存在问题的:
由于我们为分布式锁设置了过期时间,很有可能会出现 线程一 业务还未执行完二锁却因为超时而被释放的情况。由于锁被释放,这时其它线程比如 线程二 就会抢到锁,并在Redis中存储上新的锁标识,开始执行自己的业务逻辑。期间 线程一 可能才执行完业务逻辑,并按照流程进行释放锁的操作,此时它并不知道Redis中存储的锁已经不是自己那把了,依旧会删除掉Redis中的锁。而此时 线程二 的业务也没有完成,但锁却被删掉了,此时其它线程如 线程三 又会抢到锁,也有可能会被 线程二 误删掉锁,以此类推。
这种现象就被称之为误删问题。
我们可以通过在对锁进行删除操作前加上判断,判断此时Redis中存储的锁标识是否与自己的锁标识一致。若一致,则正常执行删除操作;若不一致,则不进行删除。
这样就可以对代码进行改进:
在加锁时存入线程标识。由于不同服务器的线程id可能会发生重复,所以我们可以生成随机的UUID作为前缀,保证线程标识的唯一性
private static final String ID_PREFIX= UUID.randomUUID().toString(true) +"-";@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX+Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}
在释放锁前,先获取锁中线程标识,判断与当前线程标识是否一致,若一致,才执行释放锁操作
@Overridepublic void unlock() {// 获取当前线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取Redis中记录的锁标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断线程标识与锁标识是否一致if(threadId.equals(id)){// 一致,成功释放锁stringRedisTemplate.delete(KEY_PREFIX+name);}}
原子性问题
由于判断锁标识与释放锁并不是原子的,可能在判断完锁标识后由于JVM的垃圾回收机制等原因陷入阻塞,期间就可能会被其他线程获取锁,而后就会导致误删:
一看到原子性可能就会想到事务,与数据库的事务不同,redis的事务可以保证原子性,但无法保证数据的一致性。事务中的多个操作其实是在做批处理,是在最终一次性去执行的。没有办法先查询,然后判断再去释放。因为做查询操作时是拿不到结果的,它是最后一次性执行的,因此无法将它们放在同一个事务中,只能用redis中的一些乐观锁进行一些判断,确保在释放时没有人做过修改
我们可以用Lua编写脚本,在脚本中编写多条命令,确保多条命令执行时的原子性。
-- 获取锁中的线程标识
local id =redis.call('get',KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if(id==ARGV[i]) then-- 释放锁 del keyreturn redis.call('del',KEYS[1])
end
return 0
接下来就可以在释放锁的方法中直接调用写好的Lua脚本,将原本的多条命令变成只有一条了,判断和删除都是在脚本中执行的,是能够满足原子性的:
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;//初始化脚本static {UNLOCK_SCRIPT=new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic void unlock() {// 调用Lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX+Thread.currentThread().getId());}
这种方式就是利用了lua脚本的原子性,在里面依次执行多个redis的命令,这就能保证原子性
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊
相关文章:

【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
一、什么是分布式锁 我们在上篇文章中实现了单机模式下的秒杀业务。其中采用了synchronized加锁来解决各种线程安全问题。而synchronized关键字是依赖于单机的JVM,在集群模式下,每个服务器都有独立的JVM,如果此时还采用synchronized关键字加…...

用docker快速安装电子白板Excalidraw绘制流程图
注:本文操作以debian12.8 最小化安装环境为host系统。 一、彻底卸载原有的残留 apt-get purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras 二、设置docker的安装源 # Add Dockers official G…...

使用Turtle库实现,鼠标左键绘制路径,用鼠标右键结束绘制,小海龟并沿路径移动
使用Turtle库实现,鼠标左键绘制路径,用鼠标右键结束绘制,小海龟并沿路径移动 Turtle库是Python标准库的一部分,它提供了一种基于命令的图形绘制方式。Turtle模块通过一个“海龟”(Turtle)对象在屏幕上移动…...

人工智能入门是先看西瓜书还是先看花书?
在人工智能入门时,关于先看《机器学习》(西瓜书)还是先看《深度学习》(花书)的问题,实际上取决于个人的学习目标和背景。 《机器学习》(西瓜书)由周志华教授撰写,是一本…...
winform中屏蔽双击最大化或最小化窗体(C#实现),禁用任务管理器结束程序,在需要屏蔽双击窗体最大化、最小化、关闭
winform中屏蔽双击最大化或最小化窗体(C#实现),禁用任务管理器结束程序,在需要屏蔽双击窗体最大化、最小化、关闭 protected override void WndProc(ref Message m){#region 处理点击窗体标题栏放大缩小问题,禁用点击窗体标题栏放大缩小//logger.Info($&…...

进程内存转储工具|内存镜像提取-取证工具
1.内存转储,内存转储(Memory Dump)是将计算机的物理内存(RAM)内容复制到一个文件中的过程,这个文件通常被称为“内存转储文件”或“核心转储文件”(Core Dump),内存转储的主要目的是…...

数据结构day5:单向循环链表 代码作业
一、loopLink.h #ifndef __LOOPLINK_H__ #define __LOOPLINK_H__#include <stdio.h> #include <stdlib.h>typedef int DataType;typedef struct node {union{int len;DataType data;};struct node* next; }loopLink, *loopLinkPtr;//创建 loopLinkPtr create();//…...

(OCPP服务器)SteVe编译搭建全过程
注意:建议使用3.6.0,我升级到3.7.1,并没有多什么新功能,反而电表的实时数据只能看到累计电能了,我回退了就正常,数据库是兼容的,java版本换位java11,其他不变就好 背景:…...

Mybatis分页插件的使用问题记录
项目中配置的分页插件依赖为 <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.1.7</version></dependency>之前的项目代码编写分页的方式为,通过传入的条件…...

36. Three.js案例-创建带光照和阴影的球体与平面
36. Three.js案例-创建带光照和阴影的球体与平面 实现效果 知识点 Three.js基础 WebGLRenderer WebGLRenderer 是Three.js中最常用的渲染器,用于将场景渲染到网页上。 构造器 new THREE.WebGLRenderer(parameters)参数类型描述parametersobject可选参数&#…...

CentOS 7 安装、测试和部署FastDFS
目录 FastDFS环境搭建 安装 libfastcommon 库 安装FastDFS 查看编译后的文件 FastDFS配置 FastDFS启动 启动tracker服务 启动storage服务 查看storage是否已经注册到了tracker下 查看存储文件的目录 FastDFS重启 FastDFS关闭 使用fdfs_test进行测试 修改client.co…...

全志H618 Android12修改doucmentsui选中图片资源详情信息
背景: 由于当前的文件管理器在我们的产品定义当中,某些界面有改动的需求,所以需要在Android12 rom中进行定制以符合当前产品定义。 需求: 进入file文件管理器后,点击选中图片资源,选中功能按钮,获取信息,不显示“调试信息(仅开发者)”;现状是,获取信息,显示“调试信…...
【083】基于51单片机智能烘手器【Proteus仿真+Keil程序+报告+原理图】
☆、设计硬件组成:51单片机最小系统LCD1602液晶显示DS18B20温度传感器TCRT5000红外感应传感器AT24C02存储芯片风扇加热片继电器LED灯按键设置。 1、设计采用STC89C51/52、AT89C51/52、AT89S51/52作为主控芯片; 2、系统采用DS18B20温度传感器感应当前环…...

uniApp使用腾讯地图提示未添加maps模块
uniApp使用腾讯地图,打包提示未添加maps模块解决方案 这是报错信息,在标准基座运行的时候是没问题的,但是打包后会提示未添加,可以通过在mainfest里面把地图插件上腾讯地图的key更换高德地图的key,定位服务可以继续用腾…...
未来趋势系列 篇五:自主可控科技题材解析和股票梳理
文章目录 系列文章自主可控科技题材分析国产算力信创(信息技术应用创新)华为鸿蒙军工信息化半导体芯片卫星互联网工业软件股票梳理系列文章 未来趋势系列 篇一:AI题材解析和股票梳理 未来趋势系列 篇一(加更):AI医疗题材解析和股票梳理 未来趋势系列 篇二:HBM题材解析和…...
Springboot 学习 之 logback-spring.xml 日志压缩 .tmp 临时文件问题
文章目录 前言功能简述1. 自定义日志文件名2. 归档规则 && 压缩2.1. 归档配置2.2. 归档压缩2.3. 日志格式 && 编码 现象原因解决办法 前言 在 Springboot 应用中,默认使用 logback-spring.xml 配置日志相关 功能简述 1. 自定义日志文件名 <fi…...

maven-resources-production:ratel-fast: java.lang.IndexOutOfBoundsException
Maven生产环境中遇到java.lang.IndexOutOfBoundsException的问题,尝试了重启电脑、重启IDEA等常规方法无效,最终通过直接重建工程解决了问题。 Rebuild Project 再启动OK...
K8s docker-compose的入门
一、Docker Compose 简介 什么是 Docker Compose? 用于定义和运行多容器 Docker 应用的工具。Docker Compose 的主要功能 使用 docker-compose.yml 文件定义服务。一键启动和管理多容器环境。安装与环境准备 安装 Docker 和 Docker Compose。检查版本:d…...

去雾Cycle-GAN损失函数
文章目录 GAN-LossIdentity-LossDP-lossCycle-Loss G和F都是生成器 G是hazy → \to → gt F是gt → \to → hazy D y D_y Dy判别无雾图是真实还是生成的? D x D_x Dx判别有雾图是真实还是生成的? GAN-Loss 在 DAM-CCGAN 中存在两个判别器 D x D_x D…...

word实现两栏格式公式居中,编号右对齐
1、确定分栏的宽度 选定一段文字 点击分栏:如本文的宽度为22.08字符 2、将公式设置为 两端对齐,首行无缩进。 将光标放在 公式前面 点击 格式-->段落-->制表位 在“制表位位置”输入-->11.04字符(22.08/211.04字符)&…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器
拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件: 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...
深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向
在人工智能技术呈指数级发展的当下,大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性,吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型,成为释放其巨大潜力的关键所在&…...

自然语言处理——文本分类
文本分类 传统机器学习方法文本表示向量空间模型 特征选择文档频率互信息信息增益(IG) 分类器设计贝叶斯理论:线性判别函数 文本分类性能评估P-R曲线ROC曲线 将文本文档或句子分类为预定义的类或类别, 有单标签多类别文本分类和多…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...

NineData数据库DevOps功能全面支持百度智能云向量数据库 VectorDB,助力企业 AI 应用高效落地
NineData 的数据库 DevOps 解决方案已完成对百度智能云向量数据库 VectorDB 的全链路适配,成为国内首批提供 VectorDB 原生操作能力的服务商。此次合作聚焦 AI 开发核心场景,通过标准化 SQL 工作台与细粒度权限管控两大能力,助力企业安全高效…...