背景介绍
随同物流行业的迅猛发展,一体化供应链模式的落地,对系统吞吐、零碎稳固收回微小挑战,库存作为供应链的重中之重体现更为显著。近三年数据能够看出:
接入商家同比增长 37.64%、货品品种同比增长 53.66%
货品数量同比增长 46.43%、仓库数量同比增长 18.87%
通过剖析过往大促流量,分钟级流量增长率为 75%,大促仓内反馈三方订单下传不及时,库存预占吞吐量和性能是导致订单积压因素之一。目前库存应用 mysql 数据库作为接单预占的扛量伎俩,随着一体化供应链建设以及重点 KA 商家一直接入,现有库存架构在业务撑持上存在危险和缺点。
此外将来 3 到 5 年业务增长、流量增长预计增长 5 -10 倍。为防止零碎性能和技术架构缺点导致业务损失,轻量级库存架构势在必行。
// 名词解释:
库存预占:是指消费者拍下商品订单后,库存先为该订单短暂预留,预留的库存即为预占库存。
架构准则
架构 :是⾯向问题,解决问题的伎俩。库存零碎的问题: 非功能性:1. 高并发 2. 零碎稳定性 (容灾) 3. 数据一致性 功能性: 1. 业务简单 2. 数据一致性
零碎设计
设计思路
- 以后库存零碎瓶颈在哪里?:抗写流量,数据库成为瓶颈点。
- 如何解决零碎瓶颈?:由高并发组件 Redis 代替数据库。
- 利用 Redis 须要解决哪些问题?:防超卖,异步写数据库保障最终一致性。
总体设计
- 扛量局部 :库存性能瓶颈在预占,传统架构次要依附数据库事务保持数据统一以及数据读写;新版架构设计将数据扛量局部移植到 Redis,利用 Redis 高性能吞吐解决高并发场景下数据读写。
- 数据回写 :Redis 进行扛量削峰,后续数据仅用于记账,最终就义数据的短暂一致性达到削峰的目标。
- 差别局部 :老版本库存预占设计仅依附数据进行数据处理,新版设计依附切量配置建数据切换到 Redis,利用 Redis 高读写进行削峰操作。
具体设计
- 主流程:
- 库存初始化 :竞态条件利用 Redis watch 命令来实现锁期待,解决并发场景数据不统一问题。
- LUA 执行器 :将原子操作指令 / 复用指令封装到 LUA 脚本中以缩小网络开销。
- 弥补机制:i> 执行流程中所有业务异样产生时会同步发动反向操作申请;ii> 反向操作执行异样后会提交异步反向操作工作;**iii>** 异步工作执行异样后,依赖监 q 控系统扫描异样单据或异样库存并批改异样库存量
- 回溯回写 :工作落库后收回 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: {库存记录索引}
key | 预占 | 缺量预占 | 回滚 | 回写 |
---|---|---|---|---|
可售库存数量 | – | – | + | 不变 |
扣减库存量 | + | + | – | – |
预占防重 key | + | + | – | 不变 |
回滚防重 | 不变 | 不变 | + | 不变 |
缺量预占库存量 | 不变 | + | 反向 | 不变 |
扣减库存单据记录 | + | + | – | – |
Redis&DB
- 首先进行 redis& 从库数据比对,若存在差别则对主库进行校验
- 比对过程中,DB 中 sku 明细行进行锁定 (for update),比对逻辑为 DB 可用库存量 ==(Redis 可用库存量 +Redis 预占量)
- 有差别,报警且触发 SDK 可用量过期,同时改正预占量
容灾计划
// 对系统容错 / 降级、监控机制 (空间换稳定性,两份 redis,故障 3 次丢数),流量散布资料,618 流量大、峰值数据切量。数据不统一,多个商家,不能超过 5 分。
预占工作长久化:mysql 须要将外围属性字段数据长久化:事业部,商品编码,仓编码,等级,库存类型,库存状态,预占库存量,工作状态; 调度执行实现后须要更新 stockTask 状态为实现
初始化:
(1) lock db
(2) sum stockTask
(3) 应用 DB 可用库存初始化 Redis 可用库存,stockTask 预占量初始化 Redis 预占量
(4)Redis 库存回滚,如果预占量 key 不存在,该 key 不须要回滚
性能后果
23 年 618 大促
切量细则
切量细则
冷热数据
OMS 库存冷热安装
预占架构降级切量重点 key 监控
库存预占架构降级切量商家
架构降级切量商家明细 2
已切量商家
反向切量
原有设计中存在以下名单
禁止切量商家:优先级较高,一旦在名单中,禁止切量
批次库存商家:批次库存治理商家,目前该局部能力尚未建设
动静质押商家:物流金融业务,目前该局部能力尚未建设 切量名单商家:该局部为切量商家
原有切量流程:!禁止切量 ->!批次库存 ->!动静质押 -> 切量名单中,通过以上校验为切量商家。
原有流程在增量商家中须要手动将商家配置到切量名单中才可进行切量操作,对于新增商家场景操作不变,且原有流程中逻辑库存名单为痛点:逻辑库存的启用配置在事业部主数据中,不在库存侧。
新版切量流程中对切量名单进行优化,将原来切量名单商家拆分成非逻辑库存名单、逻辑库存两个名单,其中:
非逻辑库存名单:蕴含可切量商家
逻辑库存名单:逻辑库存商家,该局部不可切量
原流程新流程对切量商家名单进行优化,拆分成非逻辑库存名单、逻辑库存两个名单
构建模型 (批次库存 & 内存模型待续)
Redis 存储数据结构
- 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& 属性保留工具
本文篇幅无限,余下二期进行分享。
作者:京东物流 金鹏
起源:京东云开发者社区 自猿其说 Tech 转载请注明起源