高并发业务下的库存扣减技术方案设计

2024-08-27 11:12

本文主要是介绍高并发业务下的库存扣减技术方案设计,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

扣减库存需要查询库存是否足够:

  • 足够就占用库存
  • 不够则返回库存不足(这里不区分库存可用、占用、已消耗等状态,统一成扣减库存数量,简化场景)

并发场景,若 查询库存和扣减库存不具备原子性,就可能超卖,而高并发场景超卖概率会增高,超卖数额也会增高。处理超卖的确麻烦:

  • 系统全链路刷数会很麻烦(多团队协作),客服外呼也有额外成本
  • 最主要原因,客户抢到订单又被取消,严重影响客户体验,甚至引发客诉产生公关危机
1.4.1 实现逻辑

常用方案redis+lua,借助redis单线程执行+lua脚本中的逻辑,可在一次执行中顺序完成的特性达到原子性(叫排它性更准确,因为不具备回滚动作,异常情况需自己手动编码回滚)。

lua脚本基本实现

-- 1. 获取库存缓存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]-- 2. 获取剩余库存数量
local stock = tonumber(redis.call('get', hot_item_stock))-- 3. 购买数量
local buy_qty = tonumber(ARGV[1])-- 4. 如果库存小于购买数量,则返回1,表达库存不足
if stock < buy_qty thenreturn 1
end-- 5. 库存足够,更新库存数量
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))-- 6. 扣减成功则返回2,表达库存扣减成功
return 2

但脚本还有一些问题:

  • 不具备幂等性,同个订单多次执行会导致重复扣减,手动回滚也无法判断是否会回滚过,会出现重复增加的问题

  • 不具备可追溯性,不知道库存被谁被哪个订单扣减了

增强后的lua脚本:

-- 1. 获取库存扣减记录缓存 key KYES[2] = hot_{itemCode-skuCode}_deduction_history
local  hot_deduction_history = KYES[2]-- 2. 使用 Redis Cluster hash tag 保证 stock 和 history 在同一个槽
local exist = redis.call('hexists', hot_deduction_history, ARGV[2])
-- 3. 请求幂等判断,存在返回0,表达已扣减过库存
if exist == 1 then return 0 end-- 4. 获取库存缓存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]-- 5. 获取剩余库存数量
local stock = tonumber(redis.call('get', hot_item_stock))-- 6. 购买数量
local buy_qty = tonumber(ARGV[1])-- 7. 如果库存小于购买数量 则返回1,表达库存不足
if stock < buy_qty then return 1 end-- 8. 库存足够
-- 9. 1.更新库存数量
-- 10. 2.插入扣减记录 ARGV[2] = ${扣减请求唯一key} - ${扣减类型} 值为 buy_qty
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))
redis.call('hset', hot_deduction_history, ARGV[2], buy_qty)-- 11. 如果剩余库存等于0则返回2,表达库存已为0
if stock == 0 then return 2 end-- 12. 剩余库存不为0返回 3 表达还有剩余库存
return 3 end

利用Redis Cluster hash tag保证stock和history在同个槽,这样lua脚本才能正常执行。

因为正常要求 Lua 脚本操作的键必须在同一个 slot 中。

@Override
public <T, R> RFuture<R> evalReadAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {NodeSource source = getNodeSource(key);return evalAsync(source, true, codec, evalCommandType, script, keys, false, params);
}private NodeSource getNodeSource(String key) {int slot = connectionManager.calcSlot(key);return new NodeSource(slot);}

利用hot_deduction_history,判断扣减请求是否执行过,以实现幂等性。

借助hot_deduction_history的V值判断追溯扣减来源,如:用户A的交易订单A的扣减请求,或用户B的借出单B的扣减请求。

回滚逻辑先判断hot_deduction_history里有没有 ${扣减请求唯一key}:

  • 有,则执行回补逻辑
  • 没有,则认定回补成功

但该逻辑依旧有漏洞,如(消息乱序消费),订单扣减库存超时成功触发了重新扣减库存,但同时订单取消触发了库存扣减回滚,回滚逻辑先成功,超时成功的重新扣减库存就会成为脏数据留在redis里。

1.4.2 处理方案

有两种:

  • 追加对账,定期校验hot_deduction_history中数据对应单据的状态,对于已经取消的单据追加一次回滚请求,存在时延(业务不一定接受)以及额外计算资源开销
  • 使用顺序消息,让扣减库存、回滚库存都走同一个MQ topic的有序队列,借助MQ消息的有序性保证回滚动作一定在扣减动作后面执行,但有序串行必然带来性能下降
1.4.3 高可用

Redis终究是内存,一旦服务中断,数据就消失。所以需要追加保护数据不丢失的方案。

运用Redis部署的高可用方案:

  • 采用Redis Cluster(数据分片+ 多副本 + 同步多写 + 主从自动选举)
  • 多写节点分(同城异地)多中心防止意外灾害

定期归档冷数据。定期 + 库存为0触发redis数据往DB同步,流程如下:

CDC分发数据时,秒杀商品,hot_deduction_history的数据量不高,可以一次全量同步。但如果是普通大促商品,就需要再追加一个map动作分批处理,以保证每次执行CDC的数据量恒定,不至于一次性数据量太大出现OOM。代码如下:

/*** 对任务做分发* @param stockKey 目标库存的key值*/
public void distribute(String stockKey) {final String historyKey = StrUtil.format("hot_{}_deduction_history", stockKey);// 获取指定库存key 所有扣减记录的key(生产请分页获取,防止数据量太多)final List<String> keys = RedisUtil.hkeys(historyKey, stockKey);// 以 100 为大小,分片所有记录keyfinal List<List<String>> splitKeys = CollUtil.split(keys, 100);// 将集合分发给各个节点执行map(historyKey, splitKeys);
}/*** 对单页任务做执行* @param historyKey 目标库存的key值* @param stockKeys 要执行的页面大小*/
public void mapExec(String historyKey, List<String> stockKeys) {// 获取指定库存key 指定扣减记录 的mapfinal Map<String, String> keys = RedisUtil.HmgetToMap(historyKey, stockKeys);keys.entrySet().stream().map(stockRecordFactory::of).forEach(stockRecord -> {// (幂等 + 去重) 扣减 + 保存记录stockConsumer.exec(stockRecord);// 删除redis中的 key 释放空间RedisUtil.hdel(historyKey, stockRecord.getRecordRedisKey());});
}
1.4.4 为啥不走DB

商品库存数据在DB最终会落到单库单表的一行数据。无法通过分库分表提高请求的并行度。而在单节点场景,数据库吞吐远不如Redis。最基础的原因:IO效率不是一个量级,DB是磁盘操作,而且还可能要多次读盘,Redis是一步到位的内存操作。

同时,一般DB都是提交读隔离级别,为保证原子性,执行库存扣减,得加锁,无论悲观乐观。不仅性能差(抢不到锁要等待),而且因为非公平竞争,易出现线程饥饿。而redis是单线程操作,不存在共享变量竞争。

有些优化思路,如合并扣减,走批降低请求的并行连接数。但伴随的集单的时延,以及按库分批的诉求;还有拆库存行,商品A100个库存拆成2行商品A50库存,然后扣减时分发请求,以提高并行连接数(多行可落在不同库来提高并行连接数)。但伴随的:

  • 复杂的库存行拆分管理(把什么库存行在什么时候拆分到哪些库)
  • 部分库存行超卖的问题(加锁优化就又串行了,不加总量还有库存,个别库存行不足是允许一定系数超卖还是返回库存不足就是一个要决策的问题)

部分头部电商采用弱缓存抗读(非库存不足,不实时更新),DB抗写的方案。该方案前提在于,通过一系列技术方案,流量落到库存已相对低且平滑了(扛得住,不用再自己实现操作原子性)。

关注我,紧跟本系列专栏文章,咱们下篇再续!

作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。

各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。

负责:

  • 中央/分销预订系统性能优化

  • 活动&券等营销中台建设

  • 交易平台及数据中台等架构和开发设计

  • 车联网核心平台-物联网连接平台、大数据平台架构设计及优化

  • LLM Agent应用开发

  • 区块链应用开发

  • 大数据开发挖掘经验

  • 推荐系统项目

    目前主攻市级软件项目设计、构建服务全社会的应用系统。

参考:

  • 编程严选网

    本文由博客一文多发平台 OpenWrite 发布!

这篇关于高并发业务下的库存扣减技术方案设计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111506

相关文章

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

金融业开源技术 术语

金融业开源技术  术语 1  范围 本文件界定了金融业开源技术的常用术语。 本文件适用于金融业中涉及开源技术的相关标准及规范性文件制定和信息沟通等活动。

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

业务协同平台--简介

一、使用场景         1.多个系统统一在业务协同平台定义协同策略,由业务协同平台代替人工完成一系列的单据录入         2.同时业务协同平台将执行任务推送给pda、pad等执行终端,通知各人员、设备进行作业执行         3.作业过程中,可设置完成时间预警、作业节点通知,时刻了解作业进程         4.做完再给你做过程分析,给出优化建议         就问你这一套下

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出 在数字化时代,文本到语音(Text-to-Speech, TTS)技术已成为人机交互的关键桥梁,无论是为视障人士提供辅助阅读,还是为智能助手注入声音的灵魂,TTS 技术都扮演着至关重要的角色。从最初的拼接式方法到参数化技术,再到现今的深度学习解决方案,TTS 技术经历了一段长足的进步。这篇文章将带您穿越时

系统架构设计师: 信息安全技术

简简单单 Online zuozuo: 简简单单 Online zuozuo 简简单单 Online zuozuo 简简单单 Online zuozuo 简简单单 Online zuozuo :本心、输入输出、结果 简简单单 Online zuozuo : 文章目录 系统架构设计师: 信息安全技术前言信息安全的基本要素:信息安全的范围:安全措施的目标:访问控制技术要素:访问控制包括:等保

前端技术(七)——less 教程

一、less简介 1. less是什么? less是一种动态样式语言,属于css预处理器的范畴,它扩展了CSS语言,增加了变量、Mixin、函数等特性,使CSS 更易维护和扩展LESS 既可以在 客户端 上运行 ,也可以借助Node.js在服务端运行。 less的中文官网:https://lesscss.cn/ 2. less编译工具 koala 官网 http://koala-app.

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

Spring的设计⽬标——《Spring技术内幕》

读《Spring技术内幕》第二版,计文柯著。 如果我们要简要地描述Spring的设计⽬标,可以这么说,Spring为开发者提供的是⼀个⼀站式的轻量级应⽤开发框架(平台)。 作为平台,Spring抽象了我们在 许多应⽤开发中遇到的共性问题;同时,作为⼀个轻量级的应⽤开发框架,Spring和传统的J2EE开发相⽐,有其⾃⾝的特点。 通过这些⾃⾝的特点,Spring充分体现了它的设计理念:在