同一热卖商品的高并发写难题 - Lua脚本扣减库存方案

news2025/1/12 1:03:07

目录

一、Mysql高并发写时的行锁难题

二、Redis的相关命令

1. WATCH命令

2. EVAL与EVALSHA命令

三、SpringBoot执行Lua脚本代码示例

1. 依赖包

2. Lua脚本sku.lua

3. 加载Lua脚本

4. 执行Lua脚本

四、参考资料


一、Mysql高并发写时的行锁难题

        通常来说,秒杀场景的爆款商品库存数量相对较少,当发生库存扣减操作时,实际上落到数据库中的写流量很小,只要系统上游能够配合交易系统做好限流保护,数据库基本上不会有太大的负载压力,但大库存的限时抢购场景就恰恰相反了。

        对于同一热卖商品高并发写时,如高并发扣减商品库存,而商品其他信息更新不是那么频繁时场景,此时直接更新DB会造成行锁难题。根据聚集索引(主键)更新时,则Next-Key Lock降级为Record Lock,即:锁住索引本身;根据辅助索引更新时,则Next-Key Lock锁定一个范围,且同时会对下一个键值加上一个Gap Lock。加锁参考资料《Mysql锁》。

        Mysql出现行锁时,其他线程处于等待状态,即:业务系统中的数据库连接池的大量连接处于等待状态,严重影响Mysql的TPS,直接导致RT线性上升,从而导致业务系统崩溃。

        若采用分布式锁,该方案太重,并发量上不去。那么如何避免商品超卖呢?业务上单时段抢购可以分多时段抢购,进行流量分摊处理。代码上避免超卖的解决方案如下:

  • 采用Redis的WATCH/MULTI/EXEC实现库存扣减(缺点:重试来提高扣减成功率)
  • 结合Lua脚本库存扣减(缺点:需要库存同步到数据库)

        Redis的WATCH/MULTI/EXEC库存扣减。其中WATCH命令用于监视一个或多个key,如果在事务执行之前,目标 key 所对应的值发生了改变,那么事务就会执行失败;而MULTI命令用于将事务块内的多条命令按照先后顺序放进一个队列中,最后由EXEC命令原子性地进行提交执行。这种方案会存在一个问题,对于同一热卖商品的并发写操作越高,其WATCH的碰撞概率就越大,会导致同时库存扣减的成功率也就越低。开发过程中需要在业务代码中指定重试次数来提升库存扣减的成功率

        结合Lua脚本库存扣减,其整体上客户端只需要向Redis请求一次,降低了网络开销。更重要的是,只要商品未售罄,就能够确保库存一定可以扣减成功,避免了高并发导致的WATCH碰撞概率问题,大大提升了库存扣减的成功率。那么变化后的库存如何同步到数据库呢?扣减成功后,采用发送消息来实现削峰

二、Redis的相关命令

1. WATCH命令

        WATCH命令的作用是对目标key进行监视,那么被监视的key所对应的值在事务执行前发生了改变,客户端又是如何感知的呢?其实在Redis内部,每个数据库都是由redis.h/redisDb结构类型来表示的,其内部会存储一个watched_keys字典,字典的key就是被监视的目标key,而对应的value则是一个链表类型的数据结构,链表中存放着所有正在监视的客户端,如下图所示。

        其任何的修改命令,在成功执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,确认是否有客户端在监视已经发生修改的key,如果有,那么就会将链表中的这些客户端全部标记为REDIS_DIRTY_CAS状态,后续提交事务时将会发生中断

2. EVAL与EVALSHA命令

        Redis使用EVAL或EVALSHA来执行Lua脚本,参考资料《Redis脚本》。两者的区别是:

  • EVAL:立即执行,但是每次都需要重复向Redis传递一段相同的Lua脚本,网络开销较大。
  • EVALSHA:SCRIPT LOAD提前加载脚本到Redis内存缓存中,不直接执行而是返回校验码;EVALSHA再使用校验码执行脚本。

三、SpringBoot执行Lua脚本代码示例

1. 依赖包

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

2. Lua脚本sku.lua

-- 获取目标sku的key
local sku = KEYS[1]
-- 当前扣减数量
local num = tonumber(ARGV[1])

-- 获取目标key的当前库存数量
local stock = tonumber(redis.call('GET', sku))
-- 返回结果
local result = 0
if stock >= num then
    -- 库存减去扣减数量
    redis.call('DECRBY', sku, num)
    result = 1
end

return result

3. 加载Lua脚本

package com.common.instance.demo.config.loadLua;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

/**
 * @author tcm
 * @version 1.0.0
 * @description 加载Lua脚本
 * @date 2023/3/24 15:47
 **/
@Configuration
public class LuaScriptLoad {

    @Bean
    public DefaultRedisScript<String> getLuaScript() {
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/sku.lua")));
        defaultRedisScript.setResultType(String.class);
        return defaultRedisScript;
    }

}

4. 执行Lua脚本

package com.common.instance.demo.service.impl;

import com.common.instance.demo.service.LuaScriptService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @description Lua脚本测试
 * @author TCM
 * @version 1.0
 * @date 2023/3/25 11:12
 **/
@Service
public class LuaScriptServiceImpl implements LuaScriptService {

    @Resource
    private DefaultRedisScript<String> redisScript;

    @Resource
    @Qualifier("dynamicRedisTemplate")
    private RedisTemplate stringRedisTemplate;

    @Override
    public void luaScript(List<String> keys, Integer num) {
        // 执行lua脚本
        Object execute = stringRedisTemplate.execute(redisScript, keys, num);

        if ("1".equals(execute.toString())) {
            // 成功后,发送消息
            System.out.println("扣减库存成功");
        } else {
            // 失败后返回结果
            System.out.println("扣减库存失败");
        }
    }

}

        org.springframework.data.redis.core.script.DefaultScriptExecutor#eval是执行Lua脚本的核心代码,如下图所示。

protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys,
		byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {

	Object result;
	try {
		result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
	} catch (Exception e) {

		if (!ScriptUtils.exceptionContainsNoScriptError(e)) {
			throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e);
		}

		result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
	}

	if (script.getResultType() == null) {
		return null;
	}

	return deserializeResult(resultSerializer, result);
}

        org.springframework.data.redis.core.script.DefaultRedisScript#getSha1获取Lua脚本的校验码,是对Lua脚本的预加载,首次执行生成SHA的校验码,第二次执行无需生成校验码。

/*
 * (non-Javadoc)
 * @see org.springframework.data.redis.core.script.RedisScript#getSha1()
 */
public String getSha1() {

	synchronized (shaModifiedMonitor) {
		if (sha1 == null || scriptSource.isModified()) {
			this.sha1 = DigestUtils.sha1DigestAsHex(getScriptAsString());
		}
		return sha1;
	}
}

四、参考资料

【更新】SpringBoot自带RedisTemplate执行lua脚本以及预加载lua脚本到Redis集群_spring script load_武话不港1的博客-CSDN博客

https://www.cnblogs.com/RedOrange/p/17095549.html

Redis 脚本 | 菜鸟教程

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

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

相关文章

百度大模型ERNIE3.0

大模型对比 文心全景图 ERNIE 3.0

《Java8实战》第9章 重构、测试和调试

9.1 为改善可读性和灵活性重构代码 Lambda 表达式可以帮助我们用更紧凑的方式描述程序的行为。 9.1.1 改善代码的可读性 可读性非常主观&#xff0c;但是通俗的理解就是“别人理解这段代码的难易程度”。改善可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有…

【Java|基础篇】方法的定义使用、重载以及递归

文章目录 1.什么是方法2.方法的定义和使用返回值类型形参与实参方法执行过程 3.⭐方法的重载4.递归5. 总结 1.什么是方法 方法就是组织好的,可重复使用的具有某种功能的特定代码块 (类似于函数) 在我们平时写代码时,如果遇到会经常使用的一些功能相同的代码时,我们就可以把这段…

在线考试系统学员答题批改日志,实战练习

一、环境要求 sandbox-hdp 2.6.4 或同等版本自建的HadoopHiveSparkHBase 开发环境。 二、数据描述 这是一份来自于某在线考试系统的学员答题批改日志&#xff0c;日志中记录了日志生成时间,题目难度系数&#xff0c;题目所属的知识点 ID&#xff0c;做题的学生 ID&#xff0…

Oracle的学习心得和知识总结(二十一)|Oracle数据库可插拔数据库PDB的创建及删除

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《Oracle Database SQL Language Reference》 2、参考书籍&#xff1a;《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Gui…

华为OD机试(Java),5键键盘的输出

一、题目描述 有一个特殊的5键键盘&#xff0c;上面有a&#xff0c;ctrl-c&#xff0c;ctrl-x&#xff0c;ctrl-v&#xff0c;ctrl-a五个键。 a键在屏幕上输出一个字母a&#xff1b;ctrl-c将当前选择的字母复制到剪贴板&#xff1b;ctrl-x将当前选择的字母复制到剪贴板&#…

极氪要上市,吉利“基因改造”成功

文|智能相对论 作者| 落笔 当越来越多的国产新锐品牌入局新能源汽车市场&#xff0c;晚半拍的极氪却用较短的时间实现了主观逆袭。据统计&#xff0c;极氪001自问世以来便接连斩获消费者欢心&#xff0c;2022年极氪001更是实现了全年交付71941台的战绩&#xff0c;且持续蝉联…

ASP音乐网站的设计与实现

本文阐述了音乐网站的设计与实现&#xff0c;本系统采用ASPSQL Sever 2000作为开发工具。前台主要实现歌曲的分类显示及分类查询、歌曲的在线试听及下载、会员注册、登录以及最新音乐的新闻介绍&#xff0c;并且还为用户提供了在线交流的平台等功能&#xff1b;后台主要用于数据…

BGP路由优选实验

一&#xff0c;实验要求及其拓扑图 二&#xff1a;划分好IP的拓扑 三&#xff1a; 实验分析 1、使用 Preval 策略&#xff0c;确保R4通过R2到达192.168.10.0/24 1、抓取流量 [r4]ip ip-prefix PV permit 192.168.10.0 24 2、配置策略 [r4]route-policy PV permit node 10 [r4…

Web UI自动化测试框架

WebUI automation testing framework based on Selenium and unittest. 基于 selenium 和 unittest 的 Web UI自动化测试框架。 特点 提供更加简单API编写自动化测试。提供脚手架&#xff0c;快速生成自动化测试项目。自动生成HTML测试报告生成。自带断言方法&#xff0c;断言…

Linux常用环境配置

一、sqlite3 1、官网地址 SQLite Download Page 2、在Linux下载 右键复制源代码链接在linux用wget下载 wget https://www.sqlite.org/2023/sqlite-autoconf-3410200.tar.gz 3、解压 tar -xzvf sqlite-autoconf-3410200.tar.gz 4、编译 # 进入解压目录 cd sqlite-autoconf-34102…

App 版本更新插件介绍及使用指南

随着移动互联网的发展&#xff0c;APP 已经成为人们生活中必不可少的一部分&#xff0c;而版本更新也是 APP 发展过程中必不可少的环节。为了更好地实现 APP 版本更新和管理&#xff0c;我们推荐一个非常实用的插件&#xff1a;App 版本更新插件。 该插件支持强制更新、静默更…

Hystrix传递ThreadLocal范围对象的问题(最为细致的分析)

场景 在springcloud微服务体系下&#xff0c;从网关层开始要在request请求头放置一些重要参数&#xff0c;比如traceId&#xff0c;并要求在fegin之间的调用时&#xff0c;也能够一直传递下去&#xff0c;由于实际项目使用中&#xff0c;都是fegin集成了hystrix一起配合使用的…

cloud-canal的部署使用

一&#xff0c;官网参考&#xff1a; https://www.clougence.com/ https://www.clougence.com/cc-doc/quick/quick_start 二&#xff0c;点击下载私有部署版 返回数据&#xff1a; 版本号: 2.5.0.7 MD5值: 18e2502xxxxxxx 下载地址: https://tgzdownload.clougence.com/lates…

华为OD机试(Java),分班

一、题目描述 幼儿园两个班的小朋友在排队时混在了一起&#xff0c;每位小朋友都知道自己是否与前面一位小朋友是否同班&#xff0c;请你帮忙把同班的小朋友找出来。 小朋友的编号为整数&#xff0c;与前一位小朋友同班用Y表示&#xff0c;不同班用N表示。 二、输入描述 输…

PYQT5学习笔记01——PYQT5初体验以及PYQT5程序基本结构分析

一、PYQT5初体验 我们首先用代码编写一个窗口&#xff0c;窗口里面有一个标签控件&#xff0c;标签内的文本是 Hello World&#xff0c;代码如下&#xff1a; # -*- coding: UTF-8 -*- # 导入需要的包 from PyQt5.Qt import * import sys# 创建应用程序对象 app QApplicatio…

【c++初阶】:

c入门 一.概念二.使用三.应用四.常引用五.引用与指针 一.概念 c语言中我们常用指针找地址&#xff0c;但在c中&#xff0c;忽略了指针&#xff08;当然也可以使用指针&#xff09;。常用引用这个概念。 二.使用 可以看到这里的b和c本质上都是a&#xff0c;只是不同的称呼罢了。…

手把手教你将项目部署到服务器!

一、导入centos7虚拟机&#xff1a; 打开VMWare&#xff0c;点击“打开虚拟机”&#xff0c;选择centos7.ova之后&#xff0c;选择存储路径&#xff1a; 点击导入&#xff1a; 选择“不再显示此消息”&#xff0c;点击“重试”按钮&#xff1a; 点击“编辑虚拟机设置”&#x…

【数据结构】二叉树OJ题

&#x1f63d;PREFACE &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐ 评论&#x1f4dd; &#x1f4e2;系列专栏&#xff1a;数据结构 &#x1f50a;本专栏主要更新的是数据结构部分知识点 &#x1f4aa;种一棵树最好是十年前其次是现在 目录 1.单值二叉树 2.相同的树 …

Hadoop之Hive

文章目录 一、Hive简介1.1 Hive 基本概念1.2 Hive架构图1.3 Hive数据模型 二、Hive安装配置2.1 内嵌模式2.2 配置元数据到mysql2.3本地模式2.4远程模式2.5 Hive JDBC Hiverserver22.5.1远程模式下使用Beeline CLI2.5.2 DataGrip图形化客户端 2.6 Hive常见属性配置 一、Hive简介…