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

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

将商品进行数据分片,并将分片分散存储在各个 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.mzph.cn/news/192629.shtml

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

相关文章

PyEcharts快速上手_Python数据分析与可视化

PyEcharts快速上手 导入图表类型添加数据设置图表样式输出图表链式调用 导入图表类型 和其他库的导入方法一样&#xff0c;在绘图之前首先要在文件开头导入所需图表类型。 from pyecharts.charts import BarBar 类型是柱状图/条形图在 pyEcharts 中的英文名。 pyEcharts 中有…

vr工业制造流程3D模拟仿真可视化展示

工业仿真3D数字化展示系统具有多方面的独特之处&#xff0c;主要体现在以下几个方面&#xff1a; 1、真实感和交互性&#xff1a;该系统可以将实际的工业设备、产品、场景等进行数字化建模&#xff0c;通过三维图形技术将其呈现在计算机屏幕上&#xff0c;使用户可以在虚拟环境…

Python中的并发编程

目录 一、引言 二、Python中的线程 1、线程的概念 2、创建线程 3、线程同步和锁 4、线程池 三、Python中的进程 1、进程的概念 2、创建进程 四、Python中的异步IO 1、异步IO的概念 2、异步IO的实现 3、异步IO的并发执行 五、总结 一、引言 并发编程是一种计算机…

【漏洞复现】智跃人力资源管理系统GenerateEntityFromTable.aspx接口存在SQL注入漏洞 附POC

漏洞描述 智跃人力资源管理系统是基于B/S网页端广域网平台,一套考勤系统即可对全国各地多个分公司进行统一管控,成本更低。信息共享更快。跨平台,跨电子设备。智跃人力资源管理系统GenerateEntityFromTable.aspx接口处存在SQL注入漏洞,攻击者可通过该漏洞获取数据库中的信…

100W用户、8000W流量在线贺卡应用架构如何优化?

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容&#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作…

GOLAND搭建GIN框架以及基础框架搭建

创建GO环境文件夹 终端输入安装GIN go get -u github.com/gin-gonic/gin如果遇到超时错误 package golang.org/x/net/html: unrecognized import path "golang.org/x/net/html": https fetch: Get "https://golang.org/x/net/html?go-get1": dial tcp …

KubeShark: Kubernetes的Wireshark

Wireshark可以帮助我们抓取并分析网络数据包&#xff0c;理解网络中发生的事情&#xff0c;那是否有工具可以帮助我们理解在K8S集群中发生的事情&#xff0c;帮助排查集群故障&#xff0c;Kubeshark就是这样的工具。原文: KubeShark: Wireshark for Kubernetes[1] 简介 Wiresha…

【动态代理详解】

动态代理 知道什么是动态代理以及动态代理能干什么就可以 代理的概述 什么是动态代理 ​ 使用jdk的反射机制&#xff0c;创建对象的能力&#xff0c; 创建的是代理类的对象。 动态&#xff1a;在程序执行时&#xff0c;调用jdk提供的方法才能创建代理类的对象。jdk动态代理&…

【Oracle】数据库登陆错误:ORA-28000:the account is locked解决方法

问题描述 在连接Oracle数据库的时候出现了ORA-28000:the account is locked报错&#xff0c;登录账号被锁定&#xff0c;出现这种情况就需要将被锁定用户解锁。 解决方法 解锁方法就是通过用system账号登录数据库&#xff0c;然后修改被锁定账户状态&#xff0c;具体如下图所示…

51爱心流水灯32灯炫酷代码

源代码摘自远眺883的文章&#xff0c;大佬是30个灯的&#xff0c;感兴趣的铁汁们可以去看看哦~&#xff08;已取得原作者的许可&#xff09;&#xff1a;基于STC89C51单片机设计的心形流水灯软件代码部分_单片机流水灯代码_远眺883的博客-CSDN博客 由于博主是个小菜鸡&#xff…

selenium+python

selenium 八大查找元素 from selenium import webdriver from selenium.webdriver.common.by import By# 创建一个 WebDriver 实例 driver webdriver.Chrome()# 打开网页 driver.get("https://www.baidu.com/")# 使用 find_element 方法查找元素 element driver.…

springboot 整合 Spring Security 上篇

1.创建springBoot 项目工程(spring6.0的底层、JDK17) 1.添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>配置完成启动访问controller会出现登录…

prometheus部署及与grafana结合应用

一、prometheus 介绍 prometheus server 是 Prometheus组件中的核心部分&#xff0c;负责实现对监控数据的获取&#xff0c;存储以及查询。它会定期从静态配置的监控目标或者基于服务发现自动配置的自标中进行拉取数据&#xff0c;当新拉取到的数据大于配置的内存缓存区时&…

绘制彩色正多边形-第11届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第10讲。 绘制彩色正多边形…

论文解读--Visual Lane Tracking and Prediction for Autonomous Vehicles

自动驾驶汽车视觉车道线跟踪和预测 摘要 我们提出了一种用于自动驾驶汽车跟踪水平道路车道标记位置的可视化方法。我们的方法是基于预测滤波的。预测步骤估计在每个新的图像帧中期望的车道标记位置。它也是基于汽车的运动学模型和嵌入式测程传感器产生的信息。使用适当准备的测…

弱网模拟工具

一、背景 一个人晚上在家通过 Wi-Fi 上网&#xff0c;在线电影播放基本流畅&#xff0c;可一旦在晚间用网高峰期打视频电话就画面糊&#xff0c;这时不仅可能带宽受限了&#xff0c;还可能有较高的丢包率。与有线网络通信相比&#xff0c;无线网络通信受环境影响会更大&#x…

【Java Web学习笔记】 1 - HTML入门

项目代码 https://github.com/yinhai1114/JavaWeb_LearningCode/tree/main/html 零、网页的组成 HTML是网页内容的载体。内容就是网页制作者放在页面上想要让用户浏览的信息&#xff0c;可以包含文字、图片视频等。 CSS样式是表现。就像网页的外衣。比如&#xff0c;标题字体、…

树基本概念+前中后序遍历二叉树

&#x1f308;一、树的基本概念 ☀️1.树的定义&#xff1a;树是一种非线性结构&#xff0c;看起来像一棵倒挂的树&#xff0c;根朝上&#xff0c;而叶朝下。 ☀️2.相关术语 1.根节点&#xff1a;图中的A&#xff0c;无前驱结点 2.叶节点&#xff08;终端节点&#xff09;&a…

第九节HarmonyOS 常用基础组件4-Button

一、Button Button组件主要用来响应点击操作&#xff0c;可以包含子组件。 示例代码&#xff1a; Entry Component struct Index {build() {Row() {Column() {Button(确定, { type: ButtonType.Capsule, stateEffect: true }).width(90%).height(40).fontSize(16).fontWeigh…

Java类的初始化顺序

类初始化顺序遵循以下三个原则&#xff08;优先级依次递减&#xff09; 1、静态对象&#xff08;变量&#xff09;优先于非静态对象&#xff08;变量&#xff09;初始化&#xff0c;其中静态对象&#xff08;变量&#xff09;只初始化一次&#xff0c;而非静态对象&…