需求分析
在分享源码之前,先将b2b2c系统中商品模块需求整理、明确,方便源码的理解。
业务需求
- b2b2c电子商务系统中商品的库存存放在redis和数据库中,实现发货退货等操作库存的扣减或增加
技术需求
- redis事务问题,若扣减库存完成后,发生异常,则redis没有事务,无法实现数据回滚,导致数据异常
- 采用lua脚本扣减库存方式,原子性提交操作,lua脚本中实现扣减失败则回滚操作
- 数据库中的库存信息,非实时更新,而是采用缓冲池方式,缓冲池方式可以自主选择是否开启
架构思路
商品库存领域模型架构 
基于lua+redis的库存扣减
GoodsQuantityVO
1/** 2 * 商品库存vo 3 * @author fk 4 * @version v6.4 5 * @since v6.4 6 * 2017年9月7日 上午11:23:16 7 */ 8public class GoodsQuantityVO implements Cloneable{ 9 10 11 12 private Integer goodsId; 13 14 private Integer skuId; 15 16 private Integer quantity; 17 18 private QuantityType quantityType; 19 20 public GoodsQuantityVO() {} 21 22 23 public GoodsQuantityVO(Integer goodsId, Integer skuId, Integer quantity ) { 24 super(); 25 this.goodsId = goodsId; 26 this.skuId = skuId; 27 this.quantity = quantity; 28 29 } 30 setter and getter 31}
GoodsQuantityManager
1/** 2 * 商品库存接口 3 * @author fk 4 * @version v2.0 5 * @since v7.0.0 6 * 2018年3月23日 上午11:47:29 7 * 8 * @version 3.0 9 * 统一为一个接口(更新接口)<br/> 10 * 内部实现为redis +lua 保证原子性 -- by kingapex 2019-01-17 11 */ 12public interface GoodsQuantityManager { 13 /** 14 * 库存更新接口 15 * @param goodsQuantityList 要更新的库存vo List 16 * @return 如果更新成功返回真,否则返回假 17 */ 18 Boolean updateSkuQuantity(List<GoodsQuantityVO> goodsQuantityList ); 19 20 /** 21 * 同步数据库数据 22 */ 23 void syncDataBase(); 24 25 /** 26 * 为某个sku 填充库存cache<br/> 27 * 库存数量由数据库中获取<br/> 28 * 一般用于缓存被击穿的情况 29 * @param skuId 30 * @return 可用库存和实际库存 31 */ 32 Map<String,Integer> fillCacheFromDB(int skuId); 33 34 35}
GoodsQuantityManagerImpl
库存业务类基于lua+redis的实现:
1/** 2 * 商品库存接口 3 * 4 * @author fk 5 * @author kingapex 6 * @version v2.0 written by kingapex 2019年2月27日 7 * 采用lua脚本执行redis中的库存扣减<br/> 8 * 数据库的更新采用非时时同步<br/> 9 * 而是建立了一个缓冲池,当达到一定条件时再同步数据库<br/> 10 * 这样条件有:缓冲区大小,缓冲次数,缓冲时间<br/> 11 * 上述条件在配置中心可以配置,如果没有配置采用 ${@link UpdatePool} 默认值<br/> 12 * 在配置项说明:<br/> 13 * <li>缓冲区大小:javashop.pool.stock.max-pool-size</li> 14 * <li>缓冲次数:javashop.pool.stock.max-update-time</li> 15 * <li>缓冲时间(秒数):javashop.pool.stock.max-lazy-second</li> 16 * @see JavashopConfig 17 */ 18@Service 19public class GoodsQuantityManagerImpl implements GoodsQuantityManager { 20 21 22 private final Logger logger = LoggerFactory.getLogger(getClass()); 23 24 @Autowired 25 private DaoSupport daoSupport; 26 27 @Autowired 28 private JavashopConfig javashopConfig; 29 30 31 /** 32 * sku库存更新缓冲池 33 */ 34 private static UpdatePool skuUpdatePool; 35 /** 36 * goods库存更新缓冲池 37 */ 38 private static UpdatePool goodsUpdatePool; 39 40 41 /** 42 * 单例获取sku pool ,初始化时设置参数 43 * 44 * @return 45 */ 46 private UpdatePool getSkuPool() { 47 if (skuUpdatePool == null) { 48 skuUpdatePool = new UpdatePool(javashopConfig.getMaxUpdateTime(), javashopConfig.getMaxPoolSize(), javashopConfig.getMaxLazySecond()); 49 logger.debug("初始化sku pool:"); 50 logger.debug(skuUpdatePool.toString()); 51 } 52 53 return skuUpdatePool; 54 } 55 56 57 /** 58 * 单例获取goods pool ,初始化时设置参数 59 * 60 * @return 61 */ 62 private UpdatePool getGoodsPool() { 63 if (goodsUpdatePool == null) { 64 goodsUpdatePool = new UpdatePool(javashopConfig.getMaxUpdateTime(), javashopConfig.getMaxPoolSize(), javashopConfig.getMaxLazySecond()); 65 66 67 } 68 69 return goodsUpdatePool; 70 } 71 72 @Autowired 73 public StringRedisTemplate stringRedisTemplate; 74 75 private static RedisScript<Boolean> script = null; 76 77 private static RedisScript<Boolean> getRedisScript() { 78 79 if (script != null) { 80 return script; 81 } 82 83 ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("sku_quantity.lua")); 84 String str = null; 85 try { 86 str = scriptSource.getScriptAsString(); 87 } catch (IOException e) { 88 e.printStackTrace(); 89 } 90 91 script = RedisScript.of(str, Boolean.class); 92 return script; 93 } 94 95 @Override 96 public Boolean updateSkuQuantity(List<GoodsQuantityVO> goodsQuantityList) { 97 98 List<Integer> skuIdList = new ArrayList(); 99 List<Integer> goodsIdList = new ArrayList(); 100 101 List keys = new ArrayList<>(); 102 List values = new ArrayList<>(); 103 104 for (GoodsQuantityVO quantity : goodsQuantityList) { 105 106 Assert.notNull(quantity.getGoodsId(), "goods id must not be null"); 107 Assert.notNull(quantity.getSkuId(), "sku id must not be null"); 108 Assert.notNull(quantity.getQuantity(), "quantity id must not be null"); 109 Assert.notNull(quantity.getQuantityType(), "Type must not be null"); 110 111 112 //sku库存 113 if (QuantityType.enable.equals(quantity.getQuantityType())) { 114 keys.add(StockCacheKeyUtil.skuEnableKey(quantity.getSkuId())); 115 } else if (QuantityType.actual.equals(quantity.getQuantityType())) { 116 keys.add(StockCacheKeyUtil.skuActualKey(quantity.getSkuId())); 117 } 118 values.add("" + quantity.getQuantity()); 119 120 //goods库存key 121 if (QuantityType.enable.equals(quantity.getQuantityType())) { 122 keys.add(StockCacheKeyUtil.goodsEnableKey(quantity.getGoodsId())); 123 } else if (QuantityType.actual.equals(quantity.getQuantityType())) { 124 keys.add(StockCacheKeyUtil.goodsActualKey(quantity.getGoodsId())); 125 } 126 values.add("" + quantity.getQuantity()); 127 128 129 skuIdList.add(quantity.getSkuId()); 130 goodsIdList.add(quantity.getGoodsId()); 131 } 132 133 RedisScript<Boolean> redisScript = getRedisScript(); 134 Boolean result = stringRedisTemplate.execute(redisScript, keys, values.toArray()); 135 136 logger.debug("更新库存:"); 137 logger.debug(goodsQuantityList.toString()); 138 logger.debug("更新结果:" + result); 139 140 //如果lua脚本执行成功则记录缓冲区 141 if (result) { 142 143 //判断配置文件中设置的商品库存缓冲池是否开启 144 if (javashopConfig.isStock()) { 145 146 //是否需要同步数据库 147 boolean needSync = getSkuPool().oneTime(skuIdList); 148 getGoodsPool().oneTime(goodsIdList); 149 150 logger.debug("是否需要同步数据库:" + needSync); 151 logger.debug(getSkuPool().toString()); 152 153 //如果开启了缓冲池,并且缓冲区已经饱和,则同步数据库 154 if (needSync) { 155 syncDataBase(); 156 } 157 } else { 158 //如果未开启缓冲池,则实时同步商品数据库中的库存数据 159 syncDataBase(skuIdList, goodsIdList); 160 } 161 162 } 163 164 165 return result; 166 } 167 168 @Override 169 public void syncDataBase() { 170 171 //获取同步的skuid 和goodsid 172 List<Integer> skuIdList = getSkuPool().getTargetList(); 173 List<Integer> goodsIdList = getGoodsPool().getTargetList(); 174 175 logger.debug("goodsIdList is:"); 176 logger.debug(goodsIdList.toString()); 177 178 //判断要同步的goods和sku集合是否有值 179 if (skuIdList.size() != 0 && goodsIdList.size() != 0) { 180 //同步数据库 181 syncDataBase(skuIdList, goodsIdList); 182 } 183 184 //重置缓冲池 185 getSkuPool().reset(); 186 getGoodsPool().reset(); 187 } 188 189 @Override 190 public Map<String, Integer> fillCacheFromDB(int skuId) { 191 Map<String, Integer> map = daoSupport.queryForMap("select enable_quantity,quantity from es_goods_sku where sku_id=?", skuId); 192 Integer enableNum = map.get("enable_quantity"); 193 Integer actualNum = map.get("quantity"); 194 195 stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuActualKey(skuId), "" + actualNum); 196 stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuEnableKey(skuId), "" + enableNum); 197 return map; 198 } 199 200 201 /** 202 * 同步数据库中的库存 203 * 204 * @param skuIdList 需要同步的skuid 205 * @param goodsIdList 需要同步的goodsid 206 */ 207 private void syncDataBase(List<Integer> skuIdList, List<Integer> goodsIdList) { 208 209 //要形成的指更新sql 210 List<String> sqlList = new ArrayList<String>(); 211 212 213 //批量获取sku的库存 214 List skuKeys = StockCacheKeyUtil.skuKeys(skuIdList); 215 List<String> skuQuantityList = stringRedisTemplate.opsForValue().multiGet(skuKeys); 216 217 218 int i = 0; 219 220 //形成批量更新sku的list 221 for (Integer skuId : skuIdList) { 222 String sql = "update es_goods_sku set enable_quantity=" + skuQuantityList.get(i) + ", quantity=" + skuQuantityList.get(i + 1) + " where sku_id=" + skuId; 223 daoSupport.execute(sql); 224 i = i + 2; 225 } 226 227 //批量获取商品的库存 228 List goodsKeys = createGoodsKeys(goodsIdList); 229 List<String> goodsQuantityList = stringRedisTemplate.opsForValue().multiGet(goodsKeys); 230 231 i = 0; 232 233 //形成批量更新goods的list 234 for (Integer goodsId : goodsIdList) { 235 String sql = "update es_goods set enable_quantity=" + goodsQuantityList.get(i) + ", quantity=" + goodsQuantityList.get(i + 1) + " where goods_id=" + goodsId; 236 daoSupport.execute(sql); 237 i = i + 2; 238 } 239 } 240 241 242 /** 243 * 生成批量获取goods库存的keys 244 * 245 * @param goodsIdList 246 * @return 247 */ 248 private List createGoodsKeys(List<Integer> goodsIdList) { 249 List keys = new ArrayList(); 250 for (Integer goodsId : goodsIdList) { 251 keys.add(StockCacheKeyUtil.goodsEnableKey(goodsId)); 252 keys.add(StockCacheKeyUtil.goodsActualKey(goodsId)); 253 } 254 return keys; 255 } 256}
sku_quantity.lua
库存扣减lua脚本
1-- 可能回滚的列表,一个记录要回滚的skuid一个记录库存 2local skuid_list= {} 3local stock_list= {} 4 5local arg_list = ARGV; 6local function cut ( key , num ) 7 KEYS[1] = key; 8 local value = redis.call("get",KEYS[1]) 9 10 if not value then 11 value = 0; 12 end 13 14 value=value+num 15 if(value<0) 16 then 17 -- 发生超卖 18 return false; 19 end 20 redis.call("set",KEYS[1],value) 21 return true 22end 23 24local function rollback ( ) 25 for i,k in ipairs (skuid_list) do 26 -- 还原库存 27 KEYS[1] = k; 28 redis.call("incrby",KEYS[1],0-stock_list[i]) 29 end 30end 31 32local function doExec() 33 for i, k in ipairs (arg_list) 34 do 35 local num = tonumber(k) 36 local key= KEYS[i] 37 local result = cut(key,num) 38 39 -- 发生超卖,需要回滚 40 if (result == false) 41 then 42 rollback() 43 return false 44 else 45 -- 记录可能要回滚的数据 46 table.insert(skuid_list,key) 47 table.insert(stock_list,num) 48 end 49 50 end 51 return true; 52end 53 54return doExec()
JavashopConfig
缓冲池相关设置信息
1/** 2 * javashop配置 3 * 4 * @author zh 5 * @version v7.0 6 * @date 18/4/13 下午8:19 7 * @since v7.0 8 */ 9@Configuration 10@ConfigurationProperties(prefix = "javashop") 11@SuppressWarnings("ConfigurationProperties") 12public class JavashopConfig { 13 14 /** 15 * 缓冲次数 16 */ 17 @Value("${javashop.pool.stock.max-update-timet:#{null}}") 18 private Integer maxUpdateTime; 19 20 /** 21 * 缓冲区大小 22 */ 23 @Value("${javashop.pool.stock.max-pool-size:#{null}}") 24 private Integer maxPoolSize; 25 26 /** 27 * 缓冲时间(秒数) 28 */ 29 @Value("${javashop.pool.stock.max-lazy-second:#{null}}") 30 private Integer maxLazySecond; 31 32 /** 33 * 商品库存缓冲池开关 34 * false:关闭(如果配置文件中没有配置此项,则默认为false) 35 * true:开启(优点:缓解程序压力;缺点:有可能会导致商家中心商品库存数量显示延迟;) 36 */ 37 @Value("${javashop.pool.stock:#{false}}") 38 private boolean stock; 39 40 41 public JavashopConfig() { 42 } 43 44 setter and getter... 45 46}
以上是javashop中商品模块扣减库存的思路以及相关源码。
易族智汇(javashop)原创文章