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

news2024/9/25 23:22:34

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

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

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

  • 系统全链路刷数会很麻烦(多团队协作),客服外呼也有额外成本
  • 最主要原因,客户抢到订单又被取消,严重影响客户体验,甚至引发客诉产生公关危机
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 then
  return 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 为大小,分片所有记录key
    final 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 指定扣减记录 的map
    final 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.coloradmin.cn/o/2077591.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

node.js使用express框架实现api接口开发(从零开始,超简单可直接复制)

目录 一、效果图 二、实现 1、引入express框架依赖 2、 新建启动文件&#xff08;/server/index.js&#xff09; 3、新建接口函数文件&#xff08;/server/router.js&#xff09; 一、效果图 二、实现 1、引入express框架依赖 在项目文件夹根目录下&#xff0c;打开控制台…

ShenNiusModularity:一款基于 .NET Core 框架研发的自媒体内容管理系统

项目介绍 ShenNiusModularity是一款基于 .NET Core 框架研发的、开源、免费的自媒体内容管理系统。项目秉承大道至简的原则开发&#xff0c;坚持业务模块最低复杂度复用&#xff0c;代码方面追求简单、高效、实用。适合小白入门进阶&#xff0c;同样适用老手上路干活。 项目包…

Python青少年简明教程:字符串

Python青少年简明教程&#xff1a;字符串 字符串&#xff08;string&#xff09;是用于表示文本的数据类型。它是不可变的序列类型&#xff0c;即一旦创建&#xff0c;字符串中的字符就无法改变。 下面对Python中字符串的详细介绍&#xff0c;包括字符串的创建、操作和常见方法…

MySQL 集群技术全攻略:从搭建到优化(下)

目录 四.mysql高可用之组复制 (MGR) 1.组复制流程 2.组复制单主和多主模式 3.实现mysql组复制 五.mysql-router&#xff08;mysql路由&#xff09; 1.Mysql route的部署方式 六.mysql高可用之MHA 1.MHA架构图 2.为什么要用MHA&#xff1f; 3.MHA 的组成 4.什么是 MH…

网络 (tcp)

客户端 /*************************************************************************> File Name: client.c> Author: yas> Mail: rage_yashotmail.com> Created Time: Thu 22 Aug 2024 04:04:26 PM CST******************************************************…

从零开始学习SLAM六(单应矩阵)

本文参考&#xff1a;计算机视觉life 概念 单应性&#xff08;homography&#xff09;是指两个平面之间的一种保直线性的对应关系。如果一个平面上的点集经过某种变换后&#xff0c;在另一个平面上形成的新点集仍然保持原来的线性特性&#xff08;如共线的点仍然共线&#xf…

一起搭WPF界面之View的简单设计一

一起搭WPF界面之View的简单设计一 1 前言2 界面预期设想3 基础的实现步骤3.1 界面划分3.1.1 基础框架代码&#xff1a;3.1.2 实现效果 4 界面花样设计4.1 花样设计4.2 界面源代码4.3 错误提醒4.3.1 错误14.3.2 错误2 总结 1 前言 基于上一篇的window、Gird、Border的简单介绍&…

XSS LABS - Level 13 过关思路

关注这个靶场的其他相关笔记&#xff1a;XSS - LABS —— 靶场笔记合集-CSDN博客 0x01&#xff1a;过关流程 进入靶场&#xff0c;老样子&#xff0c;右击&#xff0c;查看页面源码&#xff0c;找找不同&#xff1a; 可以看到&#xff0c;本关又多了一个新字段 t_cook&#xf…

关于 Vue/React 的 cli 中运用 webpack 打包的原理简单解析

webpack、webpack-cli的打包 关于 webpack 对前端工程中进行资源文件进行打包处理的过程中&#xff0c;运用到的核心插件主要是 webpack 和 webpack-cli&#xff0c;在 react 和 vue 对于打包各自工程中的 cli 则是进行了自定义的构建&#xff0c;专门用于项目打包的 …

【MySQL-25】万字总结<锁>——(全局锁&行级锁&表级锁)【共享锁,排他锁】【间隙锁,临键锁】【表锁,元数据锁,意向锁】

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

Bytebase 2.22.2 - 允许在工作空间为群组分配角色

&#x1f680; 新功能 允许在工作空间给群组分配角色。 支持禁用邮箱密码登录&#xff0c;仅允许 SSO 登录的设置项。 新增 Postgres SQL 审核规则&#xff1a;禁止在列上设置会变化的默认值。 &#x1f514; 重大变更 下线项目内的变更历史页面&#xff1b;所有变更历史仍可…

Phoenix

Apache Phoenix 是一个开源的关系数据库层&#xff0c;运行在 Apache HBase 之上&#xff0c;旨在为 HBase 提供 SQL 查询能力和优化的存储引擎。它允许用户使用标准的 SQL 查询和事务语义来管理 HBase 中的数据&#xff0c;并且可以与现有的大数据生态系统无缝集成。Phoenix 通…

IOS开发 铃声制作(库乐队)

IOS开发&#xff0c;实现铃声制作功能。 在IOS端&#xff0c;要设置铃声都是通过库乐队来制作的。 先看一下库乐队中铃声的文件结构。下面是弄的一个示例的文件&#xff0c;文件信息如下&#xff1a; 我们右击文件&#xff0c;点击显示包内容如下&#xff1a; 能看到一个aiff格…

解决ssl certificates updated-生成环境中的实例

应原来小伙伴的要求&#xff0c;生产环境出错了&#xff0c;是harbor的ssl cert过期了&#xff0c;也因为使用的是免费的ssl证书&#xff0c;现在无法正常使用harbor&#xff0c;所以贴来了2023年1月曾经搭建的文档&#xff0c;希望能解决问题。^v^. -------------------------…

25 filedialog组件

Tkinter filedialog 组件使用指南 Tkinter 的 filedialog 组件提供了一个图形界面&#xff0c;用于打开和保存文件。它允许用户通过标准的文件选择对话框来选择文件&#xff0c;非常适合需要文件操作的GUI应用程序。以下是对 filedialog 组件的详细说明和一个使用案例。 file…

爆改YOLOv8 |YOLOv8融合SEAM注意力机制

1&#xff0c;本文介绍 SEAM&#xff08;Spatially Enhanced Attention Module&#xff09;是一个注意力网络模块&#xff0c;旨在解决面部遮挡导致的响应损失问题。通过使用深度可分离卷积和残差连接的组合&#xff0c;SEAM模块增强未遮挡面部的响应。深度可分离卷积在每个通…

Xshell 连接 Ubuntu 服务器失败问题(Connection failed)

目录 Xshell 连接 Ubuntu 服务器失败问题&#xff08;Connection failed&#xff09; 1.查看Ubuntu中是否安装 sshd 2.在Ubuntu中安装sshd 3.需要打开Ubuntu中新安装的sshd 4.在检查Ubuntu中sshd是否安装成功 5.临时关闭Ubuntu中的防火墙 6.Xshell 连接 Ubuntu 服务器成…

认知杂谈24

今天分享 有人说的一段争议性的话 I I 《人生逆袭的关键&#xff1a;开窍带来的转变》 在女人的生活里啊&#xff0c;最宝贝的东西可不是那些金银首饰啥的&#xff0c;也不只是那些起起落落的经历&#xff0c;更不是偶尔碰到的贵人帮忙。真正无价的&#xff0c;是在某个瞬间…

构建buildroot根文件系统

目录 1.确定gcc工具版本2.下载Buildroot源码并编译2.1 下载Buildroot源码2.2 配置Buildroot2.2.1 配置 Target options2.2.2 配置交叉编译工具链2.2.3 配置 System configuration2.2.4 配置 Filesystem images2.2.5 禁止编译 Linux 内核和 uboot2.2.6 编译Buildroot源码2.2.7 查…

Bootloader中的PBL、SBL的区别

从0开始学习CANoe使用 从0开始学习车载车身 相信时间的力量 星光不负赶路者&#xff0c;时光不负有心人。 目录 1.概述2.BootloaderPBLSBL3.SBL存在意义4.PBL存在意义 1.概述 应用软件和应用数据可以同时编程或者相互独立编程&#xff0c;不允许Boot Loader在软件运行时被非法…