Redis实战—基于setnx的分布式锁与Redisson

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

目录

分布式锁介绍

基于SETNX的分布式锁

SETNX锁代码实现

修改业务代码 

SETNX锁误删问题

SETNX锁原子性问题 

Lua脚本

编写脚本 

代码优化

总结 

Redisson

前言

介绍

Redisson快速入门 

原理介绍

Redission可重入锁原理

Redission锁重试和WatchDog机制


分布式锁介绍

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

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

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

 分布式锁的实现


基于SETNX的分布式锁


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


SETNX锁代码实现

// 接口类
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();}}

SETNX锁误删问题

        如上图所示,持有锁的线程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);}
}

SETNX锁原子性问题 

        如上图所示,线程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集群保证高可用和高并发特性(本文未涉及)


Redisson

前言

基于SETNX实现的分布式锁存在以下的问题:

重入问题:重入问题是指获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable的代码中,方法都是使用synchronized修饰的。假如它在一个方法内,调用另一个方法,如果此时是不可重入的,就会导致死锁。所以可重入锁的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

不可重试:是指目前的分布式只能尝试一次获取锁,我们认为合理的情况是:当线程在获得锁失败后,它应该能再次尝试获取锁。

超时释放:我们在加锁时添加了过期时间,目的是防止死锁,但如果阻塞的时间超长,尽管我们采用了lua表达式防止误删其它锁,但因为阻塞,锁被超时释放,没有锁住,依然有安全隐患。

主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,将会导致死锁问题。


介绍

        Redisson是一个在Redis基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。


Redisson快速入门 

一、引入依赖

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

二、配置Redisson客户端

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");// 创建RedissonClient对象return Redisson.create(config);}
}

三、基本使用方法

@Resource
private RedissionClient redissonClient;@Test
void testRedisson() throws Exception{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位//若无参数,默认为-1,30s 即不重试获取锁,自动释放时间为30sboolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);//判断获取锁成功if(isLock){try{System.out.println("执行业务");          }finally{//释放锁lock.unlock();}}    
}

原理介绍

 原理篇章详细见:黑马程序员Redis入门到实战 P66 - P68 (深入学习建议看视频)

Redission可重入锁原理

        在Lock锁中,它是借助于底层的一个voaltile的一个state变量来记录重入状态的,比如当前没有线程持有这把锁,那么state=0,假如有线程持有这把锁,那么state=1。如果持有这把锁的线程再次持有这把锁,那么state就会+1 。如果是对于synchronized而言,它在c语言代码中会有一个count,其原理和state类似,也是重入一次就+1,释放一次就-1 ,直到值为0 时,表示当前这把锁没有被任何线程持有。
        而Redission也支持可重入锁,其底层采用lua脚本实现,原理与上述内容相似,它采用hash结构来存储锁,其中大key表示当前这把锁被哪个线程持有,小key表示这把锁是否存在,如果持有这把锁的线程再次持有这把锁,那么小key就会+1。


Redission锁重试和WatchDog机制

很复杂,建议多看几遍视频

Redisson分布式锁原理
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间


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

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

相关文章

基于STM32的温湿度检测TFT屏幕proteus恒温控制仿真系统

一、引言 本文介绍了一个基于STM32的恒温控制箱检测系统&#xff0c;该系统通过DHT11温湿度传感器采集环境中的温湿度数据&#xff0c;并利用TFT LCD屏幕进行实时显示。通过按键切换页面显示&#xff0c;通过按键切换实现恒温控制箱的恒温控制。为了验证系统的可靠性和稳定性&…

png格式快速压缩该怎么做?在电脑压缩png图片的方法

png格式的图片如何快速压缩变小呢&#xff1f;现在网络的不断发展&#xff0c;图片是日常用来分享展示内容的一种常用手段&#xff0c;其中使用最多的一种图片格式就是png&#xff0c;png格式具有无损压缩支持透明底的特性&#xff0c;在很多的场景下都会使用。 现在图片的清晰…

Arduino - 线性执行器(支撑或滑杆)

Arduino - 线性执行器(支撑或滑杆) Arduino - 执行器 In this tutorial, we are going to learn: 在本教程中&#xff0c;我们将学习&#xff1a; How linear actuator works 线性执行器的工作原理How to make linear actuator extend or retract. 如何使线性执行器伸展或缩…

百元蓝牙耳机哪款性价比高?盘点性价比高的百元蓝牙耳机品牌

在如今快节奏的生活中&#xff0c;蓝牙耳机已经成为人们日常生活中不可或缺的配件。然而&#xff0c;市面上百元左右性价比高的蓝牙耳机琳琅满目&#xff0c;消费者往往难以选择到一款质量好、耐用的产品。我们希望可以为广大消费者提供一些参考和建议&#xff0c;接下来&#…

基于51单片机电子称—串口显示

基于51单片机电子称设计 &#xff08;仿真&#xff0b;程序&#xff09; 功能介绍 具体功能&#xff1a; 1.矩阵键盘组成按键&#xff0c;输入价格结算、打印&#xff1b; 2.用滑动变阻器和ADC0832模拟称重&#xff1b; 3.LCD1602可以显示重量、单价和总价&#xff1b; 4.…

家用洗地机十大品牌什么牌子好用?2024十大爆款洗地机分享

在快节奏的生活中&#xff0c;清洁家居成为了一项繁琐却必不可少的任务。而洗地机的出现&#xff0c;无疑给忙碌的都市人带来了福音。选择一款优质的洗地机可以大大提升我们清洁的效率&#xff0c;改善我们的生活品质。那么&#xff0c;哪家洗地机清洁力最强&#xff0c;更适合…

ONLYOFFICE 桌面编辑器 8.1 版发布:全面提升文档处理效率的新体验

文章目录 什么是ONLYOFFICE &#xff1f;ONLYOFFICE 桌面编辑器 8.1 发布&#xff1a;新功能和改进功能强大的 PDF 编辑器幻灯片版式功能从右至左语言支持多媒体功能增强无缝切换工作模式其他改进和优化总结 什么是ONLYOFFICE &#xff1f; https://www.onlyoffice.com/zh/off…

JavaScript的学习之图片的切换

目录 一、寻找素材 二、编写简单的静态html页面 代码示例 效果展示 三、JS功能的实现 JS代码 完整代码 效果展示 一、寻找素材 随便去网上找几张图片素材 二、编写简单的静态html页面 代码示例 <!doctype html> <html><head><meta charset"…

svn明明都在环境变量中添加了,但还是无法在cmd中生效

svn明明都在环境变量中添加了&#xff0c;但还是无法在cmd中生效 cmd显示原因问题解决 cmd显示 svn不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件 原因 安装svn一直点下一步下一步…&#xff0c;没有勾选command line client。 问题解决 1.按下winx&…

如何加密电脑文件?电脑重要文件怎么加密?

在电脑中&#xff0c;我们会存储很多重要文件&#xff0c;包括个人隐私视频、照片&#xff0c;企业机密数据&#xff0c;工作数据等。为了避免重要文件泄露&#xff0c;我们需要加密保护电脑文件。下面我们就来了解一下电脑文件怎么加密&#xff1f; 超级加密3000 当需要加密电…

vue 实现 word/excel/ppt/pdf 等文件格式预览操作

效果图&#xff1a; 问题描述&#xff1a;一般情况下使用iframe标签就可以实现文件预览&#xff0c;但是这个标签只针对于ppt和pdf是有效的。对于doc文件就需要借助第三方插件&#xff08;vue-office/docx&#xff09;来实现预览了。下面介绍使用方法。 安装插件&#xff1a;n…

力扣爆刷第155天之TOP100五连刷41-45(下一个排列、正序数组中位数、归并排序链表)

力扣爆刷第155天之TOP100五连刷41-45&#xff08;下一个排列、正序数组中位数、归并排序链表&#xff09; 文章目录 力扣爆刷第155天之TOP100五连刷41-45&#xff08;下一个排列、正序数组中位数、归并排序链表&#xff09;一、31. 下一个排列二、4. 寻找两个正序数组的中位数三…

量化交易策略:定义及其重要性

量化交易是华尔街和硅谷的秘密结合点&#xff0c;在这里数学和算法与金钱和市场相遇。虽然它曾经是金融巨头的专属领域&#xff0c;但现在它比以往任何时候都更易于接触。 但不要被愚弄&#xff0c;量化交易仍然是一种高速、高压的游戏&#xff0c;在毫秒间可以赚到或失去财富…

STM32CubeIDE复制工程文档

目录 1. 粘贴复制2. 重命名 1. 粘贴复制 复制粘贴 重命名&#xff0c;别重名&#xff0c;点击- copy 结果 2. 重命名 重命名 ioc文件 编译 OK

IDEA 安装与激活详细教程最新(附最新激活码)2099年亲测有效!

我们先从 IDEA 官网下载 IDEA 2024.1 版本的安装包&#xff0c;下载链接如下&#xff1a; https://www.jetbrains.com/idea/download/ 点击下载(下载Ultimate版)&#xff0c;静心等待其下载完毕即可。 激活方式&#xff1a; 正版专属激活码领取

手机越用越慢?试试这4个秘籍,让手机流畅如新

智能手机作为日常生活的得力助手&#xff0c;最初总是以惊人的速度和流畅性给我们留下深刻印象。 但你有没有发现&#xff0c;随着时间的推移&#xff0c;手机似乎开始变得不那么敏捷&#xff0c;甚至出现了反应迟缓和卡顿的情况&#xff1f; 别让这个问题困扰你,下面是四个关…

Linux关闭swap分区操作[适用于CDH报警等]

1.查看swap分区挂载路径(没卵用) swapon -s 2.设置配置文件的swap配置 echo “vm.swappiness 0” > /etc/sysctl.conf 3.设置内存中的swap状态。有时候配置文件为0&#xff0c;但集群或服务仍然使用了swap分区&#xff0c;可能原因就是内存没有同步配置 echo “0” > …

step5:“串口设置”逻辑(下)

文章目录 文章介绍效果图SerialPort.qml代码v1代码v2代码v3 文章介绍 文章qml/c&#xff1a;基础界面的“串口设置”逻辑1实现了 1、串口连接&#xff0c;连接成功的弹窗提示 2、读取数据。 本篇文章需要实现的功能是 1&#xff09;接收数据后更新“已接收”的行数 效果图 Se…

文本分类-RNN-LSTM

1.前言 本节介绍RNN和LSTM&#xff0c;并采用它们在电影评论数据集上实现文本分类&#xff0c;会涉及以下几个知识点。 1. 词表构建&#xff1a;包括数据清洗&#xff0c;词频统计&#xff0c;词频截断&#xff0c;词表构建。 2. 预训练词向量应用&#xff1a;下载并加载Glove的…

鸿蒙星河NEXT学习笔记

1.1 字符串 // 变量的存储和修改&#xff08;string number boolean&#xff09; // 1. 变量存储 // 1.1 字符串 string 类型 // 注意点1&#xff1a;字符串需要用引号引起来&#xff08;单引双引号&#xff09;字符串 "字符串" // 注意点2&#xff1a;存储的时候&a…