黑马点评-分布式锁业务

分布式锁原理和实现

分布式系统部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

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

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路。

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

分布式锁接口

public interface ILock {/*** 尝试获取锁* @param timeoutSec 锁持有的超时时间,过期后自动释放* @return true 代表获取锁成功;false代表获取锁失败* */boolean tryLock(long timeoutSec);/** 释放锁* */void unlock();
}

在 Spring Data Redis 中,setIfAbsent 方法实际上就是对 Redis 的 setnx 命令的封装。SETNX 是 Redis 中的一个字符串命令,用于设置 key 的值,当且仅当 key 不存在时才设置成功。这个命令通常用于实现分布式锁,即在多个客户端中只有一个能够成功地获取锁。

加锁代码

private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = Thread.currentThread().getId()// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

释放锁代码

public void unlock() {//通过del删除锁stringRedisTemplate.delete(KEY_PREFIX + name);
}

修改业务代码

@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(1200);//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

误删问题

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

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

代码中在获取锁时存入线程标示(可以用UUID表示) 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致。

加锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

释放锁

public void unlock() {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

还有更为极端的误删情景:

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

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Redis的拿锁比锁删锁的lua脚本

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,则删除锁return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

Java中用execute方法调用lua脚本

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());
}

Redission

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

示例demo

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

在 VoucherOrderServiceImpl注入RedissonClient

@Resource
private RedissonClient redissonClient;@Override
public 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);RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁对象boolean isLock = lock.tryLock();//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

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

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

相关文章

2024 PhpStorm激活,分享几个PhpStorm激活的方案

文章目录 PhpStorm 公司简介我这边使用PhpStorm的理由PhpStorm 2023.3 最新变化AI Assistant 预览阶段结束 正式版基于 LLM 的代码补全测试代码生成编辑器内代码生成控制台中基于 AI 的错误解释 Pest 更新PHP 8.3 支持#[\Override] 特性新的 json_validate() 函数类型化类常量弃…

142.环形链表II

142.环形链表II 力扣题目链接(opens new window) 题意&#xff1a; 给定一个链表&#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 为了表示给定链表中的环&#xff0c;使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;索引从 0…

Linux性能分析之CPU实战

本课程专注于教授学员如何利用各种工具和技术来分析Linux系统中的CPU性能问题。通过实际操作和案例研究&#xff0c;学员将深入了解CPU性能优化和故障排除&#xff0c;提升其在Linux环境下的技能水平。 课程大小&#xff1a;1.9G 课程下载&#xff1a;https://download.csdn.…

例行性工作(at,crontab)

目录 单一执行的例行性工作at 语法 选项 时间格式 at的工作文件存放目录 at工作的日志文件 实例 命令总结&#xff1a; 循环执行的例行性工作crond 语法 选项 crontab工作调度对应的系统服务 crontab工作的日志文件 用户定义计划任务的文件所在目录 动态查看 crontab文件格式 文…

尤雨溪:Vue 未来展望新的一轮

十年&#xff0c;一个既漫长又短暂的时光跨度&#xff0c;对于技术世界来说&#xff0c;更是沧海桑田的瞬间。在这十年里&#xff0c;Vue.js 从无到有&#xff0c;从默默无闻到蜚声全球&#xff0c;不仅改变了前端开发的面貌&#xff0c;更成为了无数开发者手中的得力工具。 在…

【RT-DETR有效改进】全新的SOATA轻量化下采样操作ADown(轻量又涨点,附手撕结构图)

一、本文介绍 本文给大家带来的改进机制是利用2024/02/21号最新发布的YOLOv9其中提出的ADown模块来改进我们的Conv模块,其中YOLOv9针对于这个模块并没有介绍,只是在其项目文件中用到了,我将其整理出来用于我们的RT-DETR的项目,经过实验我发现该卷积模块(作为下采样模块)…

多路归并总结

1.鱼塘钓鱼 1262. 鱼塘钓鱼 - AcWing题库 多路归并的模型。 对于每个鱼塘构成的等差数列&#xff0c;我们每次在数列最头部进行选择&#xff0c;选完后再顺延到下一个数即可。我们可以通过维护一个包含所有等差序列首元素的大根堆&#xff0c;使每次可以很容易地选出最大的数…

Vivado使用记录(未完待续)

一、Zynq开发流程 二、软件安装 三、软件使用 字体大小修改&#xff1a;Setting、Font 四、Vivado基本开发流程 1、创建工程 Quick Start 组包含有 Create Project&#xff08;创建工程&#xff09;、 Open Project&#xff08;打开工程&#xff09;、 Open Example Project&…

List之ArrayList、LinkedList深入分析

集合 Java 集合&#xff0c; 也叫作容器&#xff0c;主要是由两大接口派生而来&#xff1a;一个是 Collection接口&#xff0c;主要用于存放单一元素&#xff1b;另一个是 Map 接口&#xff0c;主要用于存放键值对。对于Collection 接口&#xff0c;下面又有三个主要的子接口&…

洗衣洗鞋店小程序对接水洗唛打印,一键预约,支付无忧

随着社会的进步和科技的发展&#xff0c;我们的生活幸福感与日俱增。为了让我们从琐碎中解脱出来&#xff0c;干洗店洗鞋店行业也日新月异。今天&#xff0c;我为大家推荐这款优秀的干洗店小程序系统&#xff0c;让您的洗衣洗鞋服务体验更上一层楼。 干洗店管理系统是一款专为洗…

阿里云和腾讯云区别价格表,云服务器费用对比2024年最新

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器61元一年&#xff0c;2核2G3M、2核4G、4核8G、4核16G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配…

力扣热题100_普通数组_73_矩阵置零

文章目录 题目链接解题思路解题代码 题目链接 73.矩阵置零 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&…

lvs集群介绍

目录 一、LVS集群基本介绍 1、什么是集群 2、集群的类型 2.1 负载均衡群集&#xff08;Load Balance Cluster) 2.2 高可用群集(High Availiablity Cluster) 2.3 高性能运算群集(High Performance Computing Cluster) 3、负载均衡集群的结构 ​编辑 4、LVS集群类型中的…

3D资产管理

3D 资产管理是指组织、跟踪、优化和分发 3D 模型和资产以用于游戏、电影、AR/VR 体验等各种应用的过程。 3D资产管理也称为3D内容管理。 随着游戏、电影、建筑、工程等行业中 3D 内容的增长&#xff0c;实施有效的资产管理工作流程对于提高生产力、减少错误、简化工作流程以及使…

足球俱乐部管理系统:Java与SpringBoot的管理系统实践

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

MySQL存储函数

存储函数是有返回值存储过程&#xff0c;存储函数的参数只能还是IN类型的 1、语法 create function 存储函数名称 &#xff08;[参数列表]&#xff09;Returns type [characterrastic...]Begin--SQL语句Return ..;End;characterrastic说明&#xff1a;Deterministic:相同的输…

uniapp使用openlayers加载地图服务

uniapp使用openlayers加载地图服务 <!-- 地图组件 --> <template><view id"myMap" :regionChangeItem"regionChangeItem" :change:regionChangeItem"olRender.selectAdministrativeRegion":tagSelectProducetagSelectProduce :t…

Visual Studio 2022之Release版本程序发送到其它计算机运行

目录 1、缺少dll​ 2、应用程序无法正常启动 3、This application failed to start because no Qt platform plugin could be initialized. 代码在Debug模式下正常运行&#xff0c;然后切换到Release模式下&#xff0c;也正常运行&#xff0c;把第三方平台的dll拷贝到exe所在…

IPv6扩展头(四)——分片头

分片头部&#xff08;Fragment Header&#xff09;用于IPv6源节点向目的节点发送一个大于路径MTU的数据报。 一、优势 IPv6 分片头具有多种优势&#xff0c;可提高网络效率&#xff0c;包括减少数据包延迟和减少网络拥塞。使用 IPv6 分片头&#xff0c;数据包在源处而不是中间…

Uninty 鼠标点击(摄像机发出射线-检测位置)

平面来触发碰撞&#xff0c;胶囊用红色材质方便观察。 脚本挂载到胶囊上方便操作。 目前实现的功能&#xff0c;鼠标左键点击&#xff0c;胶囊就移动到那个位置上。 using System.Collections; using System.Collections.Generic; using UnityEngine;public class c6 : MonoBe…