百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

电商库存系统的防超卖和高并发扣减方案

mhr18 2024-12-07 21:52 22 浏览 0 评论

电商库存系统的防超卖和高并发扣减方案

摘要:如果你要开发一个电商库存系统,最担心的是什么?闭上眼睛想下,当然是高并发和防超卖了!本文给出一个统筹考虑如何高并发和防超卖数据准确性的方案。读者可以直接借鉴本设计,或在此基础上做出更切合使用场景的设计。

下面用电商库存为示例,来说明如何高并发扣减库存,原理同样适用于其他需要并发写和数据一致性的场景。

库存数量模型示例

为了描述方便,我们使用简化的库存数量模型,真实场景中库存数据项会比我的示例多很多,但已经够说明原理。如下表,库存数量表(stockNum)包含商品标识和库存数量两个字段,库存数量代表有多少货可以卖。

字段名

英文名

字段类型

商品标识

skuId

长整型

库存数量

num

整数

传统通过数据库保证不超卖

库存管理的传统方案为了保证不超卖,都是使用数据库的事务来保证的:通过Sql判断剩余的库存数够用,多个并发执行update语句只有一个能执行成功;为了保证扣减不重复,会配合一个防重表来防止重复的提交,做到幂等性,防重表示例(antiRe)设计如下:

字段名

英文名

字段类型

标识

id

长整型

防重码

code

字符串(唯一索引)

比如一个下单过程的扣减过程示例如下:

事务开始
Insert into antiRe(code) value (‘订单号+Sku’)
Update stockNum set num=num-下单数量 where skuId=商品ID and num-下单数量>0
事务结束
复制代码

面临系统流量越来越大,数据库的性能瓶颈就会暴露出来:就算分库分表也是没用的,促销的时候高并发都是针对少量商品的,最终并发流量会打向少数表,只能去提升单分片的抗量能力。我们接下来设计一种使用Redis缓存做库存扣减的方案。

综合使用数据库和Redis满足高并发扣减的原理

扣减库存其实包含两个过程:第一步是超卖校验,第二步是扣减数据的持久化;在传统数据库扣减中,两步是一起完成的。抗写的实现原理其实是巧妙的利用了分离的思想,分离开防超卖和数据持久化;首先防超卖是由Redis来完成的;通过Redis防超卖后,只要落库就可以;落库通过任务引擎,业务数据库使用商品分库分表,任务引擎任务通过单据号分库分表,热点商品的落库会被状态机分散开,消除热点。

整体架构如下:

第一关解决超卖检验:我们可以把数据放入Redis中,每次扣减库存,都对Redis中的数据进行incryby 扣减,如果返回的数量大于0,说明库存够,因为Redis是单线程,可以信任返回结果。第一关是Redis,可以抗高并发,性能Ok。超卖校验通过后,进入第二关。

第二关解决库存扣减:经过第一关后,第二关不需要再判断数量是否足够,只需要傻瓜扣减库存就行,对数据库执行如下语句,当然还是需要处理防重幂等的,不需要判断数量是否大于0了,扣减SQL只要如下写就可以。

事务开始
Insert into antiRe(code) value (‘订单号+Sku’)
Update stockNum set num=num-下单数量 where skuId=商品ID
事务结束
复制代码

要点:最终还是要使用数据库,热点怎么解决的呢?任务库使用订单号进行分库分表,这样针对同一个商品的不同订单会散列在任务库的不同库存,虽然还是数据库抗量,但已经消除了数据库热点。

整体交互序列图如下:

热点防刷

但Redis也是有瓶颈的,如果出现过热SKU就会打向Redis单片,会造成单片性能抖动。库存防刷有个前提是不能卡单的。可以定制设计JVM内毫秒级时间窗的限流,限流的目的是保护Redis,尽可能的不限流。限流的极端情况就是商品本来应该在一秒内卖完,但实际花了两秒,正常并不会发生延迟销售,之所以选择JVM是因为如果采用远端集中缓存限流,还未来得及收集数据就已经把Redis打死。

实现方案可以通过guava之类的框架,每10ms一个时间窗,每个时间窗进行计数,单台服务器超过计数进行限流。比如10ms超过2个就限流,那么一秒一台服务器就是200个,50台服务器一秒就可以卖出1万个货,自己根据实际情况调整阈值就可以。

Redis扣减原理

Redis的incrby 命令可以用做库存扣减,扣减项可能多个,我们使用Hash结构的hincrby命令,先用Reids原生命令模拟整个过程,为了简化模型我们演示一个数据项的操作,多个数据项原理完全等同。

127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock   #查看苹果手机可售库存为1
"1"
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
127.0.0.1:6379> hget iphone inStock #验证剩余0
"0"
127.0.0.1:6379> hincrby iphone inStock -1 #应用并发超卖但Redis单线程返回剩余-1,下单失败
(integer) -1
127.0.0.1:6379> hincrby iphone inStock 1 #识别-1,回滚库存加一,剩余0
(integer) 0
127.0.0.1:6379> hget iphone inStock #库存恢复正常
"0"
复制代码

扣减的幂等性保证

如果应用调用Redis扣减后,不知道是否成功,可以针对批量扣减命令增加一个防重码,对防重码执行setnx命令,当发生异常的时候,可以根据防重码是否存在来决定是否扣减成功,针对批量命名可以使用pipeline提高成功率。

// 初始化库存
127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock   #查看苹果手机可售库存为1
"1"

// 应用线程一扣减库存,订单号a100,jedis开启pipeline
127.0.0.1:6379> set a100_iphone "1" NX EX 10 #通过订单号和商品防重码
OK
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
//结束pipeline,执行结果OK和0会一起返回
复制代码

防止并发扣减后校验:为了防止并发扣减,需要对Redis的hincrby命令返回值是否为负数,来判断是否发生高并发超卖,如果扣减后的结果为负数,需要反向执行hincrby,把数据进行加回。

如果调用中发生网络抖动,调用Redis超时,应用不知道操作结果,可以通过get命令来查看防重码是否存在来判断是否扣减成功。

127.0.0.1:6379> get a100_iphone   #扣减成功
"1"
127.0.0.1:6379> get a100_iphone   #扣减失败
(nil)
复制代码

单向保证

在很多场景中,因为没有使用事务,你很那做到不超卖,并且不少卖,所以在极端情况下,我的抉择是选择不超卖,但有可能少卖。当然还是应该尽量保证数据准确,不超卖,也不少卖;不能完全保证的前提下,选择不超卖单向保证,也要通过手段来尽可能减少少卖的概率。

比如如果扣减Redis过程中,命令编排是先设置防重码,再执行扣减命令失败;如果执行过程网络抖动可能放重码成功,而扣减失败,重试的时候就会认为已经成功,造成超卖,所以上面的命令顺序是错误的,正确写法应该是:

如果是扣减库存,顺序为:1.扣减库存 2.写入放重码。

如果是回滚库存,顺序为 1.写入放重码 2.扣减库存。

为什么使用PiPeline

在上面命令中,我们使用了Redis的Pipeline,来看下Pipeline的原理。

非pipeline模式 request-->执行 -->response request-->执行 -->response pipeline模式 request-->执行 server将响应结果队列化 request-->执行 server将响应结果队列化 -->response -->response

使用Pipeline,能尽量保证多条命令返回结果的完整性,读者可以考虑使用Redis事务来代替Pipeline,实际项目中,个人有过Pipeline的成功抗量经验,并没有使用Redis事务,正常情况下事务比pipeline慢一些,所以没有采用。

Redis事务 1)mutil:开启事务,此后的所有操作将被添加到当前链接事务的“操作队列”中 2)exec:提交事务 3)discard:取消队列执行 4)watch:如果watch的key被修改,触发dicard。

通过任务引擎实现数据库的最终一致性

前面通过任务引擎来保证数据一定持久化数据库,「任务引擎」的设计如下,我们把任务调度抽象为业务无关的框架。「任务引擎」可以支持简单的流程编排,并保证至少成功一次。「任务引擎」也可以作为状态机的引擎出现,支持状态机的调度,所以「任务引擎」也可以称为「状态机引擎」,在此文是同一个概念。

**任务引擎设计核心原理:**先把任务落库,通过数据库事务保证子任务拆分和父任务完成的事务一致性。

**任务库分库分表:**任务库使用分库分表,可以支撑水平扩展,通过设计分库字段和业务库字段不同,无数据热点。

任务引擎的核心处理流程:

**第一步:**同步调用提交任务,先把任务持久化到数据库,状态为「锁定处理」,保证这件事一定得到处理。

注:原来的最初版本,任务落库是待处理,然后由扫描Worker进行扫描,为了防止并发重复处理,扫描后进行单个任务锁定,锁定成功再进行处理。后来优化为落库任务直接标识状态为「锁定处理」,是为了性能考虑,省去重新扫描再抢占任务,在进程内直接通过线程异步处理。

锁定Sql参考:

UPDATE 任务表_分表号 SET 状态 = 100,modifyTime = now() WHERE id = #{id} AND 状态 = 0
复制代码

**第二步:**异步线程调用外部处理过程,调用外部处理完成后,接收返回子任务列表。通过数据库事务把父任务状态设置为已经完成,子任务落库。并把子任务加入线程池。

要点:保证子任务生成和父任务完成的事务性

**第三步:**子任务调度执行,并重新把新子任务落库,如果没有子任务返回,则整个流程结束。

异常处理Worker

异常解锁Worker来把长时间未处理完成的任务解锁,防止因为服务器重启,或线程池满造成的任务一直锁定无服务器执行。

补漏Worker防止服务器重启造成的线程池任务未执行完成,补漏程序重新锁定,触发执行。

任务状态转换过程

任务引擎数据库设计

任务表数据库结构设计示例(仅做示例使用,真实使用需要完善)

字段

类型

说明

任务ID标识

Long

主键

状态

Int

0待处理,100锁定处理,1完成

数据

String

Json格式的业务数据

执行时间

Date

执行时间

任务引擎数据库容灾:

任务库使用分库分表,当一个库宕机,可以把路由到宕机库的流量重新散列到其他存活库中,可以手工配置,或通过系统监控来自动化容灾。如下图,当任务库2宕机后,可以通过修改配置,把任务库2流量路由到任务库1和3。补漏引擎继续扫描任务库2是因为当任务库2通过主从容灾恢复后,任务库2宕机时未来的及处理的任务可以得到补充处理。

任务引擎调度举例

比如用户购买了两个手机和一个电脑,手机和电脑分散在两个数据库,通过任务引擎先持久化任务,然后驱动拆分为两个子任务,并最终保证两个子任务一定成功,实现数据的最终一致性。整个执行过程的任务编排如下:

任务引擎交互流程:

差异对比-异构数据的终极解决方案

只要有异构,一定会有差异的,为了保证差异的影响可控,终极方案还是要靠差异对比来解决。本文篇幅所限,不再展开,后续再单独成文。DB和Redis差异对比的大概过程为:接收库存变化消息,不断跟进对比Redis和DB的数据是否一致,如果连续稳定不一致,则进行数据修复,用DB数据来修改Redis的数据。

相关推荐

Dubai's AI Boom Lures Global Tech as Emirate Reinvents Itself as Middle East's Silicon Gateway

AI-generatedimageAsianFin--Dubaiisrapidlytransformingitselffromadesertoilhubintoaglob...

OpenAI Releases o3-pro, Cuts o3 Prices by 80% as Deal with Google Cloud Reported to Make for Compute Needs

TMTPOST--OpenAIisescalatingthepricewarinlargelanguagemodel(LLM)whileseekingpartnershi...

黄仁勋说AI Agent才是未来!但究竟有些啥影响?

,抓住风口(iOS用户请用电脑端打开小程序)本期要点:详解2025年大热点你好,我是王煜全,这里是王煜全要闻评论。最近,有个词被各个科技大佬反复提及——AIAgent,智能体。黄仁勋在CES展的发布...

商城微服务项目组件搭建(五)——Kafka、Tomcat等安装部署

1、本文属于mini商城系列文档的第0章,由于篇幅原因,这篇文章拆成了6部分,本文属于第5部分2、mini商城项目详细文档及代码见CSDN:https://blog.csdn.net/Eclipse_...

Python+Appium环境搭建与自动化教程

以下是保姆级教程,手把手教你搭建Python+Appium环境并实现简单的APP自动化测试:一、环境搭建(Windows系统)1.安装Python访问Python官网下载最新版(建议...

零配置入门:用VSCode写Java代码的正确姿

一、环境准备:安装JDK,让电脑“听懂”Java目标:安装Java开发工具包(JDK),配置环境变量下载JDKJava程序需要JDK(JavaDevelopmentKit)才能运行和编译。以下是两...

Mycat的搭建以及配置与启动(mycat2)

1、首先开启服务器相关端口firewall-cmd--permanent--add-port=9066/tcpfirewall-cmd--permanent--add-port=80...

kubernetes 部署mysql应用(k8s mysql部署)

这边仅用于测试环境,一般生产环境mysql不建议使用容器部署。这里假设安装mysql版本为mysql8.0.33一、创建MySQL配置(ConfigMap)#mysql-config.yaml...

Spring Data Jpa 介绍和详细入门案例搭建

1.SpringDataJPA的概念在介绍SpringDataJPA的时候,我们首先认识下Hibernate。Hibernate是数据访问解决技术的绝对霸主,使用O/R映射(Object-Re...

量子点格棋上线!“天衍”邀您执子入局

你是否能在策略上战胜量子智能?这不仅是一场博弈更是一次量子智力的较量——量子点格棋正式上线!试试你能否赢下这场量子智局!游戏玩法详解一笔一画间的策略博弈游戏目标:封闭格子、争夺领地点格棋的基本目标是利...

美国将与阿联酋合作建立海外最大的人工智能数据中心

当地时间5月15日,美国白宫宣布与阿联酋合作建立人工智能数据中心园区,据称这是美国以外最大的人工智能园区。阿布扎比政府支持的阿联酋公司G42及多家美国公司将在阿布扎比合作建造容量为5GW的数据中心,占...

盘后股价大涨近8%!甲骨文的业绩及指引超预期?

近期,美股的AI概念股迎来了一波上升行情,微软(MSFT.US)频创新高,英伟达(NVDA.US)、台积电(TSM.US)、博通(AVGO.US)、甲骨文(ORCL.US)等多股亦出现显著上涨。而从基...

甲骨文预计新财年云基础设施营收将涨超70%,盘后一度涨8% | 财报见闻

甲骨文(Oracle)周三盘后公布财报显示,该公司第四财季业绩超预期,虽然云基建略微逊于预期,但管理层预计2026财年云基础设施营收预计将增长超过70%,同时资本支出继上年猛增三倍后,新财年将继续增至...

Springboot数据访问(整合MongoDB)

SpringBoot整合MongoDB基本概念MongoDB与我们之前熟知的关系型数据库(MySQL、Oracle)不同,MongoDB是一个文档数据库,它具有所需的可伸缩性和灵活性,以及所需的查询和...

Linux环境下,Jmeter压力测试的搭建及报错解决方法

概述  Jmeter最早是为了测试Tomcat的前身JServ的执行效率而诞生的。到目前为止,它的最新版本是5.3,其测试能力也不再仅仅只局限于对于Web服务器的测试,而是涵盖了数据库、JM...

取消回复欢迎 发表评论: