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

黑马redis学习记录:分布式锁

一、基本原理和实现方式对比

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
在这里插入图片描述
分布式锁应该满足的条件:

  1. 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

  2. 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行

  3. 高可用:程序不易崩溃,时时刻刻都保证较高的可用性

  4. 高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能

  5. 安全性:安全也是程序中必不可少的一环
    在这里插入图片描述

常见的三种分布式锁:

  1. Mysql:mysql 本身就带有锁机制,但是由于 mysql 性能本身一般,所以采用分布式锁的情况下,其实使用 mysql 作为分布式锁比较少见

  2. Redis:redis 作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用 redis 或者 zookeeper 作为分布式锁,利用 setnx 这个方法,如果插入 key 成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁

  3. Zookeeper:zookeeper 也是企业级开发中较好的一个实现分布式锁的方案,由于这套视频不讲解zookeeper 的原理和分布式锁的实现,自己搜索了解
    在这里插入图片描述

二、 Redis分布式锁的实现核心思路

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

在这里插入图片描述
核心思路:

利用 redis 的 setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个 key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可。
在这里插入图片描述

三、实现分布式锁(版本一)

锁的基本接口
在这里插入图片描述
实现类: SimpleRedisLock

  • 获取锁:利用 setnx 方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
  • 释放锁:防止删除别人的锁
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) {// 获取线程标识String threadId = Thread.currentThread().getId()String key = KEY_PREFIX + name;// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);// 直接返回 success 会有空指针的风险,因为有个拆箱操作return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {String key = KEY_PREFIX + name;// 释放锁stringRedisTemplate.delete(key);}
}

修改业务代码

  @Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象(新增代码)SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁对象boolean isLock = lock.tryLock(5);//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

四、Redis分布式锁误删情况说明

逻辑说明:

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。
在这里插入图片描述

五、解决Redis分布式锁误删问题

需求:修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁

核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
在这里插入图片描述
修改实现类: SimpleRedisLock

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:";// 线程标识id的前缀private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();String key = KEY_PREFIX + name;// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);// 直接返回 success 会有空指针的风险,因为有个拆箱操作return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {String key = KEY_PREFIX + name;// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标识String id = stringRedisTemplate.opsForValue().get(key);// 判断标识是否一致if (threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(key);}}
}

有关代码实操说明:

在我们修改完此处代码后,我们重启工程,然后启动两个线程,第一个线程持有锁后,手动释放锁,第二个线程 此时进入到锁内部,再放行第一个线程,此时第一个线程由于锁的value值并非是自己,所以不能释放锁,也就无法删除别人的锁,此时第二个线程能够正确释放锁,通过这个案例初步说明我们解决了锁误删的问题。

六、分布式锁的原子性问题

更为极端的误删逻辑说明

线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,
在这里插入图片描述

七、Lua脚本解决多条命令原子性问题

基本语法参考网站:https://www.runoob.com/lua/lua-tutorial.html
释放锁的逻辑:

释放锁的业务流程:

​ 1、获取锁中的线程标示

​ 2、判断是否与指定的标示(当前线程标示)一致

​ 3、如果一致则释放锁(删除)

​ 4、如果不一致则什么都不做

-- 锁的 key
-- local key = KEYS[1]-- 获取线程标识
-- local threadId = ARGV[1]-- 获取锁中的线程标识 get key
-- local id = redis.call('get', KEYS[1])-- 比较线程标识与锁中的标识是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then-- 释放锁 del key,释放成功则返回 1return redis.call('del', KEYS[1])
end
return 0

ps:看最后这些亮的就可以,上面是注释掉的

八、利用Java代码调用Lua脚本,改造分布式锁

lua脚本本身并不需要大家花费太多时间去研究,只需要知道如何调用,大致是什么意思即可

RedisTemplate中,可以利用 execute 方法去执行lua脚本,参数对应关系就如下图:
在这里插入图片描述
unlock.lua 脚本文件放在 resources目录下,保证释放锁时,拿锁、比锁、删锁的操作具有原子性

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}

九、总结

基于Redis的分布式锁实现思路:

  • 利用 set nx ex 获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁
    • 特性:
      • 利用 set nx 满足互斥性
      • 利用 set ex 保证故障时锁依然能释放,避免死锁,提高安全性
      • 利用 Redis 集群保证高可用和高并发特性

秒杀实现分布式锁总结:一路走来,利用添加过期时间,防止死锁问题的发生,但是有了过期时间之后,可能出现误删别人锁的问题,这个问题我们开始是利用删之前 通过拿锁,比锁,删锁这个逻辑来解决的,也就是删之前判断一下当前这把锁是否是属于自己的,但是现在还有原子性问题,也就是我们没法保证拿锁比锁删锁是一个原子性的动作,最后通过lua表达式来解决这个问题

但是目前还剩下一个问题锁不住,什么是锁不住呢,你想一想,如果当过期时间到了之后,我们可以给他续期一下,比如续个30s,就好像是网吧上网, 网费到了之后,然后说,来,网管,再给我来10块的,是不是后边的问题都不会发生了,那么续期问题怎么解决呢?就可以选择依赖于 redission

测试逻辑:

第一个线程进来,得到了锁,手动删除锁,模拟锁超时了,其他线程会执行lua来抢锁,当第一天线程利用lua删除锁时,lua能保证他不能删除他的锁,第二个线程删除锁时,利用lua同样可以保证不会删除别人的锁,同时还能保证原子性。

相关文章:

黑马redis学习记录:分布式锁

一、基本原理和实现方式对比 分布式锁&#xff1a;满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁&#xff0c;只要大家使用的是同一把锁&#xff0c;那么我们就能锁住线程&#xff0c;不让线程进行&#xff0c;让程序串行…...

对React-Fiber的理解,它解决了什么问题?

对React-Fiber的理解&#xff0c;它解决了什么问题&#xff1f;Fiber用来解决什么问题&#xff1f;Fiber是什么&#xff1f;Fiber是如何解决问题的&#xff1f;Fiber用来解决什么问题&#xff1f; JavaScript引擎和页面渲染引擎两个线程是互斥的&#xff0c;当其中一个线程执行…...

【Linux】初学Linux你需要掌握这些基本指令(二)

目录 1.man指令 2.cp指令 3.mv指令 4.tree指令 5.echo指令 6.more指令 7.less指令&#xff08;重要&#xff09; 8.head与tail指令 9.date指令 显示时间常用参数&#xff1a; 设置时间常用参数&#xff1a; 10.cal指令 11.find & whereis & which指令 …...

Linux中VI/VIM 编辑器

1、概述所有Linux系统都会内置vi文本编辑器vim是vi的升级版&#xff0c;可以主动以字体颜色分辨语法的正确性&#xff0c;代码补完和编译&#xff0c;错误跳转等功能。2、vi和vim的三种模式基本上 vi/vim 共分为三种模式&#xff0c;分别是一般模式、编辑模式、命令模式2.1、一…...

PDF怎么转换成Word?两种PDF免费转Word方法推荐

不知道你们有没有发现&#xff0c;我们在网上下载的很多资料都是PDF格式的&#xff0c;尽管PDF文件也可以通过专门的PDF编辑器来编辑&#xff0c;但是PDF文档作为版式文档&#xff0c;编辑起来还是存在很多局限性&#xff0c;所有当我们需要大量编辑修改文档的时候&#xff0c;…...

极兔一面:Dockerfile如何优化?注意:千万不要只说减少层数

说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;面试题是一个非常、非常高频的交流话题。 最近&#xff0c;有小伙伴面试极兔时&#xff0c;遇到一个面试题&#xff1a; 如果优化 Dockerfile&#xff1f; 小伙伴没有回答好&#xff0c;只是提到了减少镜像层数。…...

SpringBoot+Vue实现酒店客房管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…...

自适应多因素认证:构建不可破解的企业安全防线|身份云研究院

打开本文意味着你理解信息安全的重要性&#xff0c;并且希望获取行业最佳实践来保护你所在组织的信息安全。本文将带你了解多因素认证&#xff08;MFA&#xff1a;Multi-Factor-Authentication&#xff09;对于企业信息安全的重要性以及实施方法。 多因素认证&#xff08;MFA&…...

阶段二8_集合ArrayList_学生管理系统_详细步骤

一.学生管理系统案例 1.需求&#xff1a; 针对目前我们的所学内容&#xff0c;完成一个综合案例&#xff1a;学生管理系统&#xff01; 该系统主要功能如下&#xff1a; 1.添加学生&#xff1a;通过键盘录入学生信息&#xff0c;添加到集合中 2.删除学生&#xff1a;通过键盘录…...

一篇解决Linux 中的负载高低和 CPU 开销并不完全对应

负载是查看 Linux 服务器运行状态时很常用的一个性能指标。在观察线上服务器运行状况的时候&#xff0c;我们也是经常把负载找出来看一看。在线上请求压力过大的时候&#xff0c;经常是也伴随着负载的飙高。 但是负载的原理你真的理解了吗&#xff1f;我来列举几个问题&#x…...

关于IDM下载器,提示:一个假冒的序列号被用来注册……idea项目文件路径报红

关于IDM下载器&#xff0c;提示&#xff1a;一个假冒的序列号被用来注册……到C:\Windows\System32\drivers\etc 修改目录下面的hosts文件&#xff08;如果没有修改的权限就右键属性hosts文件修改user的权限为完全控制&#xff09;&#xff0c;在hosts里面增加以下内容&#xf…...

JVM - 高效并发

目录 Java内存模型和内存间的交互操作 Java内存模型 内存间的交互操作 内存间交互操作的规则 volatile特性 多线程中的可见性 volatile 指令重排原理和规则 指令重排 指令重排的基本规则 多线程中的有序性 线程安全处理 锁优化 锁优化之自旋锁与自适应自旋 锁优…...

中小学智慧校园电子班牌系统源码 Saas云平台模式

智慧电子班牌区别于传统电子班牌&#xff0c;智慧校园电子班牌系统更加注重老师和学生的沟通交流和及时数据交互。学校为每个教室配置一台智能电子班牌&#xff0c;一般安装于教室门口&#xff0c;用来实时显示学校通知、班级通知&#xff0c;可设置集中分布式管理&#xff0c;…...

记录一次服务器被攻击的经历

突然收到阿里云发过来的异常登陆的信息&#xff1a; 于是&#xff0c;急忙打开电脑查看对应的ECS服务器的记录&#xff1a; 发现服务器的cpu占用率异常飙升&#xff0c;所以可以大概断定服务器已经被非法入侵了。 通过自己的账号登陆后&#xff0c;发现sshd服务有异常的链接存…...

Python解题 - CSDN周赛第29期 - 争抢糖豆

本期问哥是志在必得&#xff0c;这本算法书我已经觊觎许久&#xff0c;而之前两次因为种种原因未能如愿。因此&#xff0c;问哥这几天花了不少时间&#xff0c;把所有之前在每日一练做过的题目重新梳理了一遍。苦心人&#xff0c;天不负&#xff0c;感谢官方大大&#xff01; 第…...

C代码中访问链接脚本中的符号

一、目的在之前的《GNU LD脚本命令语言&#xff08;一&#xff09;》、《GNU LD脚本命令语言&#xff08;二&#xff09;》我们介绍了GNU链接脚本的知识点&#xff0c;基本上对链接脚本中的SECTION、REGION、以及加载地址与执行地址的关系等内容有了一定的了解。本篇主要讲解链…...

MySQL 8:MySQL索引

索引就是通过一定的算法建立数据模型&#xff0c;用于快速查找某一列中具有特定值的行。如果没有索引&#xff0c;MySQL 必须从第一条记录开始读取整个表&#xff0c;直到找到相关的表。表越大&#xff0c;查询数据所花费的时间就越多。如果表中查询的列有索引&#xff0c;MySQ…...

JVM详解

一&#xff0c;JVM 1&#xff0c;JVM区域划分 类装载器&#xff0c;运行时数据区&#xff0c;字节码执行引擎 2&#xff0c;JVM内存模型&#xff08;运行时数据区&#xff09; 由本地方法栈&#xff0c;虚拟机栈&#xff0c;堆&#xff0c;方法区&#xff0c;和程序计数器组成。…...

MySQL数据库调优————索引数据结构

B-TREE B-TREE数据结构 B-TREE特性 根节点的子结点个数2 < X < m&#xff0c;m是树的阶 假设m 3&#xff0c;则根节点可有2-3个孩子 中间节点的子节点个数m/2 < y < m 假设m 3&#xff0c;中间节点至少有2个孩子&#xff0c;最多3个孩子 每个中间节点包含n个关…...

visual studio 改变界面语言

在使用visual studio 2019 时&#xff0c;开始是英文界面&#xff0c;后面变成了中文界面。但是看视频教学时有的是英文界面&#xff0c;我就想回到英文界面&#xff0c;所以有切换界面语言的需要。其实操作很简单&#xff1a;工具-> 选项 打开界面在界面里选择环境&#xf…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别

UnsatisfiedLinkError 在对接硬件设备中&#xff0c;我们会遇到使用 java 调用 dll文件 的情况&#xff0c;此时大概率出现UnsatisfiedLinkError链接错误&#xff0c;原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用&#xff0c;结果 dll 未实现 JNI 协…...

微信小程序云开发平台MySQL的连接方式

注&#xff1a;微信小程序云开发平台指的是腾讯云开发 先给结论&#xff1a;微信小程序云开发平台的MySQL&#xff0c;无法通过获取数据库连接信息的方式进行连接&#xff0c;连接只能通过云开发的SDK连接&#xff0c;具体要参考官方文档&#xff1a; 为什么&#xff1f; 因为…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

高防服务器能够抵御哪些网络攻击呢?

高防服务器作为一种有着高度防御能力的服务器&#xff0c;可以帮助网站应对分布式拒绝服务攻击&#xff0c;有效识别和清理一些恶意的网络流量&#xff0c;为用户提供安全且稳定的网络环境&#xff0c;那么&#xff0c;高防服务器一般都可以抵御哪些网络攻击呢&#xff1f;下面…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...