(四)库存超卖案例实战——优化redis分布式锁

news/2024/2/28 17:05:02

前言

在上一节内容中,我们已经实现了使用redis分布式锁解决商品“超卖”的问题,本节内容是对redis分布式锁的优化。在上一节的redis分布式锁中,我们的锁有俩个可以优化的问题。第一,锁需要实现可重入,同一个线程不用重复去获取锁;第二,锁没有续期功能,导致业务没有执行完成就已经释放了锁,存在一定的并发访问问题。本案例中通过使用redis的hash数据结构实现可重入锁,使用Timer实现锁的续期功能,完成redis分布式锁的优化。最后,我们通过集成第三方redisson工具包,完成分布式锁以上俩点的优化内容。Redisson提供了简单易用的API,使得开发人员可以轻松地在分布式环境中使用Redis。

正文

  • 加锁的lua脚本:使用exists和hexists指令判断是否存在锁,如果不存在或者存在锁并且该锁下面的field有值,就使用hincrby指令使锁的值加1,实现可重入,否则直接返回0,加锁失败。
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
"   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
"   redis.call('expire', KEYS[1], ARGV[2]) " +
"   return 1 " +
"else " +
"   return 0 " +
"end"
  • 解锁的lua脚本: 使用hexists指令判断是否存在锁,如果为0,代表没有对应field字段的锁,直接返回nil;如果使用hincrby指令使锁field字段锁的值减少1之后值为0,代表锁已经不在占用,可以删除该锁;否则直接返回0,代表是可重入锁,锁还没有释放。
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
"   return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
"   return redis.call('del', KEYS[1]) " +
"else " +
"   return 0 " +
"end"
  •  实现续期的lua脚本:使用hexists指令判断锁的field值是否存在,如果值为1存在,则将该锁的过期时间更新,否则直接返回0,代表没有找到该锁,续期失败。
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
"   return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
"   return 0 " +
"end";
  • 创建一个自定义的锁工具类MyRedisDistributeLock,实现加锁、解锁、续期功能

- MyRedisDistributeLock实现

package com.ht.atp.plat.util;import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;public class MyRedisDistributeLock implements Lock {public MyRedisDistributeLock(StringRedisTemplate redisTemplate, String lockName, long expire) {this.redisTemplate = redisTemplate;this.lockName = lockName;this.expire = expire;this.uuid = getId();}/*** redis工具类*/private StringRedisTemplate redisTemplate;/*** 锁名称*/private String lockName;/*** 过期时间*/private Long expire;/*** 锁的值*/private String uuid;@Overridepublic void lock() {this.tryLock();}@Overridepublic void lockInterruptibly() {}@Overridepublic boolean tryLock() {try {return this.tryLock(-1L, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}@Overridepublic boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {if (time != -1) {this.expire = unit.toSeconds(time);}String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +"   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +"   redis.call('expire', KEYS[1], ARGV[2]) " +"   return 1 " +"else " +"   return 0 " +"end";while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {Thread.sleep(50);}
//        //加锁成功后,自动续期this.renewExpire();return true;}@Overridepublic void unlock() {String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +"then " +"   return nil " +"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +"then " +"   return redis.call('del', KEYS[1]) " +"else " +"   return 0 " +"end";Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);if (flag == null) {throw new IllegalMonitorStateException("this lock doesn't belong to you!");}}@NotNull@Overridepublic Condition newCondition() {return null;}/*** 给线程拼接唯一标识** @return*/private String getId() {return UUID.randomUUID() + "-" + Thread.currentThread().getId();}private void renewExpire() {String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +"then " +"   return redis.call('expire', KEYS[1], ARGV[2]) " +"else " +"   return 0 " +"end";new Timer().schedule(new TimerTask() {@Overridepublic void run() {System.out.println("-------------------");Boolean flag = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire));if (flag) {renewExpire();}}}, this.expire * 1000 / 3);}
}

- 实现加锁功能

- 实现解锁功能


 - 使用Timer实现锁的续期功能

  • 使用MyRedisDistributeLock实现库存的加锁业务 

- 使用自定义MyRedisDistributeLock工具类实现加锁业务

public void checkAndReduceStock() {//1.获取锁MyRedisDistributeLock myRedisDistributeLock = new MyRedisDistributeLock(stringRedisTemplate, "stock", 10);myRedisDistributeLock.lock();try {// 2. 查询库存数量String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");// 3. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 4.扣减库存stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {myRedisDistributeLock.unlock();}}

- 启动服务7000、7001、7002,压测优化后的自定义分布式锁:平均访问时间362ms,吞吐量每秒246,库存扣减为0,表明优化后的分布式锁是可用的。

  • 集成redisson工具包,使用第三方工具包实现分布式锁,完成并发访问“超卖”问题案例演示
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.11.6</version>
</dependency>
  • 创建一个redisson配置类,引入redisson客户端工具
package com.ht.atp.plat.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyRedissonConfig {@BeanRedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://192.168.110.88:6379");//配置看门狗的默认超时时间为30s,供续期使用config.setLockWatchdogTimeout(30000);return Redisson.create(config);}
}
  • 使用Redisson锁实现“超卖”业务方法 
//可重入锁@Overridepublic void checkAndReduceStock() {// 1.加锁,获取锁失败重试RLock lock = this.redissonClient.getLock("lock");lock.lock();try {// 2. 查询库存数量String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");// 3. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 4.扣减库存stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {// 4.释放锁lock.unlock();}}
  • 开启7000、7001、7002服务,压测扣减库存接口 

- 压测结果:平均访问时间222ms,吞吐量为384每秒

- 库存扣减结果为0

结语

综上所述,无论是自定义分布式锁还是使用redisson工具类,都能实现分布式锁解决并发访问的“超卖问题”,redisson工具使用集成更加方便简洁,推荐使用redisson工具包。本节内容到这里就结束了,我们下期见。。。。。。


http://www.ppmy.cn/news/1188537.html

相关文章

省钱兄短剧短视频视频滑动播放模块源码支持微信小程序h5安卓IOS

# 开源说明 开源省钱兄短剧系统的播放视频模块&#xff08;写了测试弄了好久才弄出来、最核心的模块、已经实战了&#xff09;&#xff0c;使用uniapp技术&#xff0c;提供学习使用&#xff0c;支持IOSAndroidH5微信小程序&#xff0c;使用Hbuilder导入即可运行 #注意&#xff…

验收测试的关键步骤是怎样的?

验收测试是项目管理中的一个关键步骤&#xff0c;旨在确保项目交付物(通常是软件、产品或服务)符合预期的质量标准和需求。这个过程有助于验证项目的可交付成果是否满足客户或利益相关者的期望&#xff0c;同时也为项目团队提供了机会来修复可能存在的问题和改进之前的工作。 一…

精通Nginx(02)-Nginx安装

本文主要讲述Nginx在Linux下的yum安装方式。 Yum安装一般用root模式。 安装方式 在Linux下&#xff0c;Nginx安装有源码编译安装和yum(不同OS可能命令名字不一样)直接安装两种方式。两种方式优缺点如下&#xff1a; 源码编译安装 优点&#xff1a;灵活性高定制性强&#xff…

C++算法: 戳气球

题目 有 n 个气球&#xff0c;编号为0 到 n - 1&#xff0c;每个气球上都标有一个数字&#xff0c;这些数字存在数组 nums 中。 现在要求你戳破所有的气球。戳破第 i 个气球&#xff0c;你可以获得 nums[i - 1] * nums[i] * nums[i 1] 枚硬币。 这里的 i - 1 和 i 1 代表和 …

sd卡部分数据不见了怎么办?三个解决方案

在当今数字化时代&#xff0c;SD卡作为一种常见的存储设备&#xff0c;广泛应用于各种电子设备中。然而&#xff0c;有时候我们会遇到一个问题&#xff0c;那就是SD卡部分数据丢失。这可能是由于各种原因引起的&#xff0c;比如病毒攻击、意外删除、格式化等。本文将深入分析SD…

SC密封件的材料成分

SC密封件也称为轴密封件&#xff0c;是许多机械系统中的关键组件&#xff0c;提供防止润滑剂泄漏和污染物进入的屏障。但SC密封件是由什么材料制成的呢? SC密封型材由带有橡胶涂层的单个金属保持架和带有集成弹簧的主密封唇组成。这种材料的组合为密封件提供了其基本特性&…

【python】数组与步长

概念 数组&#xff1a;mumber [1,3,5,7,9,6,7,8,9,10,11,12,13] 数组截取&#xff1a;number[number.startIndex&#xff1a;number.endIndex&#xff1a;1] 数值截取默认形参&#xff1a;数组首索引&#xff1a;数组尾部索引&#xff1a;默认步长1 步长&#xff1a;&…

有一个 3*4 的矩阵,找出其中值最大的元素,及其行列号

1解题思路&#xff1a; 首先学会输入二维数组&#xff1b;然后知道如何比较求最大值&#xff1b;最后就是格式问题&#xff1b; 2代码&#xff1a; #include<stdio.h> int main() {int a[3][4];int i,j,max,row,line;for(i0;i<3;i){printf("请输入二维数组\n&…

Linux:文件操作

目录 一、关于文件 1、文件类的系统接口 2、文件的含义 二、文件操作 1、C语言文件相关接口 2、系统接口 open close write read 三、文件描述符 关于fd fd的分配规则 输出重定向示例 输入重定向示例 追加重定向示例 dup2函数 缓冲区 stdout与stderr perror…

网络安全(黑客)—零基础自学

目录 一、自学网络安全学习的误区和陷阱 二、学习网络安全的一些前期准备 三、网络安全学习路线 四、学习资料的推荐 想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&am…

1349: 【递推】【入门】踩方格

题目描述 有一个方格矩阵&#xff0c;矩阵边界在无穷远处。我们做如下假设&#xff1a; a、每走一步时&#xff0c;只能从当前方格移动一格&#xff0c;走到某个相邻的方格上&#xff1b; b、走过的格子立即塌陷无法再走第二次&#xff1b; c、只能向北、东、西三个方向走&…

微服务之负载均衡使用场景

在如见常见微服务系统中&#xff0c;负载均衡组件是一种将流量分配到多个服务的技术&#xff0c;目的是提高系统的性能和可用性。负载均衡有两种常见的模式&#xff1a;服务端模式和客户端模式。服务端模式使用独立的应用程序&#xff08;如 Nginx&#xff09;来转发请求&#…

架构设计之大数据架构(Lambda架构、Kappa架构)

大数据架构 一. 大数据技术生态二. 大数据分层架构三. Lambda架构3.1 Lambda架构分解为三层3.2 优缺点3.3 实际案例 四. Kappa架构4.1 结构图4.2 优缺点4.3 实际案例 五. Lambda架构与Kappa架构对比 其它相关推荐&#xff1a; 系统架构之微服务架构 系统架构设计之微内核架构 鸿…

组件化npm包打包和使用

背景&#xff1a;本地环境对功能组件提取&#xff0c;开发环境下通过本地路径引用&#xff0c;发布模式下走npm包引用 1、项目下新建packages/HelloWorld文件夹&#xff0c;在此文件夹下运行终端 npm init 新建packages/HelloWorld/index.vue文件 新建packages/HelloWorld/ind…

数据库系统及应用系列之数据库基础知识

数据库的基础知识&#xff1a;数据库&#xff08;Database&#xff09;是一个有组织的数据集合&#xff0c;用于存储、管理、检索和更新数据。它可以包含多个数据表&#xff0c;每个数据表都包含多个行和列&#xff0c;用于存储不同类型的数据&#xff1b;数据库还可以提供一些…

WS-*标准是什么,有什么作用

WS-*标准是一组基于SOAP协议的Web服务规范&#xff0c;目的是提供一种在分布式环境中实现可互操作的端到端服务的方式。这些标准通常由不同的组织和公司提出&#xff0c;并得到了业界广泛的支持和应用。WS-*标准涵盖了从消息传输、安全性、事务处理到服务描述等多个方面。 主要…

致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC]

文章目录 致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC] 0x01 前言 免责声明&#xff1a;请勿利用…

【年终特惠】基于最新导则下生态环评报告编制技术暨报告篇、制图篇、指数篇、综合应用篇系统性实践技能提升

根据生态环评内容庞杂、综合性强的特点&#xff0c;依据生态环评最新导则&#xff0c;将内容分为4大篇章(报告篇、制图篇、指数篇、综合篇)、10大专题(生态环评报告编制、土地利用图的制作、植被类型及植被覆盖度图的制作、物种适宜生境分布图的制作、生物多样性测定、生物量及…

视频号的视频怎么保存到相册,第三方工具快速下载保存!

视频号是目前非常火爆的短视频平台之一&#xff0c;许多用户在上面发布了各种精彩的视频内容。然而&#xff0c;很多人都想把视频保存到自己的相册中&#xff0c;以便随时欣赏和分享。那么&#xff0c;如何将视频号的视频保存到相册呢&#xff1f; 首先&#xff0c;我们需要了解…

修改例程flags3_asyncio.py以解决RuntimeError错误

运行示例18-13 flags3_asyncio.py&#xff0c;程序报错如下&#xff1a; D:\Python\Python311\python.exe D:\fluentPy\chapter17_18\flags3_asyncio.py REMOTE site: https://www.fluentpython.com/data/flags/ Searching for 20 flags: from BD to VN 5 concurrent connecti…
最新文章