详解分布式锁

知识点: 

单体锁存在的问题:

  • 单体锁,即单体应用中的锁,通过加单体锁(synchronized或RentranLock)可以保证单个实例并发安全

  • 单体锁是JVM层面的锁,只能保证单个实例上的并发访问安全

  • 如果将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同的实例

  • 每个tomocat实例都是一个JVM进程,多实例下会存在数据一致性问题。

分布式锁:

  • 分布式应用中所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。

  • 分布式锁是可以跨越多个tomcat实例,多个JVM进程的锁,所以分布式锁都是设计在第三方组件中的

  • 分布式锁都是通过第三方组件来实现的,目前主流的解决方案是使用Redis或Zookeeper来实现分布式锁

存在的问题:

出现用户超买,商家超卖的问题
具体案例:

添加相关依赖:

        <!--   redis     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

配置application.yml

# 添加redis数据库
spring:redis:port: 6379database: 1host: 127.0.0.1

编写具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {private final StringRedisTemplate redisTemplate;@SneakyThrows@GetMapping("/deductStock")public String deductStock() {System.out.println("用户正在下单……");/*** 单体锁*/int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if (total > 0) {total = total - 1;Thread.sleep(3000);redisTemplate.opsForValue().set("stock", String.valueOf(total));System.out.println("下单成功!剩余库存为:" + total);return "下单成功!剩余库存为:" + total;}System.out.println("用户下单失败!");return "下单失败!剩余库存为:" + total;}
}

测试(点击要快):

我们模拟了系统休眠 ,多线程同时进入一个方法体中,此时,票100同时卖给了两个用户!

解决方案:
 

单体锁:

使用synchronized关键字:修改方法或代码块,用于实现同步控制。当一个线程进入synchronized修饰的方法或代码块时,其他线程需要等待该线程执行完毕后才能进入。

 其中,this关键字指的是,该类的具体实例,即LockController类的具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {private final StringRedisTemplate redisTemplate;@SneakyThrows@GetMapping("/deductStock")public String deductStock() {System.out.println("用户正在下单……");/*** 单体锁*/synchronized (this) {int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if (total > 0) {total = total - 1;Thread.sleep(3000);redisTemplate.opsForValue().set("stock", String.valueOf(total));System.out.println("下单成功!剩余库存为:" + total);return "下单成功!剩余库存为:" + total;}System.out.println("用户下单失败!");return "下单失败!剩余库存为:" + total;}}
}

测试结果:

虽然单体锁,解决了在同一个类中,多线程进入方法的问题,但是,当LockController并非单例,也会出现超卖现象: 

存在问题:当项目部署到集群服务器中,由反向代理服务器,负载均衡。会导致出现多个LockController实例。

解决方法(使用分布式锁):

分布式锁:

首先创建一个工具类,用于注入静态的组件:

@Component
public class ApplicationContextHolder implements ApplicationContextAware {private static ApplicationContext ac;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.ac = applicationContext;}public static <T> T getBean(Class<T> clazz){return ac.getBean(clazz);}public static Object getBean(String name){return ac.getBean(name);}
}

定义一个工具类,用于获取锁,释放锁:

/*** 分布式锁工具类*/
public class LockUtil {private static StringRedisTemplate redisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);//获取锁的超时时间(自旋重试时间)private static long waitTimeout = 10000L;//锁的过期时间,防止死锁private static long lockTimeout = 10L;/*** 获取分布式锁*/public static boolean getLock(String lockName, String value) {//计算获取锁的超时时间long endTime = System.currentTimeMillis() + waitTimeout;//超时之前尝试获取锁while (System.currentTimeMillis() < endTime) {//判断是否能够获取锁,其实就是判断是否往redis中插入对应的keyBoolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, value, lockTimeout, TimeUnit.SECONDS);if (flag) {return true;}}return false;}/*** 释放分布式锁*/public static void unlock(String lockName, String value) {if(value.equals(redisTemplate.opsForValue().get(lockName))){redisTemplate.delete(lockName);}}
}

使用分布式锁进行加锁:

@RestController
@RequiredArgsConstructor
public class LockController {private final StringRedisTemplate redisTemplate;@SneakyThrows@GetMapping("/deductStock")public String deductStock() {System.out.println(Thread.currentThread().getName() + "用户正在下单……");/*** 分布式锁*/String lockName = "stock_lock";String value = UUID.randomUUID().toString();if (!LockUtil.getLock(lockName, value)) {return "获取锁失败……";}int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if (total > 0) {total = total - 1;Thread.sleep(3000);redisTemplate.opsForValue().set("stock", String.valueOf(total));System.out.println("下单成功!剩余库存为:" + total);LockUtil.unlock(lockName,value); //释放锁return "下单成功!剩余库存为:" + total;}System.out.println("用户下单失败!");LockUtil.unlock(lockName,value);return "下单失败!剩余库存为:" + total;}
}

测试结果:

 存在问题:当用户进入后,拿到锁后,执行后续代码,但是锁到期了,锁被释放出来。后续的用户,也是可以进入线程当中的。依旧会出现抄买现象。

解决方法(第三方库来实现分布式锁 ):判断当前用户是否完成后续操作,如果没有完成就自动续签(加时长),直到用户完成后续操作。

Redisson:

Redisson是一个基于Redis的Java驻留对象框架,它提供了一套易于使用的API,用于操作Redis的数据结构和执行分布式操作。

Redisson是Redis官网推荐实现分布式锁的一个第三方类库,用起来更简单。

执行流程:

  • 只要线程加锁成功(默认锁的超时时间为30s),Redisson就会启动一个用于监控锁的看门狗,它是一个守护线程,会每隔10秒检查一下,如果线程还持有锁,就会不断的延长锁的有效期(即每到20s就会自动续借成30s),也称为自动续期机制

  • 当业务执行完,释放锁后,会关闭守护线程。

  • 从而防止了线程业务还没执行完,而锁却过期的问题 。

首先引入相关依赖:

<!-- redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.24.3</version>
</dependency>

编写代码,调用工具类:

@RestController
@RequiredArgsConstructor
public class LockController {private final StringRedisTemplate redisTemplate;private final RedissonClient redissonClient;@SneakyThrows@GetMapping("/deductStock")public String deductStock() {System.out.println(Thread.currentThread().getName() + "用户正在下单……");/*** 使用Redisson分布式锁*/String lockName = "stock_lock";RLock rLock = redissonClient.getLock(lockName);rLock.lock(); //获取锁int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if (total > 0) {total = total - 1;Thread.sleep(3000);redisTemplate.opsForValue().set("stock", String.valueOf(total));System.out.println("下单成功!剩余库存为:" + total);rLock.unlock();return "下单成功!剩余库存为:" + total;}System.out.println("用户下单失败!");rLock.unlock();return "下单失败!剩余库存为:" + total;}
}

测试结果:

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

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

相关文章

java JMH 学习

JMH 是什么&#xff1f; JMH&#xff08;Java Microbenchmark Harness&#xff09;是一款专用于代码微基准测试的工具集&#xff0c;其主要聚焦于方法层面的基准测试&#xff0c;精度可达纳秒级别。此工具由 Oracle 内部负责实现 JIT 的杰出人士编写&#xff0c;他们对 JIT 及…

鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务

在鸿蒙的内核线程就是任务&#xff0c;系列篇中说的任务和线程当一个东西去理解. 一般二种场景下需要切换任务上下文: 在线程环境下&#xff0c;从当前线程切换到目标线程&#xff0c;这种方式也称为软切换&#xff0c;能由软件控制的自主式切换.哪些情况下会出现软切换呢? 运…

【AutoGPT】踩坑帖(follow李鱼皮)

本文写于2024年5月7日 参考视频&#xff1a;AutoGPT傻瓜式使用教程真实体验&#xff01; 对应文章&#xff1a;炸裂的AutoGPT&#xff0c;帮我做了个网站&#xff01; 平台&#xff1a;GitPod 云托管服务 原仓库已经改动很大&#xff0c;应使用的Repo为&#xff1a;Auto-GPT-ZH…

应用层协议之 DNS 协议

DNS 就是一个域名解析系统。域名就是网址&#xff0c;类似于 www.baidu.com。网络上的服务器想要访问它&#xff0c;就得需要它对应的 IP 地址&#xff0c;同时&#xff0c;每个域名对对应着一个 / N个 IP 地址&#xff08;即对应多台服务器&#xff09;。 因此&#xff0c;为了…

如何编译不同目录下的两个文件

1.直接编译 2.打包成动静态库进行链接

【bug记录】清除僵尸进程,释放GPU显存

目录 1. 为什么会出现这种情况&#xff1f;2. 解决方案方法一&#xff1a;使用 fuser 命令方法二&#xff1a; 3. 小贴士 在进行深度学习或其他需要GPU支持的任务时&#xff0c;我们有时会发现虽然没有可见的进程在执行&#xff0c;但GPU资源却意外地被占用。这种情况往往会阻碍…

如何确保UDP文件传输工具有最低稳定的传输速度?

在当前日新月异的数字时代背景下&#xff0c;文件传输工具已经成为我们日常生活与工作中不可或缺的一部分&#xff0c;尤其针对那些频繁涉及即时数据交互与多媒体流通的场景。 UDP协议&#xff0c;以其突出的高速传输与低延迟特性&#xff0c;脱颖而出成为众多用户的首选。不过…

这3种深拷贝实现,你都知道吗?

目录&#xff1a; 1、JSON.parse 2、structuredClone 3、cloneDeep

实习报告怎么写?笔灵AI实习体验报告模版分享:AI产品前端实习生

实习报告怎么写&#xff1f;笔灵AI实习体验报告模版可以帮你 点击即可使用&#xff1a;https://ibiling.cn/scene/inex?fromcsdnsx 下面分享AI产品前端实习生的实习报告 尊敬的导师和领导们&#xff1a;首先&#xff0c;我想对你们表达我的诚挚感谢&#xff0c;感谢你们给我…

C# WinForm —— 12 ListBox绑定数据

ListBox加载大量数据时&#xff0c;避免窗体闪烁的方法&#xff1a; 在加载语句的前后分别加上 BeginUpdate()方法 和 EndUpdate()方法 指定一个集合为绑定的数据源 1. 首先&#xff0c;右键项目&#xff0c;添加类 2. 在新建的类文件中添加属性值信息 3. 构建初始化的对象…

LeetCode 142.环形链表Ⅱ

题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内…

Windows命令行一键安装、配置WSL的方法

本文介绍在Windows电脑中&#xff0c;通过命令行的方式&#xff0c;快速、方便安装适用于Linux的Windows子系统&#xff08;Windows Subsystem for Linux&#xff0c;WSL&#xff09;的方法。 WSL是由微软开发的一项功能&#xff0c;允许在Windows操作系统上运行Linux发行版系统…

Adobe-Premiere-CEP 扩展 入门-视频剪辑-去气口插件-Silence Remover

短视频&#xff0c;这两年比较火&#xff0c;不要再问为什么用Premiere&#xff0c;非常难用&#xff0c;为什么不用某影&#xff0c;某些国内软件非常接地气简单&#xff0c;又例如某音资深的视频短编辑就很好用了。。。 Premiere二次开发调试难&#xff0c;不如自己搞个cons…

Ftp笑脸漏洞(VSFTPD 2.3.4)复现(后门漏洞)

Ftp笑脸漏洞&#xff08;VSFTPD 2.3.4&#xff09;复现&#xff08;后门漏洞&#xff09; 一、原理二、复现准备三、漏洞复现四、Metasploit利用脚本复现 一、原理 vsftpd 是“ very secure FTP daemon ”的缩写&#xff0c;安全性是它的一个最大的特点。 vsftpd是一个 UNIX 类…

学习笔记——字符串(单模+多模+练习题)

单模匹配 Brute Force算法&#xff08;暴力&#xff09; 算法思想 母串和模式串字符依次配对&#xff0c;如果配对成功则继续比较后面位置是否相同&#xff0c;如果出现匹配不成功的位置&#xff0c;则j&#xff08;模式串当前的位置&#xff09;从头开始&#xff0c;i&…

Qt——信号 和 槽

目录 概述 信号和槽的使用 自定义信号和槽 带参数的信号和槽 概述 在Linux系统中&#xff0c;我们也介绍了信号的产生、信号的检测以及信号的处理机制&#xff0c;它就是系统内部的通知机制&#xff0c;也可以是一种进程间通信的方式。在系统中有很多信号&#xff0c;我们可…

设计模式学习笔记 - 回顾总结:在实际软件开发中常用的设计思想、原则和模式

概述 本章&#xff0c;先来回顾下整个专栏的知识体系&#xff0c;主要包括面向对象、设计原则、编码规范、重构技巧、设计模式五个部分。 面向对象 相对于面向过程、函数式编程&#xff0c;面向对象是现在最主流的编程范式。纯面向过程的编程方法&#xff0c;现在已经不多见了…

数据结构中的栈(C语言版)

一.栈的概念 栈是一种常见的数据结构&#xff0c;它遵循后进先出的原则。栈可以看作是一种容器&#xff0c;其中的元素按照一种特定的顺序进行插入和删除操作。 压栈&#xff1a;栈的插入操作叫做进栈/压栈/入栈&#xff0c;入数据在栈顶。 出栈&#xff1a;栈的删除操作叫做…

uniapp/微信小程序实现加入购物车点击添加飞到购物车动画

1、预期效果 2、实现思路 每次点击添加按钮时&#xff0c;往该按钮上方添加一个悬浮元素&#xff0c;通过位移动画将元素移到目标位置。 1. 为每个点击元素设置不同的class&#xff0c;才能通过uni.createSelectorQuery来获取每个元素的节点信息&#xff1b; 2. 添加一个与…

在51单片机里面学习C语言

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「&#xff23;语言的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 说出来你们可能都…