首页 > 编程知识 正文

电商高并发处理,高并发下的库存扣减

时间:2023-05-06 07:56:03 阅读:113143 作者:1748

最近发布的是《掘金引言》,如果你要开发电商库存系统,我最担心的是什么? 闭上眼睛想想。 当然,是防止高合并和防止超卖! 本文给出了如何统筹考虑高并发性和防止超额销售数据准确性的方案。 读者可以以此为思考的原点,直接使用本设计,也可以在此基础上制作更好的设计。

以下,以电子商务的库存为例,说明如何通过高合并来扣除库存。 原理也适用于需要同时写入和数据完整性的其他场景。

库存数量模型示例为了便于说明,使用简化的库存数量模型。 在实际场景中,库存数据项会比我的例子多很多,但足以说明原理。 如下表所示,库存量表(stockNum )包含商品标识和库存量两个字段,库存量表示可以销售的商品数量。

字段名英文名字段类型商品skuId长整数库存量num整数传统通过数据库保证不超卖的库存管理的传统方案为了保证不超卖,都是用数据库的事务来保证的。 通过Sql判断剩下的库存数量足够,多个并发update语句中只有一个成功执行。 为了不重复地扣除,与重复防止表合作防止重复提交,实现幂等性。 防重复示例(antiRe )的设计如下。

字段名英文名字段类型idlong的防重复代码代码字符串(唯一索引)。例如,以下是采购订单流程的扣减流程示例:

事务处理开始insertintoantire(code ) value (‘(订单编号SKU ) Update stockNum set num=num-订单数量where skuId=商品ID and num-订单数量0事务处理结束

综合使用数据库和Redis满足高并发扣除原理扣除库存实际上包括两个过程。 第一步是超卖品检查,第二步是扣除数据的永久化。 在传统的数据库扣除中,两个步骤是一起进行的。抗写的实现原理其实是巧妙的利用了分离的思想首先,防止超额销售由Redis负责; 用Redis防止超级大甩卖后,只要掉进仓库就可以了; 去库存通过任务引擎,业务数据库使用商品的库存表,任务引擎的任务通过发票号的库存表,热点商品的去库存通过状态机分散,消除热点。

整体架构如下:

解决第一关超级大甩卖检查:我们可以将数据放入Redis中,每次扣除库存时对Redis的数据进行incryby扣除。 如果返回的数量大于0,则表示库存充足。 因为Redis是单线程的,所以返回的结果是可靠的。 第一关是Redis,耐高并发,性能Ok。 超卖品检验合格后,进入第二个关口。

第二关库存扣除的解决:通过第一关后,第二关不需要判断数量是否足够。 笨蛋扣除库存就行了。 即使对数据库执行以下语句,当然也需要防止重量等处理。 没有必要判断数量是否大于0。 扣除SQL只需写如下即可。

事务开始insertintoantire(code ) value (‘(订单编号SKU ) Update stockNum set num=num-订单数where skuId=商品ID事务结束要点最终这样,同一商品的不同订单会散列到任务库的不同库存中,从而保持数据库的容错能力,但消除了数据库中的热点。

整体交互序列图如下:

热点防止刷

但是,Redis也有瓶颈,过热的话SKU会击中Redis单片,单片的性能就会模糊。 防止库存刷的前提是不能刷卡。 可以自定义设计JVM中毫秒级别的时间窗限制流。 限制流的目的是保护Redis,尽可能不限制流。 流限制的极端情况是,商品本来应该在1秒以内卖完,但实际上需要2秒钟,通常不会发生延迟销售。 之所以选择JVM,是因为如果采用远程集中现金限制流,将赶不上数据收集,杀死Redis。

通过33558www.Sina.com/Guava这样的框架,可以每10ms进行一个时间窗口的计数,每一个时间窗口进行计数,由一台服务器限制流超过计数。 例如,如果10ms超过2个,则流量限制,每秒一台服务器200个,50台服务器每秒可以卖出1万个商品,自己根据情况调整阈值即可。

Redis扣除原理

Redis的incrby命令可用作库存扣减。 扣除项目可能有多个。 使用Hash结构的hincrby命令,首先使用Reids本机命令模拟整个过程。 为了简化模型,演示了一个数据项的操作。 多个数据项的原理完全相同。

127.0.0.1:6379 hsetiphoneinstock1#苹果手机设置可销售库存(integer ) 1127.0.0.133606379 hgetiphoneinstock #

,下单成功(integer) 0127.0.0.1:6379> hget iphone inStock #验证剩余0"0"127.0.0.1:6379> hincrby iphone inStock -1 #应用并发超卖但Redis单线程返回剩余-1,下单失败(integer) -1127.0.0.1:6379> hincrby iphone inStock 1 #识别-1,回滚库存加一,剩余0(integer) 0127.0.0.1:6379> hget iphone inStock #库存恢复正常"0" 扣减的幂等性保证

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

// 初始化库存127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存(integer) 1127.0.0.1:6379> hget iphone inStock   #查看苹果手机可售库存为1"1"​// 应用线程一扣减库存,订单号a100,jedis开启pipeline127.0.0.1:6379> set a100_iphone "1" NX EX 10 #通过订单号和商品防重码OK127.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主键状态Int0待处理,100锁定处理,1完成数据StringJson格式的业务数据执行时间Date执行时间

任务引擎数据库容灾:

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

任务引擎调度举例

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

任务引擎交互流程:

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

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

返回首页导航

本文为作者G原创,未经作者允许,禁止转载

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。