多业务场景下对于redis分布式锁的一些思考

现在让你写一个Redis分布式锁
大概率你会先写一个框架

public Boolean setIfAbsent(String key, Object value,Long timeout) {try {return Boolean.TRUE.equals(objectRedisTemplate.opsForValue().setIfAbsent(key, value,timeout,TimeUnit.SECONDS));} catch (Exception e) {log.error("", e);return false;}}
 private void assessInstance(){InitThreadPoolUtil.execute(() -> {while (true) {try {Boolean isMaster = setIfAbsent(RedisKeyConstant.ASSESS_INSTANCE_ONE, "admin", 2 * 60L);logger.error("执行插入===" + isMaster);if (isMaster) {// ...业务代码略if (redisService.hasKey(RedisKeyConstant.ASSESS_INSTANCE_ONE)) {redisService.del(RedisKeyConstant.ASSESS_INSTANCE_ONE);}}} catch (Exception e) {logger.error("评估规划发送通知失败:", e);}try {Thread.sleep(1000 * 60 * 1);} catch (InterruptedException e) {logger.error("线程休眠异常,异常信息为:", e);}}});}

但是这样就完了吗?
我们来评审一下此代码健壮性:

可以看到这是从线程池中取一个线程去执行该业务代码。那么我给你的场景是处理订单业务,那么你就会面对高并发情况,若某一刻发起了10个订单请求,那么就会有10个线程进入while循环。但是有且仅有一个线程会获取锁,并执行业务代码。其他9个线程会一直等待,一旦有锁释放,这9个线程会立刻抢锁。

我们给redis的锁定义了一个超时时间,某线程获取锁后最多使用 10s,然后必须释放锁。
此外你还知道执行该业务代码最多需要10s。等于你上网时间刚清零你本局游戏刚结束。
这样其他9个线程最多需要10s就可以获取到锁。

所以会出现一种现象,A线程获取到了锁后,开始执行业务代码。其他9个线程会一直重试尝试获取锁,累计10s。为了避免频繁尝试获取锁消耗资源,我们暂时设置线程第一次未获取锁后,需要休眠2s才能重新请求获取锁。这样就降低了这9个线程重试请求锁的频率。

对于用户而言,一个用户的订单正在处理,其他9个用户的订单需要等待10s,推算下来,最后一个用户的订单被处理时,已经等待了90s。如果我是用户,我可不希望等待这么长的时间且无法进行任何操作。

我更希望等待更少的时间,比如20s没反应,我可以继续提交订单。像不像抢演唱会票的过程:进入订单界面,提交的时候一直转圈圈,等待5s后显示订单提交失败,然后你会重新提交订单。

此外,上述代码还有个局限性:提交了10个订单,将会有1个线程执行业务代码,9个线程一直在等待。
执行业务代码的线程生命周期如下:尝试获取锁—>获取锁---->执行业务代码----->等待被自动回收
等待的线程生命周期如下: 休眠—>尝试获取锁—>休眠---->尝试获取锁—>…

可以发现等待的线程是始终无法被自动回收,除非执行完业务代码,操作系统才能判断:该线程已经没有被使用了,可以自动归还到线程池。(线程池自动管理线程的生命周期)

对于用户而言,他等待时间太久。对于系统而言,大量资源被此处占用、消耗。

所以我们必须优化。如何优化呢?
A线程会占用锁10s,其余9个线程会一直等待。现在我要求,一旦发现6s后,锁还没被释放,等待的线程就退出等待。而用户就可以重新提交订单了。

我们来捋一捋:A线程抢到了锁后,(超时时间也就是等待时间未超过6s)B线程先睡眠2s,再重新获取锁失败,(超时时间也就是等待时间未超过6s)再睡眠2s,重新获取锁失败,(超时时间也就是等待时间未超过6s)再睡眠2s,重新获取锁失败,(超时时间也就是等待时间超过6s),不再尝试获取锁,返回信息:订单提交失败。

推理下来,一个用户最多等待10s,变成了最多等待6s。那么10个订单同时提交而最后一个用户只需等待50s。想要再缩短等待时间,可以将超时时间从6s缩短到2s,这样10个订单同时提交而最后一个用户只需等待18s。

当然你也可以将业务处理时间优化,这里不讨论。

代码如下

 private void assessInstance(){// 初始时间long startTime = System.currentTimeMillis();InitThreadPoolUtil.execute(() -> {while (true) {try {Boolean isMaster = setIfAbsent(RedisKeyConstant.ASSESS_INSTANCE_ONE, "admin", 2 * 60L);logger.error("执行插入===" + isMaster);if (isMaster) {// ...业务代码略// 尝试超过了设定值之后直接跳出循环,避免上新锁时间过长// 例如A线程上新锁,花费了10s,这10s内B线程无法获取锁,就会一直在循环里重试,设置超时时间为2s,// 一旦B线程重试超过2s就退出循环且生命周期结束。if (System.currentTimeMillis() - startTime > timeout) {return false;}if (redisService.hasKey(RedisKeyConstant.ASSESS_INSTANCE_ONE)) {redisService.del(RedisKeyConstant.ASSESS_INSTANCE_ONE);}}} catch (Exception e) {logger.error("评估规划发送通知失败:", e);}try {Thread.sleep(1000 * 60 * 1);} catch (InterruptedException e) {logger.error("线程休眠异常,异常信息为:", e);}}});}

这是针对高并发场景下以上代码实现Redis锁的问题。有些场景下使用上述代码完全没问题。
例如服务启动后,需要初始化一些数据。单机环境只会执行一次初始化数据,什么都不需要考虑。

若是集群模式,有三个机子。当然只能一台leader机子执行一次初始化数据,其余2个机子不需要执行初始化数据,所以必须上分布式锁,且不存在高并发场景。

上述的代码直接使用了redis的一些原生api,我们尝试将其封装一层供自己使用

/*** 全局锁,包括锁的名称*/
public class Lock {private String name;private String value;public Lock(String name, String value) {this.name = name;this.value = value;}public String getName() {return name;}public String getValue() {return value;}}

搞一个redis分布式锁的工具类

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.concurrent.TimeUnit;/*** 分布式锁*/@Component
public class DistributedLockHandler {private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler.class);/*** 单个业务持有锁的时间30s,防止死锁*/private final static long LOCK_EXPIRE = 30 * 1000L;/*** 默认30ms尝试一次*/private final static long LOCK_TRY_INTERVAL = 30L;/*** 默认尝试20s*/private final static long LOCK_TRY_TIMEOUT = 20 * 1000L;@Autowiredprivate StringRedisTemplate template;/*** 尝试获取全局锁** @param lock 锁的名称* @return true 获取成功,false获取失败*/public boolean tryLock(Lock lock){return getLock(lock, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE);}/*** 尝试获取全局锁** @param lock    锁的名称* @param timeout 获取超时时间 单位ms* @return true 获取成功,false获取失败*/public boolean tryLock(Lock lock, long timeout) {return getLock(lock, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE);}/*** 尝试获取全局锁** @param lock           锁的名称* @param timeout        获取锁的超时时间* @param tryInterval    多少毫秒尝试获取一次* @param lockExpireTime 锁的过期* @return true 获取成功,false获取失败*/public boolean tryLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {return getLock(lock, timeout, tryInterval, lockExpireTime);}/*** 操作redis获取全局锁** @param lock           锁的名称* @param timeout        获取的超时时间* @param tryInterval    多少ms尝试一次* @param lockExpireTime 获取成功后锁的过期时间* @return true 获取成功,false获取失败*/public boolean getLock(Lock lock, long timeout, long tryInterval, long lockExpireTime){// 1. 锁名不为空if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {return false;}// 2. 系统时间long startTime = System.currentTimeMillis();try{do{// 不存在锁,上新锁if (!template.hasKey(lock.getName())) {ValueOperations<String, String> ops = template.opsForValue();ops.setIfAbsent(lock.getName(), lock.getValue(), lockExpireTime, TimeUnit.MILLISECONDS);return true;} else {//已存在锁logger.error("lock is exist!!!");}// 尝试超过了设定值之后直接跳出循环,避免上新锁时间过长// 例如A线程上新锁,花费了10s,这10s内B线程无法获取锁,就会一直在循环里重试,设置超时时间为3s,一旦B线程重试超过3s就退出循环且生命周期结束。if (System.currentTimeMillis() - startTime > timeout) {return false;}// A线程刚获取了锁,B线程等待A线程释放锁Thread.sleep(tryInterval);}while(template.hasKey(lock.getName()));  // 3. redis中是否存在锁}catch (Exception e){logger.error(e.getMessage());return false;}return false;}/*** 释放锁*/public void releaseLock(Lock lock){if (!StringUtils.isEmpty(lock.getName())) {template.delete(lock.getName());}}}

测试代码,可以看到这是我们自己封装的最终效果

@RestController
public class testDemo {@Autowiredprivate DistributedLockHandler distributedLockHandler;@RequestMapping("/index")public void index(){Lock lock=new Lock("lynn","min");if (distributedLockHandler.tryLock(lock)) {// 1. 成功获取锁try {//为了演示锁的效果,这里睡眠5000毫秒System.out.println("执行方法");Thread.sleep(5000);}catch (Exception e){e.printStackTrace();}// 2. 释放锁distributedLockHandler.releaseLock(lock);}}}

以上结合业务场景探讨了实现Redis分布式锁时,为何使用线程休眠,超时时间,以及针对超时时间的一些优化方案。

接下来引入一个新的问题:

若定义锁的过期时间是10s,此时A线程获取了锁然后执行业务代码,但是业务代码消耗时间花费了15s。这就会导致A线程还没有执行完业务代码,A线程却释放了锁(因为10s到了),第11s B线程发现锁已经释放,重新获取锁也开始执行业务代码。

此时多个线程同时执行业务代码,我们使用锁就是为了保证仅有一个线程执行这一块业务代码,说明这个锁是失效的!

如何处理这个情况,涉及到了锁延期操作,下一篇文章指出!

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

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

相关文章

2024开年,手机厂商革了自己的命

文&#xff5c;刘俊宏 编&#xff5c;王一粟 2024开年&#xff0c;AI终端的号角已经由手机行业吹响。 OPPO春节期间就没闲着&#xff0c;首席产品官刘作虎在大年三十就迫不及待地宣布&#xff0c;OPPO正式进入AI手机时代。随后在开年后就紧急召开了AI战略发布会&#xff0c;…

【Antd】Form 表单获取不到 Input 的值

文章目录 今天遇到了一个奇怪的bug&#xff0c;Form表单中的Input组件的值&#xff0c;不能被Form获取&#xff0c;导致输入了内容&#xff0c;但是表单提交的时候值为undefined 报错代码 import { Button, Form, Input } from antd; import React from react;const App: Rea…

GaussDB SQL调优:建立合适的索引

背景 GaussDB是华为公司倾力打造的自研企业级分布式关系型数据库&#xff0c;该产品具备企业级复杂事务混合负载能力&#xff0c;同时支持优异的分布式事务&#xff0c;同城跨AZ部署&#xff0c;数据0丢失&#xff0c;支持1000扩展能力&#xff0c;PB级海量存储等企业级数据库…

SQL中为什么不要使用1=1

最近看几个老项目的SQL条件中使用了11&#xff0c;想想自己也曾经这样写过&#xff0c;略有感触&#xff0c;特别拿出来说道说道。 编写SQL语句就像炒菜&#xff0c;每一种调料的使用都可能会影响菜品的最终味道&#xff0c;每一个SQL条件的加入也可能会影响查询的执行效率。那…

昨天Google发布了最新的开源模型Gemma,今天我来体验一下

前言 看看以前写的文章&#xff0c;业余搞人工智能还是很早之前的事情了&#xff0c;之前为了高工资&#xff0c;一直想从事人工智能相关的工作都没有实现。现在终于可以安静地系统地学习一下了。也是一边学习一边写博客记录吧。 昨天Google发布了最新的开源模型Gemma&#xf…

电商数据采集的几个标准

面对体量巨大的电商数据&#xff0c;很多品牌会选择对自己有用的数据进行分析&#xff0c;比如在控价过程中&#xff0c;需要对商品的价格数据进行监测&#xff0c;或者是需要做数据分析时&#xff0c;则需要采集到商品的价格、销量、评价量、标题、店铺名等信息&#xff0c;数…

Unity中.Net与Mono的关系

什么是.NET .NET是一个开发框架&#xff0c;它遵循并采用CIL(Common Intermediate Language)和CLR(Common Language Runtime)两种约定&#xff0c; CIL标准为一种编译标准&#xff1a;将不同编程语言&#xff08;C#, JS, VB等&#xff09;使用各自的编译器&#xff0c;按照统…

JavaScript 原始值和引用值在变量复制时的异同

相比于其他语言&#xff0c;JavaScript 中的变量可谓独树一帜。正如 ECMA-262 所规定的&#xff0c;JavaScript 变量是松散类型的&#xff0c;而且变量不过就是特定时间点一个特定值的名称而已。由于没有规则定义变量必须包含什么数据类型&#xff0c;变量的值和数据类型在脚本…

mysql.service is not a native service, redirecting to systemd-sysv-install

字面意思&#xff1a;mysql.service不是本机服务&#xff0c;正在重定向到systemd sysv安装 在CentOS上使用Systemd管理MySQL服务的具体步骤如下&#xff1a; 1、创建MySQL服务单元文件&#xff1a; 首先&#xff0c;你需要创建一个Systemd服务单元文件&#xff0c;以便Syste…

【Python笔记-设计模式】原型模式

一、说明 原型模式是一种创建型设计模式&#xff0c; 用于创建重复的对象&#xff0c;同时又能保证性能。 使一个原型实例指定了要创建的对象的种类&#xff0c;并且通过拷贝这个原型来创建新的对象。 (一) 解决问题 主要解决了对象的创建与复制过程中的性能问题。主要针对…

redhawk:使用ipf文件反标instance power

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 往期文章链接: Redhawk:Input Data Preparation 使用ptpx和redhawk报告功耗时差别总是很大,如果需要反标top/block的功耗值可以在gsr文件中使用BLOCK_POWER_FOR_SCALING的命令

Verilog刷题笔记35

题目&#xff1a; Create a 1-bit wide, 256-to-1 multiplexer. The 256 inputs are all packed into a single 256-bit input vector. sel0 should select in[0], sel1 selects bits in[1], sel2 selects bits in[2], etc. 解法&#xff1a; module top_module( input [255:…

Spring Cloud Alibaba-05-Gateway网关-02-断言(Predicate)使用

Lison <dreamlison163.com>, v1.0.0, 2023.10.20 Spring Cloud Alibaba-05-Gateway网关-02-断言(Predicate)使用 文章目录 Spring Cloud Alibaba-05-Gateway网关-02-断言(Predicate)使用通过时间匹配通过 Cookie 匹配通过 Header 匹配通过 Host 匹配通过请求方式匹配通…

C# CAD2016 cass10宗地Xdata数据写入

一、 查看cass10写入信息 C# Cad2016二次开发获取XData信息&#xff08;二&#xff09; 一共有81条数据 XData value: QHDM XData value: 121321 XData value: SOUTH XData value: 300000 XData value: 141121JC10720 XData value: 权利人 XData value: 0702 XData value: YB…

2.居中方式总结

居中方式总结 经典真题 怎么让一个 div 水平垂直居中 盒子居中 首先题目问到了如何进行居中&#xff0c;那么居中肯定分 2 个方向&#xff0c;一个是水平方向&#xff0c;一个是垂直方向。 水平方向居中 水平方向居中很简单&#xff0c;有 2 种常见的方式&#xff1a; 设…

java面试题之mybatis篇

什么是ORM&#xff1f; ORM&#xff08;Object/Relational Mapping&#xff09;即对象关系映射&#xff0c;是一种数据持久化技术。它在对象模型和关系型数据库直接建立起对应关系&#xff0c;并且提供一种机制&#xff0c;通过JavaBean对象去操作数据库表的数据。 MyBatis通过…

MATLAB练习题:randperm函数的练习题

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili MATLAB中有一个非常有用的函数&#xff1a;randperm函数&…

华为算法题 go语言或者ptython

1 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返…

如何进行高性能架构的设计

一、前端优化 减少请求次数页面静态化边缘计算 增加缓存控制&#xff1a;请求头 减少图像请求次数&#xff1a;多张图片变成 一张。 减少脚本的请求次数&#xff1a;css和js压缩&#xff0c;将多个文件压缩成一个文件。 二、页面静态化 三、边缘计算 后端优化 从三个方面进…

adb-monkey命令

目录 adb shell monkey -p/-v 包名 次数 1、指定一个包 2、指定多个包 3、不指定包 Event percentages&#xff08;事件百分比&#xff09; 常见参数 --throttle 延迟时间 单位毫秒 --pct-touch 设定触屏事件生成的百分比 --pct-motion 设定滑动事件生成…