高并发扣减库存方案(秒杀扣减库存)
mhr18 2024-12-07 21:52 12 浏览 0 评论
背景
我们在做电商的时候,经常遇到下单之后需要扣减库存的业务,那这个业务我们怎么来实现呢?
传统的做法是:
- 用户下单,执行下单服务;
- 同时,扣减库存;
如果是并发较高的场景,为了保证可用和性能,那么采用二阶段事务的方式,或者消息队列的方式,都可以实现。
但是,多个线程同时执行下单服务,库存服务,这样,会出现什么问题呢?
由于扣减服务中的逻辑并不是原子操作,必然会出现数据不一致的情况,比如,当前库存数量为 1,此时多个线程同时校验库存符合预期,但结果库存为 1 满足了多个线程,显然是不符合预期的,这样,就会导致库存不足的情况。
我们对扣减库存所需要关注的技术点如下:
- 当前剩余的数量大于等于当前需要扣减的数量,不允许超卖;
- 对于同一个数据的数量存在用户并发扣减,需要保证并发的一致性;
- 需要保证可用性和性能,性能至少是秒级;
- 一次的扣减包含多个目标数量;
- 当次扣减有多个数量时,其中一个扣减不成功即不成功,需要回滚;
- 必须有扣减才能有归还;
- 返还的数量必须要加回,不能丢失;
- 一次扣减可以有多次返还;
- 返还需要保证幂等性;
基于 MySQL 进行扣减
顾名思义,就是扣减业务完全依赖 MySQL 等数据库来完成。而不依赖一些其他的中间件或者缓存。纯数据库实现的好处就是逻辑简单,开发以及部署成本低。(适用于中小型电商)。
纯数据库的实现之所以能够满足扣减业务的各项功能要求,主要依赖两点:
- 基于数据库的乐观锁方式保证并发扣减的强一致性;
- 基于数据库的事务实现批量扣减失败进行回滚。
流程图如下:
有一句话说的非常好:一次完整的流程就是先进行数据校验,在其中做一些参数格式校验,这里做接口开发的时候,要保持一个原则就是不信任原则,一切数据都不要相信,都需要做校验判断。
1 | update xxx set leavedAmount=leavedAmount-currentAmount where skuid='xxx' and leavedAmount>=currentAmount; |
此 SQL 采用了类似乐观锁的方式实现了原子性。在 where 后面判断剩余数量大于等于需要的数量,才能成功,否则失败。
扣减完成之后,需要记录流水数据。每一次扣减的时候,都需要外部用户传入一个 uuid 作为流水编号,此编号是全局唯一的。用户在扣减时传入唯一的编号有两个作用:
- 当用户归还数量时,需要带回此编码,用来标识此次返还属于历史上的哪次扣减;
- 进行幂等性控制。当用户调用扣减接口出现超时时,因为用户不知道是否成功,用户可以采用此编号进行重试或反查,在重试时,使用此编号进行标识防重;
库存校验这一步涉及到了读请求,那么必然增加了数据库的压力,所以需要做一些优化。
- 读写分离;
整体的升级策略采用读写分离的方式,另外主从复制直接使用 MySQL 等数据库已有的功能,改动上非常小,只要在扣减服务里配置两个数据源。当客户查询剩余库存,扣减服务中的前置校验时,读取从数据库即可。而真正的数据扣减还是使用主数据库。
读写分离之后,根据二八原则,80% 的均为读流量,主库的压力降低了 80%。但采用了读写分离也会导致读取的数据不准确的问题,不过库存数量本身就在实时变化,短暂的差异业务上是可以容忍的,最终的实际扣减会保证数据的准确性。
- 增加缓存;
在上面基础上,还可以升级,增加缓存:
缓存实现扣减
这和前面的扣减库存其实是一样的。但是此时扣减服务依赖的是 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 | create table task{ |
任务表里存储的内容格式可以为 JSON、XML 等结构化的数据。以 JSON 为例,数据内容大致可以如下:
1 | { |
这里我们肯定是还有一个记录业务数据的库,这里存储的是真正的扣减名企和 SKU 的汇总数据。对于另一个库里面的数据,只需要通过这个表进行异步同步就好了。
扣减流程:
这里和纯缓存的区别在于增加了事务开启与回滚的步骤,以及同步的数据库写入流程。
任务库里存储的是纯文本的 JSON 数据,无法被直接使用。需要将其中的数据转储至实际的业务库里。业务库里会存储两类数据,一类是每次扣减的流水数据,它与任务表里的数据区别在于它是结构化,而不是 JSON 文本的大字段内容。另外一类是汇总数据,即每一个 SKU 当前总共有多少量,当前还剩余多少量(即从任务库同步时需要进行扣减的),表结构大致如下:
1 | create table 流水表{ |
商品的实时数据汇总表,结构如下:
1 | create table 汇总表{ |
在整体的流程上,还是复用了上一讲纯缓存的架构流程。当新加入一个商品,或者对已有商品进行补货时,对应的新增商品数量都会通过 Binlog 同步至缓存里。在扣减时,依然以缓存中的数量为准:
相关推荐
- Spring Boot3 连接 Redis 竟有这么多实用方式
-
各位互联网大厂的后端开发精英们,在日常开发中,想必大家都面临过系统性能优化的挑战。当系统数据量逐渐增大、并发请求不断增多时,如何提升系统的响应速度和稳定性,成为了我们必须攻克的难题。而Redis,这...
- 隧道 ssh -L 命令总结 和 windows端口转发配置
-
摘要:隧道ssh-L命令总结和windows端口转发配置关键词:隧道、ssh-L、端口转发、网络映射整体说明最近在项目中,因为内网的安全密级比较高,只能有一台机器连接内网数据库,推送...
- 火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer
-
火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer综上所述,面试遇到的所有问题,整理成了一份文档,希望大家能够喜欢!!Java面试题分享(Java中高级核心知识全面解析)一、J...
- 「第五期」游服务器一二三面 秋招 米哈游
-
一面下午2点,35分钟golang内存模型golang并发模型golanggc原理过程channel用途,原理redis数据结构,底层实现跳跃表查询插入复杂度进程,线程,协程kill原理除了kil...
- RMQ——支持合并和优先级的消息队列
-
业务背景在一个项目中需要实现一个功能,商品价格发生变化时将商品价格打印在商品主图上面,那么需要在价格发生变动的时候触发合成一张带价格的图片,每一次触发合图时计算价格都是获取当前最新的价格。上游价格变化...
- Redis 中的 zset 为什么要用跳跃表,而不是B+ Tree 呢?
-
Redis中的有序集合使用的是一种叫做跳跃表(SkipList)的数据结构来实现,而不是使用B+Tree。本文将介绍为什么Redis中使用跳跃表来实现有序集合,而不是B+Tree,并且探讨跳跃表...
- 一文让你彻底搞懂 WebSocket 的原理
-
作者:木木匠转发链接:https://juejin.im/post/5c693a4f51882561fb1db0ff一、概述上一篇文章《图文深入http三次握手核心问题【思维导图】》我们分析了简单的一...
- Redis与Java整合的最佳实践
-
Redis与Java整合的最佳实践在这个数字化时代,数据处理速度决定了企业的竞争力。Redis作为一款高性能的内存数据库,以其卓越的速度和丰富的数据结构,成为Java开发者的重要伙伴。本文将带你深入了...
- Docker与Redis:轻松部署和管理你的Redis实例
-
在高速发展的云计算时代,应用程序的部署和管理变得越来越复杂。面对各种操作系统、依赖库和环境差异,开发者常常陷入“在我机器上能跑”的泥潭。然而,容器化技术的兴起,尤其是Docker的普及,彻底改变了这一...
- Java开发中的缓存策略:让程序飞得更快
-
Java开发中的缓存策略:让程序飞得更快缓存是什么?首先,让我们来聊聊什么是缓存。简单来说,缓存是一种存储机制,它将数据保存在更快速的存储介质中,以便后续使用时能够更快地访问。比如,当你打开一个网页时...
- 国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer
-
字节跳动,先面了data部门,3面技术面之后hr说需要实习转正,拒绝,之后另一个部门捞起,四面技术面,已oc分享面经,希望对大家有所帮助,秋招顺利在文末分享了我为金九银十准备的备战资源库,包含了源码笔...
- “快”就一个字!Redis凭什么能让你的APP快到飞起?
-
咱们今天就来聊一个字——“快”!在这个信息爆炸、耐心越来越稀缺的时代,谁不希望自己手机里的APP点一下“嗖”就打开,刷一下“唰”就更新?谁要是敢让咱用户盯着个小圈圈干等,那简直就是在“劝退”!而说到让...
- 双十一秒杀,为何总能抢到?Redis功不可没!
-
一年一度的双十一“剁手节”,那场面,简直比春运抢票还刺激!零点的钟声一敲响,亿万个手指头在屏幕上疯狂戳戳戳,眼睛瞪得像铜铃,就为了抢到那个心心念念的半价商品、限量版宝贝。你有没有发现一个奇怪的现象?明...
- 后端开发必看!为什么说Redis是天然的幂等性?
-
你在做后端开发的时候,有没有遇到过这样的困扰:高并发场景下,同一个操作重复执行多次,导致数据混乱、业务逻辑出错?别担心,很多同行都踩过这个坑。某电商平台就曾因订单创建接口在高并发时不具备幂等性,用户多...
- 开发一个app需要哪些技术和工具
-
APP开发需要一系列技术和工具的支持,以下是对这些技术的清晰归纳和分点表示:一、前端开发技术HTML用于构建页面结构。CSS用于样式设计和布局。JavaScript用于页面交互和逻辑处理。React...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)