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集群保证高可用和高并发特性(本文未涉及)

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

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

相关文章

闲鱼商品详情API:深入解析与应用指南

在二手交易市场中&#xff0c;阿里巴巴集团旗下的闲鱼平台以其社区氛围和交易模式&#xff0c;吸引了大量用户。为了进一步丰富用户体验和提升交易透明度&#xff0c;开放了一系列API接口&#xff0c;其中包括商品详情API 闲鱼商品详情API接口概述 闲鱼商品详情API接口允许用…

自然语言处理(NLP)技术。

自然语言处理&#xff08;NLP&#xff09;技术可以应用于各种不同的场景和应用程序。以下是一些例子&#xff1a; 1. 机器翻译&#xff1a;NLP技术可以帮助将一种语言翻译成另一种语言。例如&#xff0c;Google Translate使用NLP技术来实现在不同语言之间进行翻译。 2. 文本分…

express+vue在线im实现【三】

往期内容 expressvue在线im实现【一】 expressvue在线im实现【二】 本期示例 本期总结 支持各种类型的文件上传&#xff0c;常见文件类型图片&#xff0c;音频&#xff0c;视频等&#xff0c;上传时同步获取音频与视频的时长&#xff0c;以及使用上传文件的缓存路径来作为vi…

WDF驱动开发-DMA(一)

在 Windows 7 及更早版本上&#xff0c;Kernel-Mode Driver Framework (KMDF) 仅支持 (DMA) 设备的总线-主直接内存访问。 此类设备包含其自己的 DMA 控制器。 在片上系统 (SoC) 上运行Windows 8及更高版本的平台上&#xff0c;该框架还支持系统模式 DMA&#xff0c;其中多个设…

视频讲解|基于模型预测算法的含储能微网双层能量管理模型【mpc】

1 主要内容 该讲解视频对应的免费程序链接为【防骗贴】基于模型预测算法的含储能微网双层能量管理模型&#xff0c;主要做的是一个微网双层优化调度模型&#xff0c;微网聚合单元包括风电、光伏、储能以及超级电容器&#xff0c;在微网的运行成本层面考虑了电池的退化成本&…

快捷方式(lnk)--加载HTA-CS上线

免责声明:本文仅做技术交流与学习... 目录 CS: HTA文档 文件托管 借助mshta.exe突破 本地生成lnk快捷方式: 非系统图标路径不同问题: 关于lnk的上线问题: CS: HTA文档 配置监听器 有效载荷---->HTA文档--->选择监听器--->选择powershell模式----> 默认生成一…

政务大厅引导系统:AR、VR技术革新引领政务服务体验升级

一、传统政务大厅面临的普遍痛点 随着城市的发展和政务服务需求的增长&#xff0c;传统的政务大厅面临着诸多挑战和痛点&#xff1a; 信息不对称&#xff1a;政务大厅内各部门信息分散&#xff0c;群众难以快速获取全面准确的服务信息&#xff0c;导致办事效率低下。 办事流…

ES6 新增Set 和 Map 两种数据结构

ES6 新增了 Set 和 Map 这两种数据结构&#xff0c;它们为 JavaScript 提供了更强大和灵活的数据处理能力。下面详细介绍一下 Set 和 Map 的特性和用法&#xff1a; Set Set 是一种类似于数组的数据结构&#xff0c;但是成员的值都是唯一的&#xff0c;没有重复的值。 特性&…

4字节十进制数 转为 IPV4点分十进制 -- C++语言实现

4字节十进制数 转为 IPV4点分十进制 – C语言实现 //IPv4表示&#xff0c;通过4字节整数&#xff0c;比如1 6777 2418(0x0A 00 01 02)&#xff0c;表示10.0.1.2 // 4字节 4 * 1字节 4 * 8bit 32 bit // 2 ^32 1024 * 1024 *1024 * 4 42 9496 7296 0xff ff ff ff 1 //写…

计算机视觉 | 基于图像处理和边缘检测算法的黄豆计数实验

目录 一、实验原理二、实验步骤1. 图像读取与预处理2. 边缘检测3. 轮廓检测4. 标记轮廓序号 三、实验结果 Hi&#xff0c;大家好&#xff0c;我是半亩花海。 本实验旨在利用 Python 和 OpenCV 库&#xff0c;通过图像处理和边缘检测算法实现黄豆图像的自动识别和计数&#xff0…

ICMPV6协议

ICMPV6&#xff1a;intermet 控制管理协议--- 存在大量的子协议 1、PMTU---路径 MTU 发现协议 通过ICMPV6error包来获取整段路径上最小 MTU 值 2、NDP------邻居发现协议---用于取代IPV4下的 ARP协议 假设PC1和PC2 通讯 1无2的MAC地址 …

JetBrains GoLand 2024 mac/win版:高效开发,Go无止境

JetBrains GoLand 2024是一款专为Go语言开发者设计的集成开发环境(IDE)&#xff0c;为开发者带来了更加高效、智能和便捷的编程体验。 GoLand 2024 mac/win版获取 在代码编辑方面&#xff0c;GoLand 2024提供了全行代码补全功能&#xff0c;通过利用先进的深度学习模型&#x…

C#面:C#中的析构函数是什么?

在C#中&#xff0c;析构函数是一种特殊的方法&#xff0c;用于在对象被销毁之前执行一些清理操作。它的名称与类名相同&#xff0c;但在名称前面加上一个波浪线&#xff08;~&#xff09;。析构函数没有参数&#xff0c;也没有返回类型。 当对象不再被使用时&#xff0c;垃圾回…

力扣85.最大矩形

力扣85.最大矩形 遍历所有行作为底边 做求矩形面积&#xff08;84. class Solution {public:int maximalRectangle(vector<vector<char>>& matrix) {if (matrix.empty()) return 0;int n matrix.size(),m matrix[0].size();int res0;vector<int> li…

grub引导LinuxMint

注意事项&#xff1a;文件系统必须是FAT32 安装 sudo apt install gparted -y 分区管理软件 使用gparted分区和查看设备路径 sudo apt-get install grub-efi-amd64 #/dev/sdd1 是需要制作分区引导的设备路径 sudo mount /dev/sdd1 /mnt/123 #bios sudo grub-install --targe…

适耳贴合的气传导耳机,带来智能生活体验,塞那Z50耳夹耳机上手

现在大家几乎每天都会用到各种AI产品&#xff0c;蓝牙耳机也是我们必不可少的装备&#xff0c;最近我发现一款很好用的分体式气传导蓝牙耳机&#xff0c;它还带有一个具备AI功能的APP端&#xff0c;大大方便了我们日常的使用。这款sanag塞那Z50耳夹耳机我用过一段时间以后&…

interface Ref<T = any> 这是什么写法?为什么写接口还需要加上<T = any>

问: export interface Ref<T any> { value: T [RefSymbol]: true } 这里既然是interface接口,为什么还有<T any>这是什么意思? 回答: <T any> 中的 <T> 表示这是一个泛型参数&#xff0c;它可以在接口中作为类型的占位符&#xff0c;在实际…

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-19图像卷积

19图像卷积 #互相关运算 #在卷积层中&#xff0c;输入张量和核张量通过互相关运算产生输出张量。 import torch from torch import nn from d2l import torch as d2ldef corr2d(X, K):"""计算二维互相关运算"""h, w K.shapeY torch.zeros((X.…

开发指南033-数据库兼容

元芳&#xff0c;你怎么看&#xff1f; 单一数据库自身就有一些不同处理之处&#xff0c;如果一个平台要兼容所有数据库&#xff0c;就是难上加难&#xff0c;像isnull函数各数据库就不同。 对于这类问题&#xff0c;平台采用统一自定义函数解决&#xff0c;例如上面的round函…

一篇文章搞懂残差网络算法

残差网络(Residual Network,简称ResNet)是一种深度学习架构,它在2015年由微软研究院的Kaiming He等四位作者提出。ResNet的提出是为了解决深度神经网络训练中的梯度消失和梯度爆炸问题,以及随着网络层数增加而出现的性能退化问题。本文将详细介绍残差网络算法的定义、产生…