Java b2b2c multi-user open source mall system commodity module deduction inventory source code sharing

demand analysis

Before sharing the source code, first organize and clarify the requirements of the commodity modules in the b2b2c system to facilitate the understanding of the source code.

Business needs

  • The inventory of goods in the b2b2c e-commerce system is stored in redis and the database, and the deduction or increase of the inventory of operations such as delivery and return is realized.

Technical requirements

  • Redis transaction problem. If an exception occurs after the deduction of inventory is completed, redis has no transaction and cannot implement data rollback, resulting in abnormal data.
  • Use the lua script to deduct inventory, submit the operation atomically, and roll back the operation if the deduction fails in the lua script
  • The inventory information in the database is not updated in real time, but adopts the buffer pool method. The buffer pool method can be independently selected to be enabled or not.

Architecture ideas

Inventory Domain Model Architecture

Inventory deduction based on lua+redis

GoodsQuantityVO


/**
 * 商品库存vo
 * @author fk
 * @version v6.4
 * @since v6.4
 * 2017年9月7日 上午11:23:16
 */
public class GoodsQuantityVO implements Cloneable{



    private Integer goodsId;

    private Integer skuId;

    private Integer quantity;

    private  QuantityType quantityType;

    public GoodsQuantityVO() {}


    public GoodsQuantityVO(Integer goodsId, Integer skuId, Integer quantity ) {
        super();
        this.goodsId = goodsId;
        this.skuId = skuId;
        this.quantity = quantity;

    }
    setter and getter
}

GoodsQuantityManager

/**
 * 商品库存接口
 * @author fk
 * @version v2.0
 * @since v7.0.0
 * 2018年3月23日 上午11:47:29
 *
 * @version 3.0
 * 统一为一个接口(更新接口)<br/>
 * 内部实现为redis +lua 保证原子性 -- by kingapex 2019-01-17
 */
public interface GoodsQuantityManager {
    /**
     * 库存更新接口
      * @param goodsQuantityList 要更新的库存vo List
     * @return 如果更新成功返回真,否则返回假
     */
    Boolean updateSkuQuantity(List<GoodsQuantityVO> goodsQuantityList );

    /**
     * 同步数据库数据
     */
    void syncDataBase();

    /**
     * 为某个sku 填充库存cache<br/>
     * 库存数量由数据库中获取<br/>
     * 一般用于缓存被击穿的情况
     * @param skuId
     * @return 可用库存和实际库存
     */
    Map<String,Integer> fillCacheFromDB(int skuId);


}

GoodsQuantityManagerImpl


The inventory business class is based on the implementation of lua+redis:

/**
 * 商品库存接口
 *
 * @author fk
 * @author kingapex
 * @version v2.0 written by kingapex  2019年2月27日
 * 采用lua脚本执行redis中的库存扣减<br/>
 * 数据库的更新采用非时时同步<br/>
 * 而是建立了一个缓冲池,当达到一定条件时再同步数据库<br/>
 * 这样条件有:缓冲区大小,缓冲次数,缓冲时间<br/>
 * 上述条件在配置中心可以配置,如果没有配置采用 ${@link UpdatePool} 默认值<br/>
 * 在配置项说明:<br/>
 * <li>缓冲区大小:javashop.pool.stock.max-pool-size</li>
 * <li>缓冲次数:javashop.pool.stock.max-update-time</li>
 * <li>缓冲时间(秒数):javashop.pool.stock.max-lazy-second</li>
 * @see JavashopConfig
 */
@Service
public class GoodsQuantityManagerImpl implements GoodsQuantityManager {


    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DaoSupport daoSupport;

    @Autowired
    private JavashopConfig javashopConfig;


    /**
     * sku库存更新缓冲池
     */
    private static UpdatePool skuUpdatePool;
    /**
     * goods库存更新缓冲池
     */
    private static UpdatePool goodsUpdatePool;


    /**
     * 单例获取sku pool ,初始化时设置参数
     *
     * @return
     */
    private UpdatePool getSkuPool() {
        if (skuUpdatePool == null) {
            skuUpdatePool = new UpdatePool(javashopConfig.getMaxUpdateTime(), javashopConfig.getMaxPoolSize(), javashopConfig.getMaxLazySecond());
            logger.debug("初始化sku pool:");
            logger.debug(skuUpdatePool.toString());
        }

        return skuUpdatePool;
    }


    /**
     * 单例获取goods pool ,初始化时设置参数
     *
     * @return
     */
    private UpdatePool getGoodsPool() {
        if (goodsUpdatePool == null) {
            goodsUpdatePool = new UpdatePool(javashopConfig.getMaxUpdateTime(), javashopConfig.getMaxPoolSize(), javashopConfig.getMaxLazySecond());


        }

        return goodsUpdatePool;
    }

    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    private static RedisScript<Boolean> script = null;

    private static RedisScript<Boolean> getRedisScript() {

        if (script != null) {
            return script;
        }

        ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("sku_quantity.lua"));
        String str = null;
        try {
            str = scriptSource.getScriptAsString();
        } catch (IOException e) {
            e.printStackTrace();
        }

        script = RedisScript.of(str, Boolean.class);
        return script;
    }

    @Override
    public Boolean updateSkuQuantity(List<GoodsQuantityVO> goodsQuantityList) {

        List<Integer> skuIdList = new ArrayList();
        List<Integer> goodsIdList = new ArrayList();

        List keys = new ArrayList<>();
        List values = new ArrayList<>();

        for (GoodsQuantityVO quantity : goodsQuantityList) {

            Assert.notNull(quantity.getGoodsId(), "goods id must not be null");
            Assert.notNull(quantity.getSkuId(), "sku id must not be null");
            Assert.notNull(quantity.getQuantity(), "quantity id must not be null");
            Assert.notNull(quantity.getQuantityType(), "Type must not be null");


            //sku库存
            if (QuantityType.enable.equals(quantity.getQuantityType())) {
                keys.add(StockCacheKeyUtil.skuEnableKey(quantity.getSkuId()));
            } else if (QuantityType.actual.equals(quantity.getQuantityType())) {
                keys.add(StockCacheKeyUtil.skuActualKey(quantity.getSkuId()));
            }
            values.add("" + quantity.getQuantity());

            //goods库存key
            if (QuantityType.enable.equals(quantity.getQuantityType())) {
                keys.add(StockCacheKeyUtil.goodsEnableKey(quantity.getGoodsId()));
            } else if (QuantityType.actual.equals(quantity.getQuantityType())) {
                keys.add(StockCacheKeyUtil.goodsActualKey(quantity.getGoodsId()));
            }
            values.add("" + quantity.getQuantity());


            skuIdList.add(quantity.getSkuId());
            goodsIdList.add(quantity.getGoodsId());
        }

        RedisScript<Boolean> redisScript = getRedisScript();
        Boolean result = stringRedisTemplate.execute(redisScript, keys, values.toArray());

        logger.debug("更新库存:");
        logger.debug(goodsQuantityList.toString());
        logger.debug("更新结果:" + result);

        //如果lua脚本执行成功则记录缓冲区
        if (result) {

            //判断配置文件中设置的商品库存缓冲池是否开启
            if (javashopConfig.isStock()) {

                //是否需要同步数据库
                boolean needSync = getSkuPool().oneTime(skuIdList);
                getGoodsPool().oneTime(goodsIdList);

                logger.debug("是否需要同步数据库:" + needSync);
                logger.debug(getSkuPool().toString());

                //如果开启了缓冲池,并且缓冲区已经饱和,则同步数据库
                if (needSync) {
                    syncDataBase();
                }
            } else {
                //如果未开启缓冲池,则实时同步商品数据库中的库存数据
                syncDataBase(skuIdList, goodsIdList);
            }

        }


        return result;
    }

    @Override
    public void syncDataBase() {

        //获取同步的skuid 和goodsid
        List<Integer> skuIdList = getSkuPool().getTargetList();
        List<Integer> goodsIdList = getGoodsPool().getTargetList();

        logger.debug("goodsIdList is:");
        logger.debug(goodsIdList.toString());

        //判断要同步的goods和sku集合是否有值
        if (skuIdList.size() != 0 && goodsIdList.size() != 0) {
            //同步数据库
            syncDataBase(skuIdList, goodsIdList);
        }

        //重置缓冲池
        getSkuPool().reset();
        getGoodsPool().reset();
    }

    @Override
    public Map<String, Integer> fillCacheFromDB(int skuId) {
        Map<String, Integer> map = daoSupport.queryForMap("select enable_quantity,quantity from es_goods_sku where sku_id=?", skuId);
        Integer enableNum = map.get("enable_quantity");
        Integer actualNum = map.get("quantity");

        stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuActualKey(skuId), "" + actualNum);
        stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuEnableKey(skuId), "" + enableNum);
        return map;
    }


    /**
     * 同步数据库中的库存
     *
     * @param skuIdList   需要同步的skuid
     * @param goodsIdList 需要同步的goodsid
     */
    private void syncDataBase(List<Integer> skuIdList, List<Integer> goodsIdList) {

        //要形成的指更新sql
        List<String> sqlList = new ArrayList<String>();


        //批量获取sku的库存
        List skuKeys = StockCacheKeyUtil.skuKeys(skuIdList);
        List<String> skuQuantityList = stringRedisTemplate.opsForValue().multiGet(skuKeys);


        int i = 0;

        //形成批量更新sku的list
        for (Integer skuId : skuIdList) {
            String sql = "update es_goods_sku set enable_quantity=" + skuQuantityList.get(i) + ", quantity=" + skuQuantityList.get(i + 1) + " where sku_id=" + skuId;
            daoSupport.execute(sql);
            i = i + 2;
        }

        //批量获取商品的库存
        List goodsKeys = createGoodsKeys(goodsIdList);
        List<String> goodsQuantityList = stringRedisTemplate.opsForValue().multiGet(goodsKeys);

        i = 0;

        //形成批量更新goods的list
        for (Integer goodsId : goodsIdList) {
            String sql = "update es_goods set enable_quantity=" + goodsQuantityList.get(i) + ", quantity=" + goodsQuantityList.get(i + 1) + " where goods_id=" + goodsId;
            daoSupport.execute(sql);
            i = i + 2;
        }
    }


    /**
     * 生成批量获取goods库存的keys
     *
     * @param goodsIdList
     * @return
     */
    private List createGoodsKeys(List<Integer> goodsIdList) {
        List keys = new ArrayList();
        for (Integer goodsId : goodsIdList) {
            keys.add(StockCacheKeyUtil.goodsEnableKey(goodsId));
            keys.add(StockCacheKeyUtil.goodsActualKey(goodsId));
        }
        return keys;
    }
}

sku_quantity.lua


inventory deduction lua script

-- 可能回滚的列表,一个记录要回滚的skuid一个记录库存
local skuid_list= {}
local stock_list= {}

local arg_list = ARGV;
local function cut ( key , num )
    KEYS[1] = key;
    local value = redis.call("get",KEYS[1])

    if not value then
        value = 0;
    end

    value=value+num
    if(value<0)
    then
        -- 发生超卖
        return false;
    end
    redis.call("set",KEYS[1],value)
    return true
end

local function rollback ( )
    for i,k in ipairs (skuid_list) do
        -- 还原库存
        KEYS[1] = k;
        redis.call("incrby",KEYS[1],0-stock_list[i])
    end
end

local function doExec()
    for i, k in ipairs (arg_list)
    do
        local num = tonumber(k)
        local key=  KEYS[i]
        local result = cut(key,num)

        -- 发生超卖,需要回滚
        if (result == false)
        then
            rollback()
            return false
        else
            -- 记录可能要回滚的数据
            table.insert(skuid_list,key)
            table.insert(stock_list,num)
        end

    end
    return true;
end

return doExec()

JavashopConfig


Buffer pool related settings information


/**
 * javashop配置
 *
 * @author zh
 * @version v7.0
 * @date 18/4/13 下午8:19
 * @since v7.0
 */
@Configuration
@ConfigurationProperties(prefix = "javashop")
@SuppressWarnings("ConfigurationProperties")
public class JavashopConfig {

    /**
     * 缓冲次数
     */
    @Value("${javashop.pool.stock.max-update-timet:#{null}}")
    private Integer maxUpdateTime;

    /**
     * 缓冲区大小
     */
    @Value("${javashop.pool.stock.max-pool-size:#{null}}")
    private Integer maxPoolSize;

    /**
     * 缓冲时间(秒数)
     */
    @Value("${javashop.pool.stock.max-lazy-second:#{null}}")
    private Integer maxLazySecond;

    /**
     * 商品库存缓冲池开关
     * false:关闭(如果配置文件中没有配置此项,则默认为false)
     * true:开启(优点:缓解程序压力;缺点:有可能会导致商家中心商品库存数量显示延迟;)
     */
    @Value("${javashop.pool.stock:#{false}}")
    private boolean stock;


    public JavashopConfig() {
    }
 
    setter and getter...
   
}

The above is the idea of ​​deducting inventory in the commodity module in javashop and the related source code.

Original article by javashop

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324127446&siteId=291194637