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

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

前言

在上一节内容中,我们已经实现了使用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工具包。本节内容到这里就结束了,我们下期见。。。。。。

相关文章:

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

前言 在上一节内容中&#xff0c;我们已经实现了使用redis分布式锁解决商品“超卖”的问题&#xff0c;本节内容是对redis分布式锁的优化。在上一节的redis分布式锁中&#xff0c;我们的锁有俩个可以优化的问题。第一&#xff0c;锁需要实现可重入&#xff0c;同一个线程不用重…...

【ROS入门】雷达、摄像头及kinect信息仿真以及显示

文章结构 雷达信息仿真以及显示Gazebo仿真雷达配置雷达传感器信息xacro文件集成启动仿真环境 Rviz显示雷达数据 摄像头信息仿真以及显示Gazebo仿真摄像头新建xacro文件&#xff0c;配置摄像头传感器信息xacro文件集成启动仿真环境 Rviz显示摄像头数据 kinect信息仿真以及显示Ga…...

实用篇-认识微服务

一、服务架构演变 1. 单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#xff0c;打成一个包部署 单体架构的优点&#xff1a; 架构简单部署成本低 单体架构的缺点&#xff1a; 耦合度高 2. 分布式架构 分布式架构&#xff1a; 根据业务功能对系…...

【产品运营】产品需求应该如何管理

产品项目在进行时经常会有一些需求需要实现&#xff0c;需求是产品更新迭代的动力&#xff0c;需求也是从用户诉求转化而来&#xff1b;在做需求管理时&#xff0c;我们需要判断一个需求的优先级等方面&#xff0c;对产品进行优化&#xff1b; 目录&#xff1a; 一、 为什么要…...

Linux 系统调用IO口,利用光标偏移实现文件复制

用系统调用IO函数实现从一个文件读取最后2KB数据并复制到另一个文件中&#xff0c;源文件以只读方式打开&#xff0c;目标文件以只写的方式打开&#xff0c;若目标文件不存在&#xff0c;可以创建并设置初始值为0664&#xff0c;写出相应代码&#xff0c;要对出错情况有一定的处…...

【原创】指针变量作为函数参数要点注意

指针变量作为函数参数要点注意&#xff08;已写至笔记&#xff09; 1传参指针不加*&#xff08;main中函数&#xff09; 2收参指针要加*&#xff08;被main调用的函数&#xff09; 3传参指针名可与收参指针名不同&#xff0c;不影响 4【问】如何看主函数中指针所指内容是否改变…...

SpringMVC Day 04 : 数据绑定

前言 SpringMVC是一个非常流行的Java Web框架&#xff0c;它提供了很多方便的功能和工具来帮助我们构建高效、灵活的Web应用程序。其中&#xff0c;数据绑定就是SpringMVC中非常重要的一部分&#xff0c;它可以帮助我们方便地将请求参数绑定到Java对象上&#xff0c;从而简化了…...

2.3.1 协程设计原理与汇编实现

1.为什么要有协程&#xff1f; 同步的编程方式&#xff0c;异步的性能。同步编程时&#xff0c;我们需要等待io就绪。但是在协程这里&#xff0c;我们使用一种机制&#xff0c;当io需要等待时&#xff0c;就切到下一个io&#xff0c;之后当之前的io就绪时&#xff0c;再切换回来…...

J2EE项目部署与发布(Windows版本)->会议OA单体项目Windows部署,spa前后端分离项目Windows部署

会议OA单体项目Windows部署spa前后端分离项目Windows部署 1.会议OA单体项目Windows部署&#xff08;以实施的角度&#xff09; 将项目放入webapp&#xff0c;项目能够访问: 首先拿到war包和数据库脚本&#xff0c;并检查是否有什么问题。 如何查看项目报错信息&#xff08;当你…...

Lua脚本语言

1. 概念 Lua&#xff08;发音为"loo-ah"&#xff0c;葡萄牙语中的"lua"意为月亮&#xff09;是一种轻量级的、高效的、可嵌入的脚本编程语言。官网Lua最初由巴西计算机科学家Roberto Ierusalimschy、Waldemar Celes和Luiz Henrique de Figueiredo于1993年开…...

cat()函数和print()函数的区别

目录 区别一&#xff1a; 区别二&#xff1a; cat、print函数都是输出函数。 区别一&#xff1a; cat()函数不能赋值&#xff1b; print()函数可以赋值。 x<-cat("hello world") //赋值 hello world x //cat函数无返回值 NULLy<-print("hello …...

宝塔面板安装Python和Flask(新版Python项目)

&#xff08;一&#xff09;宝塔面板的项目菜单&#xff0c;打开Python项目的“项目版本管理” 安装Python版本3.10.0。 会创建一个Python版本的文件夹www/server/pyproject_evn/versions/ 会创建一个Python虚拟环境的文件夹www/server/pyproject_evn/python_venv/ &#xf…...

火柴排队.

题意&#xff1a;给两列火柴&#xff0c;可以交换任意相邻的火柴&#xff0c;使得&#xff08;ai-bi)^2的和最小&#xff0c;求最小交换次数。 分析&#xff1a;使得&#xff08;ai-bi)^2的和最小&#xff0c;即a^2-2abb^2的和最小&#xff0c;那么使得2ab最大&#xff0c;就可…...

改善游戏体验:数据分析与可视化的威力

当今&#xff0c;电子游戏已经超越了娱乐&#xff0c;成为一种文化现象&#xff0c;汇聚了全球数十亿的玩家。游戏制作公司正采用越来越复杂的技术来提高游戏质量&#xff0c;同时游戏数据分析和可视化工具变得不可或缺。 数据的力量&#xff1a;解析游戏体验 游戏制作涉及到大…...

GEE:本地影像上传到GEE的Assets中,并输入机器学习算法中作为特征变量

作者:CSDN @ _养乐多_ 当我们在 Google Earth Engine(GEE)中应用机器学习算法时,会输入一些影像作为特征变量数据,进一步根据这些特征影像去推理未知区域的数据。但是 GEE 平台上计算特征变量的 API 函数并不是非常全面,我们希望获得更多的特征用于分类。这个时候,我们…...

【Mybatis源码】XMLConfigBuilder构建器 - 读取XML配置初始化Configuration对象

XMLConfigBuilder是Mybatis中定义的进行构建Configuration对象的类,此类用于读取XML配置文件创建并初始化Configuration对象; 上一篇中我们介绍了XMLConfigBuilder构建器加载XML配置文件以及创建Configuration对象https://blog.csdn.net/m1729339749/article/details/133983…...

Python算法练习 10.28

leetcode 700 二叉搜索树中的搜索 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和一个整数值 val。 你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在&#xff0c;则返回 null 。 示例 1: 输入&#xff1a;root [4,2,7,1,…...

【java学习—八】单例设计模式(5)

文章目录 1. 相关概念2. 单例设计模式-饿汉式3. 单例设计模式-懒汉式4. 总结 1. 相关概念 单例&#xff1a;只有一个实例&#xff08;实例化对象&#xff09; 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的…...

【设计模式】第4节:创建型模式之“单例模式”

一、介绍 采取一定的方法保证在整个的软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一个取得其对象实例的方法。 不使用单例模式的UML类图&#xff1a; 使用单例模式的UML类图&#xff1a; 使用场景&#xff1a; 需要频繁创建或销毁的对象…...

NodeJS爬取墨刀上的设计图片

背景 设计人员分享了一个墨刀的原型图&#xff0c;但是给的是只读权限&#xff0c;无法下载其中的素材&#xff1b;开发时想下载里面的一张动图&#xff0c;通过浏览器的F12工具在页面结构找到了图片地址。 但是浏览器直接访问后发现没权限&#xff1a; Nginx 的 403 页面。。…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

ardupilot 开发环境eclipse 中import 缺少C++

目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

C# SqlSugar:依赖注入与仓储模式实践

C# SqlSugar&#xff1a;依赖注入与仓储模式实践 在 C# 的应用开发中&#xff0c;数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护&#xff0c;许多开发者会选择成熟的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;SqlSugar 就是其中备受…...

在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?

uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件&#xff0c;用于在原生应用中加载 HTML 页面&#xff1a; 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

算法岗面试经验分享-大模型篇

文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer &#xff08;1&#xff09;资源 论文&a…...

go 里面的指针

指针 在 Go 中&#xff0c;指针&#xff08;pointer&#xff09;是一个变量的内存地址&#xff0c;就像 C 语言那样&#xff1a; a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10&#xff0c;通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...

Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解

文章目录 一、开启慢查询日志&#xff0c;定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...

在RK3588上搭建ROS1环境:创建节点与数据可视化实战指南

在RK3588上搭建ROS1环境:创建节点与数据可视化实战指南 背景介绍完整操作步骤1. 创建Docker容器环境2. 验证GUI显示功能3. 安装ROS Noetic4. 配置环境变量5. 创建ROS节点(小球运动模拟)6. 配置RVIZ默认视图7. 创建启动脚本8. 运行可视化系统效果展示与交互技术解析ROS节点通…...

goreplay

1.github地址 https://github.com/buger/goreplay 2.简单介绍 GoReplay 是一个开源的网络监控工具&#xff0c;可以记录用户的实时流量并将其用于镜像、负载测试、监控和详细分析。 3.出现背景 随着应用程序的增长&#xff0c;测试它所需的工作量也会呈指数级增长。GoRepl…...