基于Redis商品库存扣减方案

前言

电商业务场景下,对于库存的处理是比较重要的,表面上看只是对商品库存数做一个扣减操作,但是要做到不超卖、不少卖,同时还要保证高性能,却是一件非常困难的事。

传统解决方案

库存扣减的传统解决方案是完全基于关系型数据库来做的,以 MySQL 为例,假设有如下sku表:

CREATE TABLE `sku`
(`id`         BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'skuID',`product_id` BIGINT(20) NOT NULL COMMENT '商品ID',`stock`      INT(11) UNSIGNED DEFAULT '0' COMMENT '库存数',PRIMARY KEY (`id`)
) ENGINE = InnoDB COMMENT ='商品sku表';

用户下单时,先执行如下SQL扣减库存,库存扣减成功才创建订单。任一商品库存不足时,扣减就会失败,此时可以回滚事务,并给用户一个友好的提示。

UPDATE sku SET stock=stock-#{num} WHERE id=#{id} AND stock>=#{num}

这种方案可以保证不超卖,它依赖的是MySQL事务一致性和行锁,上一个请求扣减库存会持有对应sku的行锁直到事务提交,后续请求抢锁失败会阻塞,相当于库存扣减在MySQL层面被串行化了。缺点也很明显,如果系统并发较高,或者遇到大促就会存在热点问题,大量用户购买同一商品,就会导致大量线程都在竞争锁,进而导致MySQL TPS降低,RT线性上升,最终甚至引发系统雪崩。MySQL针对单行update的tps大概也就在500左右,为了避免MySQL成为瓶颈,建议把库存扣减操作转移到上层执行。

基于Redis扣减库存

Redis 高效的读写性能,是所有关系型数据库望尘莫及的,单台实例轻轻松松就能达到10W tps,高出MySQL几个数量级,基于Redis的库存扣减方案可以满足绝大多数企业。

在主流程上,用户可能一次下单多个商品,我们可以通过执行lua脚本的方式来扣减库存,并对脚本执行结果做处理。库存扣减可能有三种结果:

  • 1:库存扣减成功
  • 0:库存不足,扣减失败
  • -1:库存不存在,还未load到Redis
@Slf4j
public class StockService {private final RedisClient redisClient = RedisClient.getClient();public void reduce(List<SkuDTO> skuDTOList) {// lua脚本扣减库存int result = doReduce(skuDTOList);if (result == -1) {// 初始化库存initStock(skuDTOList);result = doReduce(skuDTOList);}if (result == 0) {throw new BizException("库存不足");} else if (result == 1) {log.info("库存扣减成功");} else {throw new BizException("处理失败,请重试");}}
}

库存扣减的脚本如下,KEYS是要扣减的sku对应的库存key,ARGV是要扣减的库存数,均是数组。
脚本会先校验,确保库存key和扣减数长度一致。然后遍历KEYS,任一库存key不存在,都会直接返回-1,提醒客户端初始化库存。如果库存key存在就判断库存数是否充足,任一库存数不足都会直接返回0,提醒客户端库存扣减失败。当库存key全都存在,且库存数都足够时,进行KEYS第二次遍历,依次扣减库存并最终返回1。

private int doReduce(List<SkuDTO> skuDTOList) {List<String> keys = skuDTOList.stream().map(s -> String.format(CacheKey.STOCK_KEY, s.getSkuId())).collect(Collectors.toList());List<String> args = skuDTOList.stream().map(SkuDTO::getNumber).map(String::valueOf).collect(Collectors.toList());Object result = redisClient.eval("if(#KEYS~=#ARGV)\n" +"then\n" +"  return nil\n" +"end\n" +"for i,key in ipairs(KEYS)\n" +"do\n" +" if(redis.call('EXISTS',key)==0)\n" +" then\n" +"   return -1\n" +" elseif(tonumber(redis.call('GET',key))<tonumber(ARGV[i]))\n" +" then\n" +"   return 0\n" +" end\n" +"end\n" +"for i,key in ipairs(KEYS)\n" +"do\n" +"  redis.call('DECRBY',key,tonumber(ARGV[i]))\n" +"end\n" +"return 1", keys, args);return Integer.valueOf(result.toString());
}

这里解释下为什么要遍历两次,第一次遍历是为了确保所有sku库存充足,第二次遍历是为了扣减库存。如果在第一次遍历时就扣减库存,后面遇到库存不足的sku扣减失败,Redis是不支持回滚操作的,在业务上去回滚就变得非常复杂了。

如果库存key不存在,则要先把库存数从MySQL load 到Redis。首先通过lua脚本判断哪些库存key不存在,然后查询数据库库存并写入到Redis。

注意:可能有多个线程发现缓存key不存在,写缓存必须用setnx 命令,否则会导致库存数不一致。

private void initStock(List<SkuDTO> skuDTOList) {List<String> keys = skuDTOList.stream().map(s -> String.format(CacheKey.STOCK_KEY, s.getSkuId())).collect(Collectors.toList());Object result = redisClient.eval("local keys = {}\n" +"for i,key in ipairs(KEYS)\n" +"do\n" +"  if(redis.call('EXISTS',key)==0)\n" +"  then\n" +"    keys[#keys+1]=key\n" +"  end\n" +"end\n" +"return keys", keys, Collections.emptyList());if (result != null && result instanceof Collection) {for (Object key : ((Collection) result)) {Integer skuId = Integer.valueOf(key.toString().split(":")[1]);int stock = 0;// mockredisClient.setnx(String.format(CacheKey.STOCK_KEY, skuId), String.valueOf(stock));}}
}

至此,基于Redis扣减库存的流程就结束了。在整个流程中,初始化库存开销是比较大的,因为要查询数据库。所以系统上可以再优化一下,针对秒杀商品或者运营可预见的热点商品,可以在上架时就提前写入Redis,以降低用户下单的延时。
最后就是Redis库存数同步到MySQL了,Redis层只负责库存数扣减拦截,实际的存储还得靠关系型数据库。实现上,可以在下单事务提交后,发送一个MQ消息,利用消息队列来削峰,确保写入数据库的流量是可控的。

尾巴

商品库存扣减的目标是:不超卖、不少卖和高性能。传统基于关系型数据库事务的解决方案实现简单,但是存在热点写问题,数据库沦为性能瓶颈。在高并发场景下更推荐用Redis来做库存扣减,核心是先把库存数从数据库load到Redis,再通过lua脚本来批量扣减库存,细节上要注意先确保所有商品的库存数都充足再统一扣减,否则回滚会非常麻烦。对于可预见的热点商品,可以提前预热,避免用户下单时再初始化缓存,增加下单延时。最后是Redis数据同步到数据库,可以通过消息队列来削峰,确保流量的可控。
用上Redis并不意味着就高枕无忧了,极端情况下仍然会出现数据不一致的情况。因为Redis主从集群复制是异步且有延迟的,如果Master扣减库存后还没同步到Slave就宕机了,此时Slave升级为Master,就会导致库存扣减丢失出现超卖的情况,没办法百分百解决,只能尽可能的在业务低峰期修正缓存里的数据。

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

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

相关文章

ChatGPT安卓版正式发布,附安装包,但有款手机无法使用

ChatGPT安卓版如约而至&#xff0c;OpenAI正式宣布该应用已在谷歌应用商店上架&#xff0c;用户可以免费下载&#xff0c;对话不限次数。 但是安卓版ChatGPT目前仅在美国、印度、孟加拉国和巴西提供下载&#xff0c;下周将会推广至更多国家。 网页端下载链接&#xff1a; http…

五、矩阵的运算

1、矩阵的加减: 前提:两个矩阵必须是同形矩阵。 矩阵加减具有交换律,矩阵矩阵相乘没有交换律。 计算结果:元素级运算。 2、矩阵的数乘: 计算结果:元素级运算。这里要区别与行列式的数乘。 3、矩阵与向量的乘法: 前提:矩阵的列数等于向量的行数。 计算方式:左列…

【Linux基础】vim、常用指令、组管理和组权限

Linux基础 1、目录结构2、vi和vim3、常用指令运行级别找回密码帮助指令时间日期指令搜索查找文件目录操作磁盘管理指令压缩和解压缩 4、组管理和组权限用户操作指令权限 1、目录结构 Linux的文件系统是采用级层式的树状目录结构&#xff0c;在此结构中的最上层是根目录“/”&a…

C++之Easyx——图形库的基本功能(2):来点色彩

一、setbkcolor 函数定义 void EGEAPI setbkcolor(color_t color, PIMAGE pimg NULL); // 设置当前绘图背景色&#xff08;设置并做背景色像素替换&#xff09; 使用说明 void EGEAPI setbkcolor(颜色RGB, PIMAGE pimg NULL); // 设置当前绘图背景色&#xff08;…

备战蓝桥杯---动态规划(应用3之空间优化)

话不多说&#xff0c;直接看题&#xff1a; 我们不妨把问题抽象一下&#xff1a; 首先&#xff0c;我们由裴蜀定理知道如果两个数互质&#xff0c;那么axbyc一定有整数解&#xff08;只要c为1的倍数也就是整数&#xff09;&#xff0c;因此问题就转换为求选一些数使他们gcd1&a…

Unity3D 物理引擎的基本配置详解

前言 在Unity3D中&#xff0c;物理引擎主要由两部分组成&#xff1a;碰撞检测和物理模拟。在本文中&#xff0c;我们将详细介绍Unity3D物理引擎的基本配置&#xff0c;并给出相应的技术详解和代码实现。 对惹&#xff0c;这里有一个游戏开发交流小组&#xff0c;希望大家可以…

图数据库 之 Neo4j - Cypher语法基础(5)

节点(Nodes) Cypher使用()来表示一个节点。 () # 最简单的节点形式,表示一个任意无特征的节点,其实就是一个空节点(movie) # 如果想指向一个节点在其他地方,我们可以给节点添加一个变量名(如movie),表示一个变量名为 movie的节点。(:Movie) # 表示一个标签为 Movie 的匿名…

适用于 Linux、Windows 和 macOS 的免费 ONLYOFFICE 桌面应用程序

前言&#xff1a; 最近也是发现了一款特别好用的免费ONLYOFFICE 桌面应用程序忍不住分享给大家&#xff0c;这款编辑器能够打开、阅读和编辑多种文件类型&#xff0c;包括.docx文档、.pptx幻灯片和.xlsx表格等开放XML格式的Office文档。此外&#xff0c;ONLYOFFICE桌面编辑器还…

收入统计-嵌入式高级软件音频工程师

加我微信hezkz17&#xff0c;可申请进入数字音频系统研究开发交流答疑群&#xff0c;加群附加赠送 蓝牙耳机音频&#xff0c;DSP音频开发资料 1 固定工资收入 2 科技创业收入 3 总收入36K

为什么在MOS管开关电路设计中使用三极管容易烧坏?

MOS管作为一种常用的开关元件&#xff0c;具有低导通电阻、高开关速度和低功耗等优点&#xff0c;因此在许多电子设备中广泛应用。然而&#xff0c;在一些特殊情况下&#xff0c;我们需要在MOS管控制电路中加入三极管来实现一些特殊功能。然而&#xff0c;不同于MOS管&#xff…

编程笔记 Golang基础 009 标识符和关键字

编程笔记 Golang基础 009 标识符和关键字 一、标识符二、标识符分类&#xff08;一&#xff09;空白标识符&#xff08;又称下划线 _&#xff09;&#xff08;二&#xff09;预声明标识符&#xff08;三&#xff09;唯一标识符&#xff08;四&#xff09;导出标识符 三、关键字…

【咕咕送书 | 第七期】世界顶级名校计算机专业,都在用哪些书当教材?

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《linux深造日志》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 写在前面参与规则 ✅参与方式&#xff1a;关注博主、点赞、收藏、评论&#xff0c;任意评论&#xff08;每人最多评论…

如何让qml使用opengl es

要让 QML 使用 OpenGL ES&#xff0c;您需要确保项目配置正确&#xff0c;并在应用程序中使用 QSurfaceFormat 来设置 OpenGL ES 渲染。 以下是一些步骤来配置 QML 使用 OpenGL ES&#xff1a; 1、项目配置&#xff1a;在您的项目配置文件&#xff08;例如 .pro 文件&#xf…

航空航天5G智能工厂数字孪生可视化平台,推进航空航天数字化转型

航空航天5G智能工厂数字孪生可视化平台&#xff0c;推进航空航天数字化转型。随着科技的不断发展&#xff0c;数字化转型已经成为各行各业关注的焦点。航空航天业作为高端制造业的代表&#xff0c;也在积极探索数字化转型之路。为了更好地推进航空航天数字化转型&#xff0c;一…

SOLIDWORKS Electrical如何设置并编辑报表

在电气设计工作中因生产需要&#xff0c;很多企业都会要求电气工程师在图纸中插入设备清单报表。比如在设计机柜布局的时候&#xff0c;在相关设计图纸中插入报表清单可以清楚的帮助了解接线、装配、调试的电气物料内容及对应图纸中的明细。 SOLIDWORKS electrical中就可以自动…

PHP实现分离金额和其他内容便于统计计算

得到的结果可以粘贴到excel计算 <?php if($_GET["x"] "cha"){ $tips isset($_POST[tips]) ? $_POST[tips] : ; $pattern /(\d\.\d|\d)/; $result preg_replace($pattern, "\t\${1}\t", $tips); echo "<h2><strong>数…

第六篇【传奇开心果系列】Python文本和语音相互转换库技术点案例示例:深度解读Kaldi库个性化定制语音搜索引擎

传奇开心果短博文系列 系列短博文目录Python文本和语音相互转换库技术点案例示例系列 短博文目录前言一、雏形示例代码二、扩展思路介绍三、数据准备示例代码四、特征提取示例代码五、声学模型训练示例代码六、语言模型训练示例代码七、解码示例代码八、评估和调优示例代码九、…

MLflow【部署 01】MLflow官网Quick Start实操(一篇学会部署使用MLflow)

一篇学会部署使用MLflow 1.版本及环境2.官方步骤Step-1 Get MLflowStep-2 Start a Tracking ServerStep 3 - Train a model and prepare metadata for loggingStep 4 - Log the model and its metadata to MLflowStep 5 - Load the model as a Python Function (pyfunc) and us…

Autosar-Mcal配置详解-MCU

3.6.1创建、配置RAM 1)创建RAM配置 2)配置RAM 以F1KM R7F7016533ABG为例,它的local RAM有512K, global RAM 192K,Retention RAM 64K. Local RAM: local RAM就是程序平常使用的RAM,在DeepStop模式下内容会丢失。 Global RAM:主要用于DMA的源地址和目的地址使用,在Dee…

Web应用程序防火墙(WAF)与传统防火墙的区别

由于WEB应用防火墙&#xff08;WAF&#xff09;的名字中有“防火墙”三个字&#xff0c;因此很多人都会将它与传统防火墙混淆。实际上&#xff0c;二者之间的有着很大的差别。传统防火墙专注在网络层面&#xff0c;提供IP、端口防护。而WAF是专门为保护基于Web的应用程序而设计…