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

库存模块缓存架构

我们先来分析一下库存模块的业务场景,分为入库和出库,入库的话,在库存模块中需要添加库存,由于库存也是 写多读多 的场景,那么也是以 Redis 作为主存储,MySQL 作为辅助存储

出库的话,是在用户下单时,需要去库存中进行减库存的操作,并且用户退款时,需要增加库存

那么库存模块是存在高并发写的情况的,通过对商品库存进行分片存储,存储在多台 Redis 节点上,就可以将高并发的请求分散在各个 Redis 节点中,并且提供了单台 Redis 节点库存不足时的合并库存的功能

先来说一下如何对商品库存进行缓存分片,比如说有 100 个商品,Redis 集群有 5 个节点,先将 100 个商品拆分为 5个分片,再将 5 个分片分散到 Redis 集群的各个节点中,每个节点 1 个分片,那么也就是每个 Redis 节点存储 20 个商品库存

那么对于该商品的瞬间高并发的操作,会分散的打到多个 Redis 节点中,库存分片的数量一般和 Redis 的节点数差不多

这里分片库存的话,我们是在对商品进行入库的时候实现的,商品在入库的时候,先去 DB 中异步落库,然后再将库存分片写入各个 Redis 节点中,这里写入的时候采用渐进性写入,比如说新入库一个商品有 300 个,有 3 个 Redis 节点,那么我们分成 3 个分片的话,1 个 Redis 节点放 1 个分片,1 个分片存储 100 个商品,那么如果我们直接写入缓存,先写入第一个 Redis 节点 100 个库存,再写入第二个 Redis 节点 100 个库存,如果这时写入第三个 Redis 节点 100 个库存的时候失败了,那么就导致操作库存的请求压力全部在前两个 Redis 节点中,采用 渐进性写入 的话,流程为:我们已经直到每个 Redis 节点放 100 个库存了,那么我们定义一个轮次的变量,为 10,表示我们去将库存写入缓存中需要写入 10 轮,那么每轮就写入 10 个库存即可,这样写入 10 轮之后,每个 Redis 节点中也就有 100 个库存了,这样的好处在于,即使有部分库存写入失败的话,对于请求的压力也不会全部在其他节点上,因为写入失败的库存很小,很快时间就可以消耗完毕

基于缓存分片的入库添加库存方案

在商品入库时,主要就是在 DB 中记录入库的日志并且保存库存信息,在 Redis 中主要将库存进行分片存储,用于分担压力,流程图如下:

在这里插入图片描述

在商品入库时,对库存进行分片,流程为:

  • 计算当前 Redis 节点需要分配的库存数量
  • 执行 lua 脚本进行库存分配

下边贴出库存分片的代码,主要是 executeStockLua 方法:

/*** 执行库存分配,使用lua脚本执行库存的变更* @param request 变更库存对象*/
public void executeStockLua(InventoryRequest request) {// Redis 存储商品库存的 keyString productStockKey = RedisKeyConstants.PRODUCT_STOCK_PREFIX  + request.getSkuId();// 当前已经分配的库存数量Integer sumNum = 0;Long startTime = System.currentTimeMillis();try {// 获取默认设定分桶int redisCount = cacheSupport.getRedisCount();// 商品入库数量Integer inventoryNum = request.getInventoryNum();// 单个机器预计分配的库存Integer countNum = inventoryNum / redisCount;// countNum 指的是每个机器每轮分配的库存数量,要么是单台机器预计分配的库存的 1/10,要么是 3 个// 也就是如果单个机器预计分配的库存比较小的话,没必要每次分配的 1 个或者 2 个,因此设置每轮分配的库存数量最小值是 3countNum = getAverageStockNum(countNum,redisCount);int i = 0;while (true){// 对每台机器进行库存分配for (long count = 0;count < redisCount; count++ ){// 最后剩余的库存小于每轮分配库存数量的时候,则以最后剩余的库存为准if (inventoryNum - sumNum < countNum){countNum = inventoryNum - sumNum;}// 这里 cacheSupport 是提供的一个工具类,用于让 Redis 去执行 lua 脚本进行库存的分配Object eval = cacheSupport.eval(count, RedisLua.ADD_INVENTORY, CollUtil.toList(productStockKey), CollUtil.toList(String.valueOf(countNum)));if (!Objects.isNull(eval) && Long.valueOf(eval+"") > 0){// 分配成功的才累计(可能出现不均匀的情况)sumNum = sumNum + countNum;i++;}if (sumNum.equals(inventoryNum)){break;}}//分配完成跳出循环if (sumNum.equals(inventoryNum)){break;}}log.info("商品编号:"+request.getSkuId()+",同步分配库存共分配"+ (i)+"次"+",分配库存:"+sumNum+",总计耗时"+(System.currentTimeMillis() - startTime)+"毫秒");}catch (Exception e){e.printStackTrace();// 同步过程中发生异常,去掉已被同步的缓存库存,发送消息再行补偿,这里出现异常不抛出,避免异常request.setInventoryNum(request.getInventoryNum() - sumNum);// 这个 MQ 的异步操作中,就去去对库存进行添加,之前已经成功添加 sumNum 个库存了,还需要再补偿添加 request.getInventoryNum() - sumNum 个库存sendAsyncStockCompensationMessage(request);log.error("分配库存到缓存过程中失败", e.getMessage(), e);}
}
/*** 获取每个机器预估的分配库存数量* @param countNum* @return*/
private Integer getAverageStockNum(Integer countNum,Integer redisCount){Integer num = 0;/*** countNum 为单个节点需要分配的库存总数* StockBucket.STOCK_COUNT_NUM 代表每个节点最多分配的轮次,这里的默认值是 10,也就是单个节点最多分配 10 次库存* redisCount 是 Redis 的节点数* 如果 countNum > (redisCount * StockBucket.STOCK_COUNT_NUM)* 那么每次分配的库存数我们以 redisCount 作为一个标准,假如 redisCount = 3* redisCount * StockBucket.STOCK_COUNT_NUM 的含义就是分配 10 轮,每轮分配 3 个库存,如果当前节点需要分配的库存数是比(每次分配3个,共分配10轮)还要多的话* 那么就每轮分配 countNum / 10* 如果单个节点的库存总数小于(分配 10 轮,每轮分配 redisCount 个库存)的话,再判断 countNum 是否大于 3,如果大于 3,就每轮分配 3 个* 如果小于 3,就分配 countNum 个*/if (countNum > (redisCount * StockBucket.STOCK_COUNT_NUM)){num = countNum / StockBucket.STOCK_COUNT_NUM;} else if (countNum > 3){num = 3;} else {num = countNum;}return num;
}

下边来讲一下库存分配中,如何选择 Redis 节点并执行 lua 脚本向 Redis 中写入库存的:

Object eval = cacheSupport.eval(count, RedisLua.ADD_INVENTORY, CollUtil.toList(productStockKey), CollUtil.toList(String.valueOf(countNum)));

也就是上边这一行代码,先说一下参数,count 表示循环到哪一个 Redis 节点了,通过 count % redisCount,就可以拿到需要操作的 Redis 节点的下标,表示需要操作哪一个 Redis,就在该 Redis 中执行 lua 脚本

RedisLua.ADD_INVENTORY 表示需要执行的 lua 脚本,CollUtil.toList(productStockKey) 表示 keys 的 list 集合,CollUtil.toList(String.valueOf(countNum)) 表示 args 的 list 集合,这两个参数用于在 lua 脚本中进行取参数使用

那么再说一下 eval 方法执行的流程,首先维护一个 List<JedisPool> 集合,那么在 eval 方法中根据 count 参数拿到需要操作的 Redis 节点的下标,取出该 Redis 节点所对应的 Jedis 客户端,再通过该客户端执行 lua 脚本,eval 方法如下:

@Override
public Object eval(Long hashKey, String script,List<String> keys, List<String> args) {/*** jedisManager.getJedisByHashKey(hashKey) 这个方法就是将传入的 count 也就是 hashKey 这个参数* 对 Redis 的节点数量进行取模,拿到一个下标,去 List 集合中取出该下标对应的 Jedis 客户端*/try (Jedis jedis = jedisManager.getJedisByHashKey(hashKey)){return jedis.eval(script,keys,args);}
}

那么在这个 eval 方法中,拿到存储当前库存分片的 Redis 客户端,在该客户端中执行 lua 脚本,脚本内容如下:

/*** 初始化新增库存* 这里的 KEYS[1] 也就是传入的 productStockKey = product_stock:{skuId}* ARGV[1] 也就是 countNum,即当前 Redis 节点需要分配的库存数量*/
public static final String ADD_INVENTORY ="if (redis.call('exists', KEYS[1]) == 1) then"+ "    local occStock = tonumber(redis.call('get', KEYS[1]));"+ "    if (occStock >= 0) then"+ "        return redis.call('incrBy', KEYS[1], ARGV[1]);"+ "    end;"+ "end;"+ "       redis.call('SET', KEYS[1], ARGV[1]);"+ "    return tonumber(redis.call('get', KEYS[1]));";

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

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

相关文章

AMBA 5 CHI 协议节点实例和读数据的来源

1. CHI协议节点实例 如上图所示&#xff0c;RN-F、RN-I、HN-F、SN-F、HN-I、SN-I通过内部互联总线连接起来。这里的ICN可以是CMN-700。其中SN-F和SN-I是连接主存&#xff0c;RN-F和HN-F内部都有cache。 2. CHI读数据的可能来源 如上图所示&#xff0c;CHI协议中可能的读数据来…

Win中Redis部署与配置

1.下载msi版本 下载传送门 2.双击next-->next安装安装 3.密码配置以及开机自启 在配置文件中配置相应配置进行配置密码以及端口和ip port 6379指定 Redis 监听端口&#xff0c;默认端口为 6379&#xff0c;作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口&…

初识Linux:保姆级教学,让你一秒记住Linux中的常用指令!

文章目录 前言一、LInux的背景及发展史二、Linux下的基本指令1、ls指令2、pwd指令3、cd指令4、touch指令5、mkdir指令&#xff08;重要&#xff09;6、tree指令7、rmdir指令和rm指令&#xff08;重要&#xff09;8、man指令&#xff08;重要&#xff09;9、cp指令&#xff08;重…

分享77个焦点幻灯JS特效,总有一款适合您

分享77个焦点幻灯JS特效&#xff0c;总有一款适合您 77个焦点幻灯JS特效下载链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;6666 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;收集整理更不易。知识付费甚欢喜&…

c语言调用free,提示已触发了一个断点。

在用c语言写数据结构的链表的时候&#xff0c;执行也没有什么大错&#xff0c;逻辑也是对的&#xff0c;但是一道free函数会自动触发一个断点。如图&#xff1a; 这个断点产生的原因是由于分配的内存太小了在使用的时候没有任何问题&#xff0c;但是在执行程序的时候&#xff0…

pytest系列——allure之在测试用例添加标题(@allure.title())

前言 通过使用装饰器allure.title可以为测试用例自定义一个更具有阅读性的易读的标题。 allure.title的三种使用方式&#xff1a; 直接使用allure.title为测试用例自定义标题&#xff1b;allure.title支持通过占位符的方式传递参数&#xff0c;可以实现测试用例标题参数化&a…

还搞不懂什么是参数,超参数吗?三分钟快速了解参数与超参数的概念和区别!!!

文章目录 前言一、参数是什么&#xff1f;二、超参数是什么三&#xff0c;常使用的超参数有哪些 前言 参数是模型中可被学习和调整的参数&#xff0c;通过训练数据进行学习和优化&#xff1b; 而超参数则是手动设置的参数&#xff0c;用于控制模型的行为和性能&#xff0c;超…

Java基础之数组拷贝

Arrays.copyOf 详解 copyOf是Arrays类下面的一个方法,用于拷贝各种数组 以整型数组为例&#xff1a;int [ ] copyOf(int [ ]array,int newLength);第一个参数是想要拷贝到数组&#xff0c;第二个参数是新拷贝得到的数组的大小&#xff08;不一定非得和原始数组大小一样&…

深入理解:Class.getResource与ClassLoader.getResource使用区别

深入理解&#xff1a;Class.getResource与ClassLoader.getResource使用区别 一作用&#xff1a;都是使用类的类加载器来读取某个文件&#xff0c;从而获取该文件的URL对象二Class.getResource()方法读取文件&#xff1a;1.若文件路径以“/”开头&#xff0c;则该方法会从classp…

Python面向对象⑤:多态【侯小啾python领航班系列(二十三)】

Python面向对象⑤:多态【侯小啾python领航班系列(二十三)】 大家好,我是博主侯小啾, 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹…

非标设计之气缸概述

气缸的组成&#xff1a; 气缸的分类 单作用气缸&#xff1a; 活塞仅一侧供气&#xff0c;气压推动活塞产生推力伸出&#xff0c;靠弹簧或自重返回。 双作用气缸&#xff1a; 气缸活塞两侧都有气压力&#xff0c;来实现前进或后退动作。 气缸的缓冲 但是&#xff0c;气缸也…

【蓝桥杯】翻硬币

翻硬币 思路&#xff1a; 其实有点贪心的意思&#xff0c;依次比较&#xff0c;不同就1&#xff0c;然后修改自己的字符串和下一个的字符串&#xff0c;再匹配。 #include<iostream> #include<string> using namespace std;string now,res;int main(void) {cin&g…

【Element-ui】Link 文字链接 与 Radio 单选框

文章目录 前言一、Link 文字链接1.1 基础用法1.2 禁用状态1.3 下划线1.4 图标 二、Radio 单选框2.1 基础用法2.2 禁用状态2.3 单选框组2.4 按钮样式2.5 带有边框2.6 Radio Eventsinput事件 2.7 Radio-group Attributes 总结 前言 在前端开发中&#xff0c;用户界面的元素设计和…

Python继承的设计及演化

Python中的继承 文章目录 Python中的继承概念明确MRO深度优先算法&#xff08;Python2.2之前及Python2.2的经典类中使用&#xff09;优化版的深度优先算法&#xff08;只在Python2.2版本的新式类中使用&#xff09;广度优先算法&#xff08;Python任何版本都从未使用过&#xf…

【Python】tensorflow学习的个人纪录(3)

sess tf.Session()actor Actor(sess, n_featuresN_S, lrLR_A, action_bound[-A_BOUND, A_BOUND])步进&#xff1a;

HDMI之数据岛

概述 发送端在发送视频信号之前,将多媒体信息通过数据岛传输给接收端。接收端通过数据岛信息获取当前分辨率(VIC),编码信息(RGB/YCR等),色彩空间,位深等等。然后对应将视频信息解码。与此同时,多余的带宽用于传输音频信息等。本文通过具体的包信息(从实验室仪器拍照…

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

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

4_企业架构双点服务器HA

企业架构双点服务器HA 学习目标和内容 1、能够描述高可用HA的作用 2、能够理解VIP的切换 3、能够描述keepalived作用 4、能够理解主master和备backup服务器关系 5、能够实现主备服务器高可用配置 6、能够实现模拟业务宕机服务切换 一、背景描述及其方案设计 1、业务背景描述 时…

JavaSE学习路线及经验所谈

前言 一.学习框架二.学习经验 相信很多小白刚开始学习Java时&#xff0c;都是靠自己在网上搜集资料&#xff0c;并没有明确规划&#xff0c;不知道要学习什么内容&#xff0c;也不知道学习的重点是什么&#xff0c;那么这篇文章会给你一个大致的指引&#xff0c;当然也可以作为…

网络之路27:IRF设备堆叠

正文共&#xff1a;3210 字 34 图&#xff0c;预估阅读时间&#xff1a;5 分钟 目录 网络之路第一章&#xff1a;Windows系统中的网络 0、序言 1、Windows系统中的网络1.1、桌面中的网卡1.2、命令行中的网卡1.3、路由表1.4、家用路由器 网络之路第二章&#xff1a;认识企业设备…