java b2b2c多用户开源商城系统商品模块扣减库存源码分享

Wesley13
• 阅读 583

需求分析

在分享源码之前,先将b2b2c系统中商品模块需求整理、明确,方便源码的理解。

业务需求

  • b2b2c电子商务系统中商品的库存存放在redis和数据库中,实现发货退货等操作库存的扣减或增加

技术需求

  • redis事务问题,若扣减库存完成后,发生异常,则redis没有事务,无法实现数据回滚,导致数据异常
  • 采用lua脚本扣减库存方式,原子性提交操作,lua脚本中实现扣减失败则回滚操作
  • 数据库中的库存信息,非实时更新,而是采用缓冲池方式,缓冲池方式可以自主选择是否开启

架构思路

商品库存领域模型架构 java b2b2c多用户开源商城系统商品模块扣减库存源码分享

基于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


库存业务类基于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


库存扣减lua脚本

-- 可能回滚的列表,一个记录要回滚的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


缓冲池相关设置信息

/**
 * 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...
   
}

以上是javashop中商品模块扣减库存的思路以及相关源码。

易族智汇(javashop)原创文章

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
java b2b2c多用户开源商城系统基于脚本引擎的促销架构源码分享
需求分析在分享源码之前,先将b2b2c系统中促销模块需求整理、明确,方便源码的理解。业务需求b2b2c电子商务系统(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.javamall.com.cn%2F)中促销活动相关规则需以脚本数据的方式存放
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这