本文介绍: 比如说新入库一个商品有 300 个,有 3 个 Redis 节点,那么我们分成 3 个分片的话,1 个 Redis 节点放 1 个分片,1 个分片存储 100 个商品,那么如果我们直接写入缓存先写第一个 Redis 节点 100 个库存,再写入第二个 Redis 节点 100 个库存,如果这时写入第三个 Redis 节点 100 个库存时候失败了,那么就导致操作库存请求压力全部在前两个 Redis 节点中,采用。那么对于该商品的瞬间高并发操作,会分散的打到多个 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 中主要将库存进行分片存储用于分担压力,流程图如下

在这里插入图片描述

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

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

/**
 * 执行库存分配,使用lua脚本执行库存的变更
 * @param request 变更库存对象
 */
public void executeStockLua(InventoryRequest request) {
    // Redis 存储商品库存的 key
    String 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 个,因此设置每轮分配的库存数量最小值是 3
        countNum = 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) &amp;&amp; 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) 表示 keyslist 集合CollUtil.toList(String.valueOf(countNum)) 表示 argslist 集合,这两个参数用于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]));";

原文地址:https://blog.csdn.net/qq_45260619/article/details/134762226

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_35914.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注