基于社区电商的Redis缓存架构-库存模块缓存架构(下)

news2025/1/24 8:26:34

基于缓存分片的下单库存扣减方案

将商品进行数据分片,并将分片分散存储在各个 Redis 节点中,那么如何计算每次操作商品的库存是去操作哪一个 Redis 节点呢?

我们对商品库存进行了分片存储,那么当扣减库存的时候,操作哪一个 Redis 节点呢?

通过轮询的方式选择 Redis 节点,在 Redis 中通过记录商品的购买次数(每次扣减该商品库存时,都对该商品的购买次数加 1),key 为 product_stock_count:{skuId},通过该商品的购买次数对 Redis 的节点数取模,拿到需要操作的 Redis 节点,再进行扣减

如果只对这一个 Redis 进行操作,可能该 Redis 节点的库存数量不够,那么就去下一个 Redis 节点中判断库存是否足够扣减,如果遍历完所有的 Redis 节点,库存都不够的话,那么就需要将所有 Redis 节点的库存数量进行合并扣减了,合并扣减库存的流程为:

  • 先累加所有 Redis 节点上的库存数量
  • 判断所有的库存数量是否足够扣减,如果够的话,就去遍历所有的 Redis 节点进行库存的扣减;如果不够,返回库存不足即可

库存在高并发场景下,写操作还是比较多的,因此还是以 Redis 作为主存储,DB 作为辅助存储

用户下单之后,Redis 中进行库存扣减流程如下:

在这里插入图片描述

出库主要有 2 个步骤:

  • Redis 中进行库存扣除
  • 将库存扣除信息进行异步落库

那么异步落库是通过 MQ 实现的,主要记录商品出库的一些日志信息,这里讲一下 Redis 中进行库存扣除的代码是如何实现的,在缓存中扣除库存主要分为 3 个步骤:

  • 拿到需要操作的 Redis 节点,进行库存扣除
  • 如果该 Redis 节点库存不足,则去下一个节点进行库存扣除
  • 如果所有 Redis 节点库存都不足,就合并库存进行扣除

先来说一下第一步,如何拿到需要操作的 Redis 节点,我们上边已经说了,通过轮询的方式,在 Redis 中通过 key:product_stock_count:{skuId} 记录对应商品的购买次数,用购买次数对 Redis 节点数取模,拿到需要操作的 Redis 节点的下标

这里该 Redis 节点库存可能不够,我们从当前选择的 Redis 节点开始循环,如果碰到库存足够的节点,就进行库存扣除,并退出不再继续循环,循环 Redis 节点进行库存扣除代码如下:

// incrementCount:商品的购买次数
Object result;
// 轮询 Redis 节点进行库存扣除
for (long i = incrementCount; i < incrementCount + redisCount - 1; i ++) {
  /**
   * jedisManager.getJedisByHashKey(hashKey) 这个方法就是将传入的 count 也就是 hashKey 这个参数
   * 对 Redis 的节点数量进行取模,拿到一个下标,去 List 集合中取出该下标对应的 Jedis 客户端
   */
  try (Jedis jedis = jedisManager.getJedisByHashKey(i)){
      // RedisLua.SCRIPT:lua 脚本
      // productStockKey:存储商品库存的 key:"product_stock:{skuId}"
      // stockNum 需要扣除的库存数量
      result = jedis.eval(RedisLua.SCRIPT, CollUtil.toList(productStockKey), CollUtil.toList(String.valueOf(stockNum));
  }
  if (Objects.isNull(result)) {
	continue;
  }
  if (Integer.valueOf(result+"") > 0){
	deduct = true;
	break;
  }
}
// 如果单个 Redis 节点库存不足的话,需要合并库存扣除
if (!deduct){
	// 获取一下当前的商品总库存,如果总库存也已不足以扣减则直接失败
	BigDecimal sumNum = queryProductStock(skuId);
	if (sumNum.compareTo(new BigDecimal(stockNum)) >=0 ){
        // 合并扣除库存的核心代码
		mergeDeductStock(productStockKey,stockNum);
	}
	throw new InventoryBizException("库存不足");
}

下边看一下库存扣除的 lua 脚本:

/**
 * 扣减库存
 * 先拿到商品库存的值:stock
 * 再拿到商品需要扣除或返还的库存数量:num
 * 如果 stock - num <= 0,说明库存不足,返回 -1
 * 扣除成功,返回 -2
 * 如果该商品库存不存在,返回 -3
 */
public static final String SCRIPT  =
        "if (redis.call('exists', KEYS[1]) == 1) then"
        + "    local stock = tonumber(redis.call('get', KEYS[1]));"
        + "    local num = tonumber(ARGV[1]);"
        + "    local results_num = stock - num"
        + "    if (results_num <= 0) then"
        + "        return -1;"
        + "    end;"
        + "    if (stock >= num) then"
        + "            return redis.call('incrBy', KEYS[1], 0 - num);"
        + "        end;"
        + "    return -2;"
        + "end;"
        + "return -3;";

对于单个 Redis 节点的库存扣除操作已经说完了,就是先选择 Redis 节点,再执行 lua 脚本扣除即可,如果发现所有 Redis 节点库存足够扣除,就需要合并库存,再进行扣除,合并库存扣除的代码如下:

private void mergeDeductStock(String productStockKey, Integer stockNum){
    // 执行多个分片的扣除扣减,对该商品的库存操作上锁,保证原子性
    Map<Long,Integer> fallbackMap = new HashMap<>();
    // 拿到 Redis 总节点数
    int redisCount = cacheSupport.getRedisCount();
    try {
        // 开始循环扣减库存
        for (long i = 0;i < redisCount; i++){
            if (stockNum > 0){
                // 对当前 Redis 节点进行库存扣除,这里返回的结果 diffNum 表示当前节点扣除库存后,还有多少库存未被扣除
                Object diffNum = cacheSupport.eval(i, RedisLua.MERGE_SCRIPT, CollUtil.toList(productStockKey), CollUtil.toList(stockNum + ""));
                if (Objects.isNull(diffNum)){
                    continue;
                }
                // 当扣减后返回得值大于0的时候,说明还有库存未能被扣减,对下一个分片进行扣减
                if (Integer.valueOf(diffNum+"") >= 0){
                    // 存储每一次扣减的记录,防止最终扣减还是失败进行回滚
                    fallbackMap.put(i, (stockNum - Integer.valueOf(diffNum+"")));
                    // 重置抵扣后的库存
                    stockNum = Integer.valueOf(diffNum+"");

                }
            }
        }
        // 完全扣除所有的分片库存后,还是未清零,则回退库存返回各自分区
        if (stockNum > 0){
            fallbackMap.forEach((k, v) -> {
                Object result = cacheSupport.eval(k, RedisLua.SCRIPT, CollUtil.toList(productStockKey), CollUtil.toList((0 - v) + ""));
                log.info("redis实例[{}] 商品[{}] 本次库存不足,扣减失败,返还缓存库存:[{}], 剩余缓存库存:[{}]", k,productStockKey, v, result);
            });
            throw new InventoryBizException("库存不足");
        }

    } catch (Exception e){
        e.printStackTrace();
        // 开始循环返还库存
        fallbackMap.forEach((k, v) -> {
            cacheSupport.eval(k, RedisLua.SCRIPT,CollUtil.toList(productStockKey),CollUtil.toList((0-v)+""));
        });
        throw new InventoryBizException("合并扣除库存过程中发送异常");
    }
}

在合并扣除库存中,主要有两个 lua 脚本:RedisLua.MERGE_SCRIPTRedisLua.SCRIPT,第一个用于扣除库存,第二个用于返还库存

第二个 lua 脚本上边在库存扣减的时候,已经说过了,我们只需要将参数加个负号即可,原来是扣除库存,这里添加库存就可以返还了

来看一下第一个 lua 脚本:

/**
 * 合并库存扣减
 * stock:该节点拥有库存
 * num:需要扣除库存
 * diff_num:扣除后剩余库存(如果该节点库存不足,则是负数)
 * 如果节点没有库存,返回 -1
 * 如果节点库存不足,令 num = stock,表示将该节点库存全部扣除完毕
 * 最后如果 diff_num 是负数,表示还有还有库存未扣减完毕,返回进行扣减
 */
public static final String MERGE_SCRIPT  =
        "if (redis.call('exists', KEYS[1]) == 1) then\n" +
        "    local stock = tonumber(redis.call('get', KEYS[1]));\n" +
        "    local num = tonumber(ARGV[1]);\n" +
        "    local diff_num = stock - num;\n" +
        "    if (stock <= 0) then\n" +
        "        return -1;\n" +
        "    end;\n" +
        "    if (num > stock) then\n" +
        "        num = stock;\n" +
        "    end;\n" +
        "    redis.call('incrBy', KEYS[1], 0 - num);\n" +
        "    if (diff_num < 0) then\n" +
        "        return 0-diff_num;\n" +
        "    end;\n" +
        "    return 0;\n" +
        "end;\n" +
        "return -3;";

总结

那么库存扣减的整个流程也就说完了,接下来总结一下,库存入库流程为:

  • DB 记录入库记录
  • Redis 对库存进行分片,采用渐进性写入缓存

库存出库流程为:

  • 轮询 Redis 节点进行扣除,如果所有节点库存不足,则合并库存进行扣除
  • 如果库存扣除成功,则 DB 记录出库记录

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1279398.html

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

相关文章

3.4_1 java自制小工具 - pdf批量转图片

相关链接 目录参考文章&#xff1a;pdf转图片(apache pdfbox)参考文章&#xff1a;GUI界面-awt参考文章&#xff1a;jar包转exe(exe4j)参考文章&#xff1a;IDEA导入GIT项目参考文章&#xff1a;IDEA中使用Gitee管理代码gitee项目链接&#xff1a;pdf_2_image网盘地址&#xf…

HCIP-十六、IGMPPIM-SM 组播

十六、IGMP&PIM-SM 组播 IGMP实验拓扑实验需求及解法1. 配置各设备IP地址2. R1启用组播功能&#xff0c;并在g0/0/0和g0/0/1上开启pim dm3. R1的g0/0/1开启igmp协议 PIM-SM实验拓扑实验需求及解法1.配置各设备IP地址。2.运行IGP3.R1/2/3/4运行PIM-SM IGMP 实验拓扑 实验需…

内网穿透工具获取一个公网ip

下载地址&#xff1a;点击即可下载很简单 然后将他复制到上面的命令行窗口直接回车

SQL自学通之查询--SELECT语句的使用

一、前言 1、目标 在今天你将学习到以下内容&#xff1a; l 如何写SQL的查询 l 将表中所有的行选择和列出 l 选择和列出表中的选定列 l 选择和列出多个表中的选定列 2、背景 在上篇中我们简要地介绍了关系型数据库系统所具有的强大功能 在对 SQL 进行了 简要的介绍中我们…

scrapy-redis

一、什么是scrapy-redis Scrapy-Redis 是 Scrapy 框架的一个扩展&#xff0c;它提供了对 Redis 数据库的支持&#xff0c;用于实现分布式爬取。通过使用 Scrapy-Redis&#xff0c;你可以将多个 Scrapy 进程连接到同一个 Redis 服务器&#xff0c;共享任务队列和去重集&#xf…

食物相关的深度学习数据集合集—食物、饮料、肉类、餐具等数据集

最近收集了一大波与食物酒水相关的数据集&#xff0c;包含食物、饮料、肉类、餐具等不同等类型的数据集&#xff0c;废话不多说&#xff0c;给大家逐一介绍&#xff01;&#xff01; 1、自制啤酒配方数据库 超过20万自制啤酒配方数据库&#xff0c;数据集包含不同精酿啤酒的名…

C# WPF上位机开发(绘图软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 本身c# wpf可以看成是生产力工具&#xff0c;它的意义在于可以快速根据业务的情况&#xff0c;把产品模型搭建出来。这一点不像c/c&#xff0c;需要…

4.OpenResty系列之Nginx负载均衡

1. 负载均衡配置 上篇文章中&#xff0c;代理仅仅指向一个服务器。但是&#xff0c;网站在实际运营过程中&#xff0c;大部分都是以集群的方式运行&#xff0c;这时需要使用负载均衡来分流。nginx 也可以实现简单的负载均衡功能。 假设这样一个应用场景&#xff1a;将应用部署…

智能优化算法应用:基于狮群算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于狮群算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于狮群算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.狮群算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

想进国家电网,电气类专业都有哪些就业方向呢?

电气工程及自动化专业的主干课程都有哪些&#xff0c;笔者跟你分享一下就业方向都有哪些主要课程呢&#xff1f;包含电路原理、模拟电子技术、数字电子技术工程、电磁场、微机原理与接口技术、自动控制原理、电机学、电力电子技术、电力系统分析等等。 电气类专业都有哪些就业方…

使用idea如何快速的搭建ssm的开发环境

文章目录 唠嗑部分言归正传1、打开idea&#xff0c;点击新建项目2、填写信息3、找到pom.xml先添加springboot父依赖4、添加其他依赖5、编写启动类、配置文件6、连接创建数据库、创建案例表7、安装MybatisX插件8、逆向工程9、编写controller10、启动项目、测试 结语 唠嗑部分 小…

技术阅读周刊第第8️⃣期

技术阅读周刊&#xff0c;每周更新。 历史更新 20231103&#xff1a;第四期20231107&#xff1a;第五期20231117&#xff1a;第六期20231124&#xff1a;第七期 Prometheus vs. VictoriaMetrics (VM) | Last9 URL: https://last9.io/blog/prometheus-vs-victoriametrics/?refd…

大文件分片上传、分片进度以及整体进度、断点续传(一)

大文件分片上传 效果展示 前端 思路 前端的思路&#xff1a;将大文件切分成多个小文件&#xff0c;然后并发给后端。 页面构建 先在页面上写几个组件用来获取文件。 <body><input type"file" id"file" /><button id"uploadButton…

VisionPro---PatMaxTool工具使用

CogPMAlignTool PatMax是一种图案位置搜索技术&#xff08;识别定位&#xff09;&#xff0c;PatMax图案不依赖于像素格栅&#xff0c;是基于边缘特征的模板匹配而不是基于像素的模板匹配&#xff0c;支持图像中特征的旋转与缩放&#xff0c;边缘特征表示图像中不同区域间界限…

Redis-安装、配置和修改配置文件、以及在Ubuntu和CentOS上设置Redis服务的开机启动和防火墙设置,以及客户端连接。

目录 1. Redis简介 2. 离线安装 2.1 准备工作 2.2 解压、安装 2.3 修改配置文件 2.4 redis服务与关闭 2.5 redis服务的开机启动 2.5.1 Ubuntu上的配置 2.5.2 centos上的配置 3. 在线安装 4. 设置防火墙 5. 客户端连接 1. Redis简介 Redis 是完全开源免费的&#x…

配置typroa上传图片到gitee

在typora这个位置下载插件 在picgo.exe文件夹下输入cmd 打开命令行输入如下命令安装相关插件 .\picgo install gitee-uploader .\picgo install super-prefix 之后按照官方文档更改相关配置 官方文档参考 https://picgo.github.io/PicGo-Core-Doc 博客参考&#xff1a;…

JSP学习资源网站系统eclipse定制开发mysql数据库BS模式java编程

一、源码特点 java 学习资源网站系统是一套完善的web设计系统 &#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c;…

FL Studio水果软件2024简体中文语言版本下载

Fl Studio21是最好的音乐制作软件&#xff0c;但它的成本超过300美元......一个年轻的新音乐创作者怎么能从上到下&#xff0c;地球上没有比 FL Studio 21 更完整的音乐制作软件了。14 年来&#xff0c;它一直是行业领导者&#xff0c;并且随着随后的每一次更新&#xff08;在此…

字符函数 和 字符串函数

今天我打算介绍一些字符函数和字符串函数&#xff0c;有一些字符串函数我实现了模拟&#xff0c;但文章中没有放出来&#xff0c;如果需要的欢迎来到我的gitee里面拿取&#xff08;在test.c11-23里面&#xff09; 这是我的gitee:小汐 (lhysxx) - Gitee.com 字符函数 1. islow…

编程怎么学才能快速入门,分享一款中文编程工具快速学习编程思路,中文编程工具之边条主控菜单构件简介

编程怎么学才能快速入门&#xff0c;分享一款中文编程工具快速学习编程思路&#xff0c;中文编程工具之边条主控菜单构件简介 一、前言 零基础自学编程&#xff0c;中文编程工具下载&#xff0c;中文编程工具构件之扩展系统菜单构件教程编程系统化教程链接https://jywxz.blog…