当前位置: 首页 > news >正文

Redis实战—Redis分布式锁

 本博客为个人学习笔记,学习网站与详细见:黑马程序员Redis入门到实战 P56 - P63

目录

分布式锁介绍

基于Redis的分布式锁

Redis锁代码实现

修改业务代码 

分布式锁误删问题

分布式锁原子性问题 

Lua脚本

编写脚本 

代码优化

总结 


分布式锁介绍

        在上一篇文章 Redis实战—优惠卷秒杀 中,我们通过使用锁、事务和代理对象实现了“一人一单”的优惠券秒杀功能。但我们使用的锁是基于JVM内部的锁,这导致锁的范围只能限制单个JVM的线程操作,因此在集群情况下,依然会出现超卖问题。所以我们需要设置一个锁,使其能够同时限制集群中的多个JVM线程操作,而这个锁就是分布式锁,由此引出本文。

集群情况下JVM锁的使用情况如下图。

 集群情况下分布式锁的使用情况如下图。

 分布式锁的实现


基于Redis的分布式锁


        我们利用Redis的SET lock thread1 NX操作来模拟获取锁,即如果当前不存在lock键,则添加lock键成功,如果当前存在lock键,则添加lock键失败。我们将添加lock键的操作视为获取锁的操作,将lock键是否存在视为当前锁是否已被其他线程获取。执行语句后,通过Redis返回OK或者nil,我们可以判断是否获取锁成功。为防止宕机时无法对锁进行销毁,我们在进行SET操作时还需通过EX为键设置一个合理的时间。


Redis锁代码实现

// 接口类
public interface ILock {/** 尝试获取锁* timeoutSec 锁持有的超时时间,过期后自动释放* 返回值 true代表获取锁成功;false代表获取锁失败* */boolean tryLock(long timeoutSec);//释放锁void unlock();}// 接口实现类
public class SimpleRedisLock implements ILock {private String name;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);//避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

修改业务代码 

    public Result seckillVoucher(Long voucherId) {//判断是否满足抢购条件...Long userId = UserHolder.getUser().getId();// 创建锁对象,根据用户ID加锁SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);// 获取锁boolean isLock = lock.tryLock(1200);// 若获取锁失败if (!isLock)return Result.fail("不允许重复下单");// 若获取锁成功try {// 获取当前代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {lock.unlock();}}

分布式锁误删问题

        如上图所示,持有锁的线程1在锁的内部出现了业务阻塞,导致它的锁被超时释放。这时线程2尝试获得锁成功,然而在线程2持有锁执行过程中,线程1的业务反应过来,继续执行,而线程1业务执行完成后,进行了删除锁逻辑,此时就会把本应属于线程2的锁进行删除,这就是误删其它线程锁的情况。 


        解决方案:当线程创建锁时,同时为该锁添加当前线程标识,该标识由UUID随机数为前缀与线程id组合而成(为避免出现集群下两个线程的id相同的情况,因此添加UUID前缀)。当一个线程删除锁时,需要判断当前线程标识与锁标识是否一致,若一致,说明该锁由当前线程创建,可进行删除;若不一致,说明该锁由其它线程创建,不可进行删除。

        对simpleRedisLock类代码优化如下。

package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";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);//避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁标识String lockID = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标识是否一致if(threadId.equals(lockID))stringRedisTemplate.delete(KEY_PREFIX + name);}
}

分布式锁原子性问题 

        如上图所示,线程1执行业务结束后,进行释放锁的操作,在对锁的标识进行判断后,开始释放锁。但是,线程1在"判断结束"到"释放锁"的期间,受到了阻塞(遇到JVM垃圾回收机制时会暂停程序,导致阻塞),这时线程2获取锁。当线程1恢复后,继续进行释放锁的操作,将会误删线程2的锁。我们前面设置了锁标识,并且要求在释放锁之前需要做一个判断,但在判断可以释放锁后,如果遇到了阻塞,将可能导致上图所示的误删操作。

        解决方法:我们需要实现"判断"和"释放锁"这两条命令的原子性问题。


Lua脚本

        Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,能够确保多条命令执行时的原子性。Lua是一种编程语言,其基本语法可以参考网站:Lua 教程 | 菜鸟教程。这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,以保证多条redis命令的原子性,这样就可以实现拿锁、判断、删锁多条命令的原子性动作了,作为一名Java程序员这一块并不需要大家过于精通,只需要知道它有什么作用即可。


编写脚本 

        我们需要在resources文件中新建.lua文件(如果没有该新建项,需要下载EmmyLua插件),并在其中添加下图中的脚本内容。


代码优化

优化后的代码如下。

public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;//初始化UNLOCK_SCRIPTstatic {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//初始化返回值UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁,并设置锁标识、添加时间Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,//要求传入KEYS集合,使用Collections单元素集合工具Collections.singletonList(KEY_PREFIX + name),//线程标识ID_PREFIX + Thread.currentThread().getId());}/*  @Overridepublic void unlock() {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁标识String lockID = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标识是否一致if(threadId.equals(lockID))stringRedisTemplate.delete(KEY_PREFIX + name);}*/
}

总结 

基于Redis的分布式锁实现思路
· 利用set nxex获取锁,并设置过期时间,保存线程标识
· 释放锁时先判断线程标识是否与锁标识一致,若一致则删除锁

特性
· 利用set nx满足互斥性
· 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
· 利用redis集群保证高可用和高并发特性(本文未涉及)

相关文章:

Redis实战—Redis分布式锁

本博客为个人学习笔记&#xff0c;学习网站与详细见&#xff1a;黑马程序员Redis入门到实战 P56 - P63 目录 分布式锁介绍 基于Redis的分布式锁 Redis锁代码实现 修改业务代码 分布式锁误删问题 分布式锁原子性问题 Lua脚本 编写脚本 代码优化 总结 分布式锁介绍…...

联想Y7000P 2023款拆机教程及升级内存教程

0.电脑参数介绍 联想Y7000P 2023电脑&#xff0c;笔者电脑CPU为i7-13700H&#xff0c;14核20线程&#xff1b;标配内存为三星的DDR5-5600MHz-8GB*2&#xff0c;由于电脑CPU限制&#xff0c;实际内存跑的频率为5200MHz; 2个内存插槽&#xff0c;2个固态硬盘插槽。每个内存插槽最…...

开发常用依赖

目录 代理对象 Swagger Web 单元测试 MybatisPlus Lombok Mysql SpringBoot Jdk SpringCloud 数据库驱动包 hutool工具 配置仓库 通用库 maven插件 nacos注册中心 OpenFeign Spring AMQP JSON转换器 Redis 邮箱验证 Redisson分布式锁 客户端 代理对象 &l…...

【区分vue2和vue3下的element UI Empty 空状态组件,分别详细介绍属性,事件,方法如何使用,并举例】

在 Element UI&#xff08;为 Vue 2 设计&#xff09;和 Element Plus&#xff08;为 Vue 3 设计&#xff09;中&#xff0c;Empty&#xff08;空状态&#xff09;组件通常用于在数据为空或没有内容时向用户展示一种占位提示。然而&#xff0c;需要注意的是&#xff0c;Element…...

【AI作曲】毁掉音乐?早该来了!一个网易音乐人对于 AI 大模型音乐创作的思辨

引言&#xff1a;AI在创造还是毁掉音乐&#xff1f; 正如当初 midjourney 和 StableDiffusion 在绘画圈掀起的风波一样&#xff0c;suno 和 各大音乐大模型的来临&#xff0c;其实早该来了。 AI 在毁掉绘画&#xff1f;或者毁掉音乐&#xff1f; 没错&#xff0c;但也错了。…...

RabbitMQ实践——最大长度队列

大纲 抛弃消息创建最大长度队列绑定实验 转存死信创建死信队列创建可重写Routing key的最大长度队列创建绑定关系实验 在一些业务场景中&#xff0c;我们只需要保存最近的若干条消息&#xff0c;这个时候我们就可以使用“最大长度队列”来满足这个需求。该队列在收到消息后&…...

【pytorch02】手写数字问题引入

1.数据集 现实生活中遇到的问题 车牌识别身份证号码识别快递单的识别 都会涉及到数字识别 MNIST&#xff08;收集了很多人手写的0到9数字的图片&#xff09; 每个数字拥有7000个图像train/test splitting:60k vs 10k 图片大小28 28 数据集划分成训练集和测试集合的意义…...

【查看显卡信息】——Ubuntu和windows

1、VMware虚拟机 VMware虚拟机上不能使用CUDA/CUDNN&#xff0c;也安装不了显卡驱动 查看显卡信息&#xff1a; lspci | grep -i vga 不会显示显卡信息&#xff0c;只会输出VMware SVGA II Adapter&#xff0c;表示这是一个虚拟机&#xff0c;无法安装和使用显卡驱动 使用上…...

在 RK3568 上构建 Android 11 模块:深入解析 m、mm、mmm 编译命令

目录 Android 编译系统概述编译命令简介 环境准备使用 m、mm、mmm 编译模块编译整个源码树编译单个模块编译指定目录下的模块 高级应用并行编译清理编译结果编译特定配置 在 Android 开发中&#xff0c;特别是在 RK3568 这样的高性能平台上&#xff0c;有效地编译和管理模块是确…...

实战|YOLOv10 自定义目标检测

引言 YOLOv10[1] 概述和使用自定义数据训练模型 概述 由清华大学的研究团队基于 Ultralytics Python 包研发的 YOLOv10&#xff0c;通过优化模型结构并去除非极大值抑制&#xff08;NMS&#xff09;环节&#xff0c;提出了一种创新的实时目标检测技术。这些改进不仅实现了行业领…...

TTS前端原理学习 chatgpt生成答案

第一篇文章学习 小绿鲸阅读器 通篇使用chatgpt生成答案 文章&#xff1a; https://arxiv.org/pdf/2012.15404 1. 文章概述 本文提出了一种基于Distilled BERT模型的统一普通话文本到语音前端模块。该模型通过预训练的中文BERT作为文本编码器&#xff0c;并采用多任务学习技术…...

AI“音乐创作”横行给音乐家带来哪些隐忧

​​​​​​​近日&#xff0c;200多名国际乐坛知名音乐人联署公开信&#xff0c;呼吁AI开发者、科技公司、平台和数字音乐服务商停止使用人工智能(AI)来侵犯并贬低人类艺术家的权利&#xff0c;具体诉求包括&#xff0c;停止使用AI侵犯及贬低人类艺术家的权利&#xff0c;要求…...

SolidityFoundry 安全审计测试 Delegatecall漏洞2

名称&#xff1a; Delegatecall漏洞2 https://github.com/XuHugo/solidityproject/tree/master/vulnerable-defi 描述&#xff1a; 我们已经了解了delegatecall 一个基础的漏洞——所有者操纵漏洞&#xff0c;这里就不再重复之前的基础知识了&#xff0c;不了解或者遗忘的可…...

【字符串 状态机动态规划】1320. 二指输入的的最小距离

本文涉及知识点 动态规划汇总 字符串 状态机动态规划 LeetCode1320. 二指输入的的最小距离 二指输入法定制键盘在 X-Y 平面上的布局如上图所示&#xff0c;其中每个大写英文字母都位于某个坐标处。 例如字母 A 位于坐标 (0,0)&#xff0c;字母 B 位于坐标 (0,1)&#xff0…...

2024.06.23【读书笔记】丨生物信息学与功能基因组学(第十七章 人类基因组 第三部分)【AI测试版】

第三部分:人类基因组的深入分析与比较基因组学 摘要: 本部分基于2001年国际人类基因组测序联盟(IHGSC)发布的人类基因组测序及分析草图,从生物信息学角度深入讨论了人类基因组的结构特征和分析方法。同时,提及了塞莱拉公司(Celera Genomics)版本的人类基因组草图及其…...

外观模式(大话设计模式)C/C++版本

外观模式 C #include <iostream> using namespace std;class stock1 { public:void Sell(){cout << "股票1卖出" << endl;}void Buy(){cout << "股票1买入" << endl;} };class stock2 { public:void Sell(){cout << …...

PHP木马原文

攻击者留下的源码 <?php $ZimXb strre.v; $SkYID ba.se64._d.eco.de; $qetGk g.zuncomp.ress; ini_set(display_errors, 0); ini_set(log_errors, 0); /*** 13f382ef7053c327e26dff2a9c14affbd9e8296a ***/ error_reporting(0); eval($qetGk($SkYID($ZimXb(Q2WA…...

湖南(市场调研)源点咨询 新产品上市前市场机会调研与研究分析

湖南源点调研认为&#xff1a;无论是创业公司&#xff0c;还是在公司内部探索新的项目或者新的产品线等&#xff0c;首先都要做“市场机会分析与调研“&#xff0c;要真正思考并解答以下疑问&#xff1a; 我们的目标客户群体是谁&#xff0c;他们如何决策&#xff1f; 我们所…...

Vue82-组件内路由守卫

一、组件内路由守卫的定义 在一个组件里面去写路由守卫&#xff0c;而不是在路由配置文件index.js中去写。 此时&#xff0c;该路由守卫是改组件所独有的&#xff01; 只有通过路由规则进入的方式&#xff0c;才会调这两个函数&#xff0c;否则&#xff0c;若是只是用<Ab…...

使用ESP32和Flask框架实现温湿度数据监测系统

项目概述 在这个项目中&#xff0c;我们将使用ESP32微控制器读取温湿度传感器的数据&#xff0c;并将这些数据通过HTTP请求传输到基于Flask框架的服务器。Flask是一个轻量级的Python Web框架&#xff0c;非常适合快速开发和部署Web应用。通过这个项目&#xff0c;我们不仅可以了…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

2024年赣州旅游投资集团社会招聘笔试真

2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

Docker 本地安装 mysql 数据库

Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker &#xff1b;并安装。 基础操作不再赘述。 打开 macOS 终端&#xff0c;开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

SQL慢可能是触发了ring buffer

简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

iview框架主题色的应用

1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题&#xff0c;无需引入&#xff0c;直接可…...