首页 > 数据库 >基于社区电商的Redis缓存架构-库存模块缓存架构(下)

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

时间:2023-12-06 12:32:54浏览次数:37  
标签:缓存 架构 Redis num 扣除 库存 扣减 电商 节点

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

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

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

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

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

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

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

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

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

出库主要有 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 记录出库记录

标签:缓存,架构,Redis,num,扣除,库存,扣减,电商,节点
From: https://blog.51cto.com/u_16186397/8701856

相关文章

  • 大语言模型底层架构丨带你认识Transformer
    本文分享自华为云社区《大语言模型底层架构你了解多少?大语言模型底层架构之一Transfomer的介绍和python代码实现》,作者:码上开花_Lancer。语言模型目标是建模自然语言的概率分布,在自然语言处理研究中具有重要的作用,是自然语言处理基础任务之一。大量的研究从n元语言模型(n-gram......
  • 技术架构演进之路基础概念
    概述在进行技术学习过程中,由于没有经历过一些中大型系统的实际经验,导致无法从全局理解一些概念,所以本文以一个"电子商务"应用为例,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,方便大家对后......
  • 单机架构
    初期,我们需要利用我们精干的技术团队,快速将业务系统投入市场进行检验,并且可以迅速响应变化要求。但好在前期用户访问量很少,没有对我们的性能、安全等提出很高的要求,而且系统架构简单,无需专业的运维团队,所以选择单机架构是合适的用户在浏览器中输入www.google.com,首先经过DNS服......
  • 深入探讨Guava的缓存机制
    第1章:引言大家好,我是小黑,今天咱们聊聊GoogleGuava的缓存机制。缓存在现代编程中的作用非常大,它能提高应用性能,减少数据库压力,简直就是性能优化的利器。而Guava提供的缓存功能,不仅强大而且使用起来非常灵活。在咱们深入挖掘之前,先简单说说缓存。缓存,其实就是一种保存数据的手段......
  • 1.需求分析和架构设计
    需求分析熟悉产品需求以架构师的思维分析需求,不能只停留在表面实现需求,要考虑怎么实现能给业务带来增长全局思维、整体思维、闭环思维,不能只考虑自己,要全局考虑整个团队,要做到有输出、有输入、有结果浅层需求分析表面需要实现的功能,如登录、创建作品、编辑、发布、访问作......
  • 100MB缓存新神U!AMD锐龙7 5700X3D蓄势待发
    AMD将在2024年第一季度发布新款锐龙75700X3D,这也将是3D缓存家族最便宜的零售型号。锐龙75800X3D作为首款3D缓存处理器,一炮打响,成为主流游戏玩家的最佳选择。Zen4时代,AMD一口气推出了锐龙97950X3D/7900X3D、锐龙77800X3D,但定位和价格都更高了。AMD虽然后来增加了锐龙55600X......
  • 网络通信、UDP通信、TCP通信、BS架构模拟、URL了解
    网络编程可以让程序与网络上的其他设备中的程序进行数据交互所以,我们学习网络编程的主要目的就是为了实现网络通信网络通信网络通信基本模式常见的通信模式有如下2种形式:Client-Server(Cs)、Browser/Server(Bs)Client-Server(Cs)主要是客户端与服务端之间的联系(就是相应的App和后......
  • 直播预约丨《实时湖仓实践五讲》第四讲:实时湖仓架构与技术选型
    如今,大规模、高时效、智能化数据处理已是“刚需”,企业需要更强大的数据平台,来应对数据查询、数据处理、数据挖掘、数据展示以及多种计算模型并行的挑战,湖仓一体方案应运而生。《实时湖仓实践五讲》是袋鼠云打造的系列直播活动,将围绕实时湖仓的建设趋势和通用问题,邀请奋战于企业数字......
  • 直播预约丨《实时湖仓实践五讲》第四讲:实时湖仓架构与技术选型
    如今,大规模、高时效、智能化数据处理已是“刚需”,企业需要更强大的数据平台,来应对数据查询、数据处理、数据挖掘、数据展示以及多种计算模型并行的挑战,湖仓一体方案应运而生。《实时湖仓实践五讲》是袋鼠云打造的系列直播活动,将围绕实时湖仓的建设趋势和通用问题,邀请奋战于企业数......
  • Quartz核心原理之架构及基本元素介绍
    1什么是QuartzQuartz是一个作业调度框架,它可以与J2EE和J2SE应用相结合,也可以单独使用。它能够创建多个甚至数万个jobs这样复杂的程序,jobs可以做成标准的java组件或EJBS。Quartz很容易上手,创建一个任务仅需实现Job接口,该接口只有一个方法voidexecute(JobExecutionContextcontex......