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

高并发扣减库存方案(秒杀扣减库存)

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

背景

我们在做电商的时候,经常遇到下单之后需要扣减库存的业务,那这个业务我们怎么来实现呢?

传统的做法是:

  1. 用户下单,执行下单服务;
  2. 同时,扣减库存;

如果是并发较高的场景,为了保证可用和性能,那么采用二阶段事务的方式,或者消息队列的方式,都可以实现。

但是,多个线程同时执行下单服务,库存服务,这样,会出现什么问题呢?

由于扣减服务中的逻辑并不是原子操作,必然会出现数据不一致的情况,比如,当前库存数量为 1,此时多个线程同时校验库存符合预期,但结果库存为 1 满足了多个线程,显然是不符合预期的,这样,就会导致库存不足的情况。

我们对扣减库存所需要关注的技术点如下:

  1. 当前剩余的数量大于等于当前需要扣减的数量,不允许超卖;
  2. 对于同一个数据的数量存在用户并发扣减,需要保证并发的一致性;
  3. 需要保证可用性和性能,性能至少是秒级;
  4. 一次的扣减包含多个目标数量;
  5. 当次扣减有多个数量时,其中一个扣减不成功即不成功,需要回滚;
  6. 必须有扣减才能有归还;
  7. 返还的数量必须要加回,不能丢失;
  8. 一次扣减可以有多次返还;
  9. 返还需要保证幂等性;

基于 MySQL 进行扣减

顾名思义,就是扣减业务完全依赖 MySQL 等数据库来完成。而不依赖一些其他的中间件或者缓存。纯数据库实现的好处就是逻辑简单,开发以及部署成本低。(适用于中小型电商)。

纯数据库的实现之所以能够满足扣减业务的各项功能要求,主要依赖两点:

  1. 基于数据库的乐观锁方式保证并发扣减的强一致性;
  2. 基于数据库的事务实现批量扣减失败进行回滚。

流程图如下:

有一句话说的非常好:一次完整的流程就是先进行数据校验,在其中做一些参数格式校验,这里做接口开发的时候,要保持一个原则就是不信任原则,一切数据都不要相信,都需要做校验判断。

1

update xxx set leavedAmount=leavedAmount-currentAmount where skuid='xxx' and leavedAmount>=currentAmount;

此 SQL 采用了类似乐观锁的方式实现了原子性。在 where 后面判断剩余数量大于等于需要的数量,才能成功,否则失败。

扣减完成之后,需要记录流水数据。每一次扣减的时候,都需要外部用户传入一个 uuid 作为流水编号,此编号是全局唯一的。用户在扣减时传入唯一的编号有两个作用:

  • 当用户归还数量时,需要带回此编码,用来标识此次返还属于历史上的哪次扣减;
  • 进行幂等性控制。当用户调用扣减接口出现超时时,因为用户不知道是否成功,用户可以采用此编号进行重试或反查,在重试时,使用此编号进行标识防重;

库存校验这一步涉及到了读请求,那么必然增加了数据库的压力,所以需要做一些优化。

  1. 读写分离;

整体的升级策略采用读写分离的方式,另外主从复制直接使用 MySQL 等数据库已有的功能,改动上非常小,只要在扣减服务里配置两个数据源。当客户查询剩余库存,扣减服务中的前置校验时,读取从数据库即可。而真正的数据扣减还是使用主数据库。

读写分离之后,根据二八原则,80% 的均为读流量,主库的压力降低了 80%。但采用了读写分离也会导致读取的数据不准确的问题,不过库存数量本身就在实时变化,短暂的差异业务上是可以容忍的,最终的实际扣减会保证数据的准确性。

  1. 增加缓存;

在上面基础上,还可以升级,增加缓存:

缓存实现扣减

这和前面的扣减库存其实是一样的。但是此时扣减服务依赖的是 Redis 而不是数据库了。

这里针对 Redis 的 hash 结构不支持多个 key 的批量操作问题,我们可以采用 Redis+lua 脚本来实现批量扣减单线程请求。

但,升级成纯 Redis 实现扣减也会有问题:

  • Redis 挂了:如果还没有执行到扣减 Redis 里面库存的操作挂了,只需要返回给客户端失败即可。如果已经执行到 Redis 扣减库存之后挂了,那这时候就需要有一个对账程序。通过对比 Redis 与数据库中的数据是否一致,并结合扣减服务的日志。当发现数据不一致同时日志记录扣减失败时,可以将数据库比 Redis 多的库存数据在 Redis 进行加回
  • Redis 扣减完成,异步刷新数据库失败了。此时 Redis 里面的数据是准的,数据库的库存是多的。在结合扣减服务的日志确定是 Redis 扣减成功到但异步记录数据失败后,可以将数据库比 Redis 多的库存数据在数据库中进行扣减

虽然使用纯 Redis 方案可以提高并发量,但是因为 Redis 不具备事务特性,极端情况下会存在 Redis 的数据无法回滚,导致出现少卖的情况。也可能发生异步写库失败,导致多扣的数据再也无法找回的情况。

数据库+缓存

在此之前,先阐述一下顺序写的性能较好。

在向磁盘进行数据操作时,向文件末尾不断追加写入的性能要远大于随机修改的性能。因为对于传统的机械硬盘来说,每一次的随机更新都需要机械键盘的磁头在硬盘的盘面上进行寻址,再去更新目标数据,这种方式十分消耗性能。而向文件末尾追加写入,每一次的写入只需要磁头一次寻址,将磁头定位到文件末尾即可,后续的顺序写入不断追加即可。

对于固态硬盘来说,虽然避免了磁头移动,但依然存在一定的寻址过程。此外,对文件内容的随机更新和数据库的表更新比较类似,都存在加锁带来的性能消耗。

数据库同样是插入要比更新的性能好。对于数据库的更新,为了保证对同一条数据并发更新的一致性,会在更新时增加锁,但加锁是十分消耗性能的。此外,对于没有索引的更新条件,要想找到需要更新的那条数据,需要遍历整张表,时间复杂度为 O(N)。而插入只在末尾进行追加,性能非常好

上述的架构和纯缓存的架构区别在于,写入数据库不是异步写入,而是在扣减的时候同步写入。同步写入数据库使用的是 insert 操作,就是顺序写,而不是 update 做数据库数量的修改,所以,性能会更好。

insert 的数据库称为任务库,它只存储每次扣减的原始数据,而不做真实扣减(即不进行 update)。它的表结构大致如下:

1
2
3
4

create table task{
id
bigint not null comment "任务顺序编号",
task_id
bigint not null
}

任务表里存储的内容格式可以为 JSON、XML 等结构化的数据。以 JSON 为例,数据内容大致可以如下:

1
2
3
4
5
6

{
"扣减号":uuid,
"skuid1":"数量",
"skuid2":"数量",
"xxxx":"xxxx"
}

这里我们肯定是还有一个记录业务数据的库,这里存储的是真正的扣减名企和 SKU 的汇总数据。对于另一个库里面的数据,只需要通过这个表进行异步同步就好了。

扣减流程:

这里和纯缓存的区别在于增加了事务开启与回滚的步骤,以及同步的数据库写入流程。

任务库里存储的是纯文本的 JSON 数据,无法被直接使用。需要将其中的数据转储至实际的业务库里。业务库里会存储两类数据,一类是每次扣减的流水数据,它与任务表里的数据区别在于它是结构化,而不是 JSON 文本的大字段内容。另外一类是汇总数据,即每一个 SKU 当前总共有多少量,当前还剩余多少量(即从任务库同步时需要进行扣减的),表结构大致如下:

1
2
3
4
5
6

create table 流水表{
id
bigint not null,
uuid
bigint not null comment '扣减编号',
sku_id
bigint not null comment '商品编号',
num
int not null comment '当次扣减的数量'
}comment
'扣减流水表'

商品的实时数据汇总表,结构如下:

1
2
3
4
5
6

create table 汇总表{
id bitint
not null,
sku_id unsigned
bigint not null comment '商品编号',
total_num unsigned
int not null comment '总数量',
leaved_num unsigned
int not null comment '当前剩余的商品数量'
}comment
'记录表'

在整体的流程上,还是复用了上一讲纯缓存的架构流程。当新加入一个商品,或者对已有商品进行补货时,对应的新增商品数量都会通过 Binlog 同步至缓存里。在扣减时,依然以缓存中的数量为准:

相关推荐

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...

取消回复欢迎 发表评论: