Redis实战高并发之扣减库存项目


Posted in Redis onApril 14, 2022

相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等。做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的。这里我今天就讲一下高并发情况下是如何扣减库存的?

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

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

第一种方案:纯MySQL扣减实现

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

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

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

基于上述方案,它包含一个扣减服务和一个数量数据库

Redis实战高并发之扣减库存项目

如果数据量单库压力很大,也可以做主从和分库分表,服务可以做集群等。

Redis实战高并发之扣减库存项目

一次完整的流程就是先进行数据校验,在其中做一些参数格式校验,这里做接口开发的时候,要保持一个原则就是不信任原则,一切数据都不要相信,都需要做校验判断。其次,还可以进行库存扣减的前置校验。比如当前库存中的库存只有8个,而用户要购买10个,此时的数据校验中即可前置拦截,减少对于数据库的写操作。纯读不会加锁,性能较高,可以采用此种方式提升并发量。

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

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

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

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

当用户只购买某个商品一个的时候,如果校验时剩余库存有8个,此时校验通过。但在后续的实际扣减时,因为其他用户也在并发的扣减,可能会出现幻读,此时用户实际去扣减时不足一个,导致失败。这种场景会导致多一次数据库查询,降低整体的扣减性能。这时候可以对MySQL架构进行升级

MySQL架构升级

多一次查询,就会增加数据库的压力,同时对整体性能也有一定的影响。此外,对外提供的查询库存数量的接口也会对数据库产生压力,同时读的请求要远大于写。

根据业务场景分析,读库存的请求一般是顾客浏览商品时产生,而调用扣减库存的请求基本上是用户购买时才触发。用户购买请求的业务价值比读请求会更大,因此对于写需要重点保障。针对上述的问题,可以对MySQL整体架构进行升级

Redis实战高并发之扣减库存项目

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

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

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

Redis实战高并发之扣减库存项目

纯数据库的方案虽然可以避免超卖和少卖的情况,但是并发量实在很低,性能不是很乐观。所以这里再进行升级

第二种方案:缓存实现扣减

Redis实战高并发之扣减库存项目

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

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

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

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

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

第三种方案:数据库+缓存 顺序写的性能更好

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

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

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

顺序写的架构

通过上面的理论就可以得出一个兼具性能和高可靠的扣减架构

Redis实战高并发之扣减库存项目

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

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

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

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

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

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

扣减流程

Redis实战高并发之扣减库存项目

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

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

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

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

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 同步至缓存里。在扣减时,依然以缓存中的数量为准

到此这篇关于Redis高并发情况下并发扣减库存项目实战的文章就介绍到这了!

Redis 相关文章推荐
Redis持久化与主从复制的实践
Apr 27 Redis
详解RedisTemplate下Redis分布式锁引发的系列问题
Apr 27 Redis
redis通过6379端口无法连接服务器(redis-server.exe闪退)
May 08 Redis
redis内存空间效率问题的深入探究
May 17 Redis
springboot使用Redis作缓存使用入门教程
Jul 25 Redis
Redisson实现Redis分布式锁的几种方式
Aug 07 Redis
Redis字典实现、Hash键冲突及渐进式rehash详解
Sep 04 Redis
Jedis操作Redis实现模拟验证码发送功能
Sep 25 Redis
Redis 持久化 RDB 与 AOF的执行过程
Nov 07 Redis
解决Redis启动警告问题
Feb 24 Redis
muduo TcpServer模块源码分析
Apr 26 Redis
Redis批量生成数据的实现
Jun 05 Redis
Redis中key的过期删除策略和内存淘汰机制
解决 Redis 秒杀超卖场景的高并发
redis 解决库存并发问题实现数量控制
Redis超详细讲解高可用主从复制基础与哨兵模式方案
redis复制有可能碰到的问题汇总
Apr 03 #Redis
 Redis 串行生成顺序编码的方法实现
浅谈Redis 中的过期删除策略和内存淘汰机制
You might like
PHP 文件上传进度条的两种实现方法的代码
2007/11/25 PHP
mysqli_set_charset和SET NAMES使用抉择及优劣分析
2013/01/13 PHP
php把大写命名转换成下划线分割命名
2015/04/27 PHP
yii2-GridView在开发中常用的功能及技巧总结
2017/01/07 PHP
jQuery 学习第六课 实现一个Ajax的TreeView
2010/05/17 Javascript
JS常见问题整理(持续更新)
2013/08/06 Javascript
Node.js文件操作详解
2014/08/16 Javascript
jQuery实现高亮显示网页关键词的方法
2015/08/07 Javascript
jquery小火箭返回顶部代码分享
2015/08/19 Javascript
浅析javascript函数表达式
2016/02/10 Javascript
用canvas 实现个图片三角化(LOW POLY)效果
2016/02/18 Javascript
jQuery实现的简单百分比进度条效果示例
2016/08/01 Javascript
Vue.js结合Ueditor富文本编辑器的实例代码
2017/07/11 Javascript
vue scroller返回页面记住滚动位置的实例代码
2018/01/29 Javascript
详解vue-cli 构建项目 vue-cli请求后台接口 vue-cli使用axios、sass、swiper
2018/05/28 Javascript
微信小程序实现弹出层效果
2020/05/26 Javascript
微信小程序实现点击图片放大预览
2019/10/21 Javascript
vue动态循环出的多个select出现过的变为disabled(实例代码)
2019/11/10 Javascript
[30:37]【全国守擂赛】第三周擂主赛 Dark Knight vs. Leopard Gaming
2020/05/04 DOTA
使用Python实现windows下的抓包与解析
2018/01/15 Python
python 定义n个变量方法 (变量声明自动化)
2018/11/10 Python
基于Python和PyYAML读取yaml配置文件数据
2020/01/13 Python
Python dict和defaultdict使用实例解析
2020/03/12 Python
python字符串判断密码强弱
2020/03/18 Python
Python使用socket模块实现简单tcp通信
2020/08/18 Python
解决pytorch下出现multi-target not supported at的一种可能原因
2021/02/06 Python
比利时网上药店: Drogisterij.net
2017/03/17 全球购物
介绍一下HDLC(High-Level Data Link Control)高层数据链路协议
2012/01/21 面试题
程序员岗位职责
2013/11/11 职场文书
水电维修专业推荐信
2014/09/06 职场文书
企业委托书范本
2014/09/13 职场文书
2016年社区国庆节活动总结
2016/04/01 职场文书
新手入门Mysql--概念
2021/06/18 MySQL
SQL Server表分区删除详情
2021/10/16 SQL Server
《金肉人》米特&《航海王》阿鹤声优松岛实因胰脏癌去世 享寿81岁
2022/04/13 日漫
阿里云ECS云服务器快照的概念以及如何使用
2022/04/21 Servers