伴随物流行业的迅猛发展,一体化供应链模式的落地,对系统吞吐、系统稳定发出巨大挑战,库存作为供应链的重中之重表现更为明显。近三年数据可以看出:
通过分析过往大促流量,分钟级流量增长率为75%,大促仓内反馈三方订单下传不及时,库存预占吞吐量和性能是导致订单积压因素之一。目前库存使用mysql数据库作为接单预占的扛量手段,随着一体化供应链建设以及重点KA商家不断接入,现有库存架构在业务支撑上存在风险和缺陷。
名词解释:
库存预占:是指消费者拍下商品订单后,库存先为该订单短暂预留,预留的库存即为预占库存。
设计思路
-
当前库存系统瓶颈在哪里?抗写流量,数据库成为瓶颈点。 -
如何解决系统瓶颈?由高并发组件Redis替代数据库。 -
利用Redis需要解决哪些问题?防超卖,异步写数据库保证最终一致性。
-
扛量部分 :库存性能瓶颈在预占,传统架构主要依靠数据库事务保持数据一致以及数据读写;新版架构设计将数据扛量部分移植到Redis,利用Redis高性能吞吐解决高并发场景下数据读写。 -
数据回写 :Redis进行扛量削峰,后续数据仅用于记账,最终牺牲数据的短暂一致性达到削峰的目的。 -
差异部分 :老版本库存预占设计仅依靠数据进行数据处理,新版设计依靠切量配置将数据切换到Redis,利用Redis高读写进行削峰操作。
-
主流程:
-
库存初始化 :竞态条件利用Redis watch命令来实现锁等待,解决并发场景数据不一致问题。 -
LUA执行器 :将原子操作指令/复用指令封装到LUA脚本中以减少网络开销。 -
补偿机制:i> 执行流程中所有业务异常发生时会同步发起反向操作请求;ii> 反向操作执行异常后会提交异步反向操作任务;iii>异步任务执行异常后,依赖监控系统扫描异常单据或异常库存并修改异常库存量
-
回溯回写 :任务落库后发出mq组装参数调用数据回写服务,数据回写服务操作库存数量;同时回写redis数据,释放预占量库存数据;更新任务库数据状态
-
库存记录索引 :{deptNo|goodsNo|warehouseNo}|stockStatus|stockType|goodsLevel -
hashTag :{deptNo|goodsNo|warehouseNo}|stockStatus|stockType|goodsLevel -
可售库存数量 :usableKey:{库存记录索引} -
扣减库存量 :usableSubtractKey:{库存记录索引} ,记录Redis到DB执行期间减库存量 -
预占防重key :operateKey:{库存记录索引:单号} 防重key防并发重复请求 -
回滚防重 :rollbackOperateKey:{库存记录索引} -
缺量预占库存量 :ullageOperateKey:{库存记录索引} -
扣减库存单据记录 :hSetrecord: {库存记录索引}
-
首先进行redis&从库数据比对,若存在差异则对主库进行校验 -
比对过程中,DB中sku明细行进行锁定(for update),比对逻辑为DB可用库存量==(Redis可用库存量+Redis预占量) -
有差异,报警且触发SDK可用量过期,同时矫正预占量
冷热数据
-
OMS库存冷热装置 -
预占架构升级切量重点key监控 -
库存预占架构升级切量商家 -
架构升级切量商家明细2 -
已切量商家
-
MD生成规则工具集
-
逻辑库存MD5工具
StringBuffer md5Key = new StringBuffer();
md5Key.append(logicWarehouseStock.getGoodsNo()+"_"+logicWarehouseStock.getWarehouseNo()+"_"+logicWarehouseStock.getOwnerNo()+
"_"+logicWarehouseStock.getDeptNo()+"_"+logicWarehouseStock.getStockType()+"_"+logicWarehouseStock.getGoodsLevel());
if(StringUtils.isBlank(logicWarehouseStock.getFactor1())){
md5Key.append("_0");
}else {
md5Key.append("_"+logicWarehouseStock.getFactor1());
}
if(StringUtils.isBlank(logicWarehouseStock.getFactor2())){
md5Key.append("_0");
}else {
md5Key.append("_"+logicWarehouseStock.getFactor2());
}
if(StringUtils.isBlank(logicWarehouseStock.getFactor3())){
md5Key.append("_0");
}else {
md5Key.append("_"+logicWarehouseStock.getFactor3());
}
if(StringUtils.isBlank(logicWarehouseStock.getFactor4())){
md5Key.append("_0");
}else {
md5Key.append("_"+logicWarehouseStock.getFactor4());
}
if(logicWarehouseStock.getYn()== null){
md5Key.append("_1");
}else {
md5Key.append("_"+logicWarehouseStock.getYn());
}
md5Key.toString().hashCode()
-
批次库存MD5工具
public void fillMd5Value(){
StringBuffer md5Key = new StringBuffer();
md5Key.append(warehouseNo);
md5Key.append("_");
md5Key.append(goodsNo);
md5Key.append("_");
md5Key.append(goodsLevel);
md5Key.append("_");
md5Key.append(stockType);
//遍历类字段不遍历map是为了控制MD5的组成顺序
Class clazz = BatchAttrStock.class;
Field[] fields = clazz.getDeclaredFields();
try {
int batchFieldCount = 0 ;
for (Field field : fields){
BatchAttrEnum attrEnum = BatchAttrEnum.batchFieldEnumMap.get(field.getName());
//不是批属性的字段不进入MD5的组成
if (attrEnum == null){
continue;
}
batchFieldCount ++;
field.setAccessible(true);
Object value = field.get(this);
if (value == null ){
md5Key.append("0");
continue;
}
if(field.getType().toString().contains("String")){
md5Key.append(value);
continue;
}
if(field.getType().toString().contains("Date")){
Date timeField = (Date) value;
md5Key.append(timeField.getTime());
continue;
}
throw new RuntimeException(attrEnum.getField()+"填充MD5异常");
}
//默认50个批属性长度,长度不够0补齐
int remainLength = 50 - batchFieldCount;
String str = String.format("%0"+remainLength+"d", 0);
md5Key.append(str);
}catch (Exception e){
throw new RuntimeException("填充MD5异常.");
}
md5Key.append(yn);
String md5Value = MD5Util.md5(md5Key.toString());
setMd5Value(md5Value);
}
-
MD&ID&属性保存工具
本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。