1.分布式库存扣减时序图和流程图
(1)分布式库存扣减时序图
(2)分布式库存扣减流程图
(3)数据库设计
一.库存分桶操作表
CREATE TABLE `inventory_bucket_operate` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`operate_id` varchar(32) NOT NULL COMMENT '操作id',
`seller_id` varchar(64) NOT NULL COMMENT '卖家id',
`sku_id` varchar(64) NOT NULL COMMENT '商品sku',
`operate_type` tinyint(3) NOT NULL COMMENT '操作类型:1-初始化,2-增加库存,3-分桶上线,4-分桶扩容,5-分桶下线',
`bucket` text COMMENT '分桶变动信息',
`inventory_num` int(11) DEFAULT NULL COMMENT '变动库存',
`feature` text COMMENT '扩展信息',
`operate_status` tinyint(4) DEFAULT '0' COMMENT '操作状态',
`del_flag` tinyint(1) DEFAULT '1' COMMENT '删除标记',
`create_user` int(11) DEFAULT NULL COMMENT '创建⼈',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` int(11) DEFAULT NULL COMMENT '更新⼈',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COMMENT='库存分桶操作表';
二.库存操作失败记录表
CREATE TABLE `inventory_operate_fail` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`operate_id` varchar(32) NOT NULL COMMENT '操作id',
`fail_type` varchar(32) DEFAULT NULL COMMENT '操作类型',
`bucket_no` varchar(32) DEFAULT NULL COMMENT '分桶编号',
`inventory_num` int(11) DEFAULT NULL COMMENT '变动库存数量',
`del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识',
`create_user` int(11) DEFAULT NULL COMMENT '创建⼈',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` int(11) DEFAULT NULL COMMENT '更新⼈',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作失败记录表';
三.库存分桶配置表
CREATE TABLE `inventory_bucket_config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`bucket_num` int(10) NOT NULL DEFAULT '0' COMMENT '分桶数量',
`max_depth_num` int(10) NOT NULL DEFAULT '0' COMMENT '最⼤库存深度,即分桶的最大库存容量',
`min_depth_num` int(10) NOT NULL DEFAULT '0' COMMENT '最⼩库存深度,即分桶的最小库存容量',
`threshold_value` int(10) NOT NULL DEFAULT '0' COMMENT '分桶下线阈值,当某个分桶的库存数小于阈值时就需要将该分桶下线了',
`back_source_proportion` int(10) NOT NULL DEFAULT '0' COMMENT '回源⽐例,从1-100设定⽐例',
`back_source_step` int(10) NOT NULL DEFAULT '0' COMMENT '回源步⻓,桶扩容的时候默认每次分配的库存⼤⼩',
`template_name` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '模板名称',
`is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否默认模板,只允许⼀个,1为默认模板',
`version_id` int(10) NOT NULL DEFAULT '0' COMMENT '版本号',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标记(1-有效,0-删除)',
`create_user` int(10) NOT NULL DEFAULT '0' COMMENT '创建⼈',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` int(10) NOT NULL DEFAULT '0' COMMENT '更新⼈',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='库存分桶配置模板表';
四.库存分配记录表
CREATE TABLE `inventory_allot_detail` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`sku_id` varchar(40) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT 'skuId',
`inventor_no` varchar(32) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '库存申请业务编号',
`seller_id` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '卖家ID',
`inventor_num` int(10) NOT NULL DEFAULT '0' COMMENT '库存变更数量',
`version_id` int(10) NOT NULL DEFAULT '0' COMMENT '版本号',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标记(1-有效,0-删除)',
`create_user` int(10) NOT NULL DEFAULT '0' COMMENT '创建⼈',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` int(10) NOT NULL DEFAULT '0' COMMENT '更新⼈',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `inde_unique_inventor_no` (`inventor_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=265 DEFAULT CHARSET=utf8 COMMENT='库存分配记录表';
五.库存扣减明细表
CREATE TABLE `inventory_deduction_detail` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_id` varchar(32) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '订单id',
`refund_no` varchar(32) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '退款编号',
`inventory_num` int(10) NOT NULL DEFAULT '0' COMMENT '扣减库存数量',
`sku_id` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '商品skuId',
`seller_id` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '卖家ID',
`bucket_no` int(10) NOT NULL COMMENT '扣减分桶编号',
`deduction_type` int(2) NOT NULL COMMENT '库存操作类型(10库存扣减,20库存退货)',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标记(1-有效,0-删除)',
`create_user` int(10) NOT NULL DEFAULT '0' COMMENT '创建⼈',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` int(10) NOT NULL DEFAULT '0' COMMENT '更新⼈',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='库存扣减明细表';
2.库存入桶分配改造
这里主要进行的是库存分桶初始化分配库存的改造。其中的缓存一致性方案是:首先将分桶元数据写入库存分桶操作表,然后再由定时任务扫描处理。
计算出元数据(待上线分桶、中⼼桶剩余库存、每个分桶分配库存)信息后,为了保证⼀致性,会先将计算出的分桶元数据信息⼊库。也就是先写⼊库存分桶操作表,然后在缓存中写⼊中⼼桶剩余库存信息。如果⼊库失败或缓存写⼊失败,会抛出异常,数据库回滚,操作不成功。只有⼊库成功和缓存写⼊成功之后,本次操作才成功。
关键环节与核心代码:
(1)库存分桶初始化入口
@RestController
@RequestMapping("/product/inventory")
public class InventoryController {
@Autowired
private InventoryBucketService inventoryBucketService;
@Autowired
private InventoryBucketCache cache;
@Resource
private TairCache tairCache;
...
//初始化库存
@RequestMapping("/init")
public void inventoryInit(@RequestBody InventorBucketRequest request) {
//清除本地缓存数据,cache.getCache()获取的是Guava Cache
cache.getCache().invalidateAll();
//清除Tair中的数据,扫描卖家ID+SKU的ID的key会比较耗时
Set<String> keys = tairCache.getJedis().keys("*" + request.getSellerId() + request.getSkuId() + "*");
if (!CollectionUtils.isEmpty(keys)) {
tairCache.mdelete(Lists.newArrayList(keys));
}
//这里模拟指定本次的库存业务单号,实际接口需要外部传入
request.setInventorCode(SnowflakeIdWorker.getCode());
//初始化库存信息
inventoryBucketService.inventorBucket(request);
}
...
}
//库存分桶业务实现类
@Service
public class InventoryBucketServiceImpl implements InventoryBucketService {
@Resource
private TairLock tairLock;
...
//商品库存入桶分配
@Override
@Transactional(rollbackFor = Exception.class)
public void inventorBucket(InventorBucketRequest request) {
//1.验证入参必填项
checkInventorParams(request);
//锁key = 卖家ID + SKU的ID
String key = buildBucketLockKey(request.getSellerId(), request.getSkuId());
String value = SnowflakeIdWorker.getCode();
//注意这里需要锁定中心桶库存
boolean lock = tairLock.tryLock(key, value);
//分配库存时,这个卖家的sku是不允许其他相关操作的
if (lock) {
try {
//2.插入库存入库的记录信息
//由于申请的库存业务编号是一个唯一key,所以可以避免重复请求
//也就是会校验库存单号是否已经存在了,保证⼀次库存变更⾏为只能执⾏⼀次
inventoryRepository.saveInventoryAllotDetail(request);
//3.将库存数据写入缓存
inventoryBucketCache(request);
} catch (Exception e) {
e.printStackTrace();
} finally {
tairLock.unlock(key, value);
}
} else {
throw new BaseBizException("请求繁忙,稍后重试!");
}
}
//将库存数据写入缓存
private void inventoryBucketCache(InventorBucketRequest request) {
//获取中心桶库存的key
String key = buildSellerInventoryKey(request.getSellerId(), request.getSkuId());
//1.先验证是否已缓存分桶元数据信息
BucketLocalCache bucketLocalCache = inventoryBucketCache.getBucketLocalCache(request.getSellerId() + request.getSkuId());
try {
//缓存不存在,则进行初始化操作
if (Objects.isNull(bucketLocalCache)) {
//2.获取库存分桶的配置模板
InventoryBucketConfigDO inventoryBucketConfig = inventoryRepository.getInventoryBucketConfig(request.getTemplateId());
//初始化分桶库存
initInventoryBucket(request, inventoryBucketConfig);
} else {
//3.缓存已存在,直接把库存加到中心桶里面,并返回中心桶库存
Integer residueNum = tairCache.incr(key, request.getInventoryNum());
if (residueNum < 0) {
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
}
//4.尝试将库存分配到新的分桶上(注意,先将中心桶的库存加上去)
InventorOnlineRequest onlineRequest = inventoryConverter.converterRequest(request);
//5.构建新的分桶元数据信息,并写入
writeBucketCache(onlineRequest, residueNum);
}
} catch (Exception e) {
log.error("分桶库存初始化出现失败", e);
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
} finally {
inventoryBucketCache.threadLocalRemove();
}
}
...
}
(2)库存分桶元数据计算以及分桶编号创建
//库存分桶业务实现类
@Service
public class InventoryBucketServiceImpl implements InventoryBucketService {
@Resource
private TairLock tairLock;
@Resource
private InventoryRepository inventoryRepository;
@Resource
private InventoryBucketCache inventoryBucketCache;
@Resource
private TairCache tairCache;
...
//初始化分桶库存
private void initInventoryBucket(InventorBucketRequest request, InventoryBucketConfigDO inventoryBucketConfig) {
//计算出分桶的元数据信息
BucketLocalCache bucketLocalCache = calcInventoryBucket(request, inventoryBucketConfig);
//库存分桶的元数据信息入库
inventoryRepository.saveBucketDetail(null, bucketLocalCache, BucketOperateEnum.INIT.getCode(), bucketLocalCache.getAvailableList(), null);
//写入中心桶的剩余库存信息
log.info("中心桶剩余库存:{}", bucketLocalCache.getResidueNum());
//获取中心桶剩余库存的key
String key = buildSellerInventoryKey(bucketLocalCache.getSellerId(), bucketLocalCache.getSkuId());
//设置中心桶剩余库存 = 本次需要入库的库存数量 - 本次最多可以放入分桶的库存数量
boolean setFlag = tairCache.set(key, bucketLocalCache.getResidueNum().toString(), 0);
if (!setFlag) {
//中心桶剩余库存写入失败,回滚事务
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
}
}
//计算出本次库存入库的具体分桶信息
private BucketLocalCache calcInventoryBucket(InventorBucketRequest request, InventoryBucketConfigDO inventoryBucketConfig) {
//分桶配置模版中默认的分桶数量
Integer bucketNum = inventoryBucketConfig.getBucketNum();
//获取本次需要入库的库存数量
Integer inventoryNum = request.getInventoryNum();
//配置模版中所有分桶的最大库存容量
Integer maxBucketNum = bucketNum * inventoryBucketConfig.getMaxDepthNum();
//配置模版中所有分桶的最小库存容量
//如果需要放入分桶的库存数量低于这个值,那么只会分配给部分分桶,此时就需要重新计算分桶
Integer minBucketNum = bucketNum * inventoryBucketConfig.getMinDepthNum();
//本次最多可以放入分桶的库存数量
int countBucketNum = Math.min(inventoryNum, maxBucketNum);
//当库存数量小于最小分桶深度*分桶数量,就需要减少分配的分桶数
//此时要分配的分桶数量 bucketNum = 本次库存入库的数量 / 每个分桶的最小库存容量
if (minBucketNum > countBucketNum) {
bucketNum = countBucketNum / inventoryBucketConfig.getMinDepthNum();
//如果库存数量不足一个分桶的最小深度,但是大于0,则上线一个分桶
if (bucketNum == 0 && countBucketNum % inventoryBucketConfig.getMinDepthNum() > 0) {
bucketNum++;
}
}
//获取每个分桶分配的库存数量
Integer bucketInventoryNum = countBucketNum / bucketNum;
//剩余库存数量,可能为0或者大于0,补到最后一个分桶上
Integer residueNum = countBucketNum - bucketInventoryNum * bucketNum;
//构建缓存数据模型时,以卖家ID + 商品skuId为唯一标识
String key = request.getSellerId() + request.getSkuId();
//构建具体的缓存数据模型
BucketLocalCache bucketLocalCache = buildBucketCache(key, bucketNum, bucketInventoryNum, residueNum, inventoryBucketConfig);
//标记到具体的数据上
bucketLocalCache.setSellerId(request.getSellerId());
bucketLocalCache.setSkuId(request.getSkuId());
bucketLocalCache.setInventoryNum(inventoryNum);
//中心桶剩余库存 = 本次需要入库的库存数量 - 本次最多可以放入分桶的库存数量
bucketLocalCache.setResidueNum(inventoryNum - countBucketNum);
bucketLocalCache.setInventoryBucketConfig(inventoryBucketConfig);
bucketLocalCache.setOperationType(BucketStatusEnum.AVAILABLE_STATUS.getCode());
return bucketLocalCache;
}
//构建缓存模型
//@param bucketNum 分桶数量
//@param inventoryNum 分桶分配的库存数量
//@param residueNum 剩余的未分配均匀的库存
//@param inventoryBucketConfig 分桶配置信息
private BucketLocalCache buildBucketCache(String key, Integer bucketNum, Integer inventoryNum, Integer residueNum, InventoryBucketConfigDO inventoryBucketConfig) {
BucketLocalCache bucketLocalCache = new BucketLocalCache();
//先获取得到这个模板配置的对应可分槽位的均匀桶列表
List<String> bucketNoList = InventorBucketUtil.createBucketNoList(key, inventoryBucketConfig.getBucketNum());
List<BucketCacheBO> bucketCacheBOList = new ArrayList<>(bucketNum);
List<BucketCacheBO> undistributedList = new ArrayList<>(bucketNum);
//构建出多个分桶对象
for (int i = 0; i < bucketNum; i++) {
//生成对应的分桶编号,方便定义到具体的分桶上
BucketCacheBO bucketCache = new BucketCacheBO();
String bucketNo = bucketNoList.get(i);
bucketCache.setBucketNo(bucketNo);
//最后一个分桶,分配剩余未除尽的库存+平均库存
if (i == bucketNum - 1) {
bucketCache.setBucketNum(inventoryNum + residueNum);
} else {
bucketCache.setBucketNum(inventoryNum);
}
bucketCacheBOList.add(bucketCache);
}
//生成的分桶对象超过实际可分配的分桶对象,保留这批多余的分桶模型为不可用分桶,后续分桶上线可以选择使用
if (bucketNoList.size() > bucketNum) {
for (int i = bucketNum; i < bucketNoList.size(); i++) {
BucketCacheBO bucketCache = new BucketCacheBO();
String bucketNo = bucketNoList.get(i);
bucketCache.setBucketNo(bucketNo);
undistributedList.add(bucketCache);
}
}
//生成缓存的明细key
List<String> bucketDetailKeyList = InventorBucketUtil.createBucketNoList(key, inventoryBucketConfig.getBucketNum(), "%07d");
//设置分桶缓存明细的key
bucketLocalCache.setBucketDetailKeyList(bucketDetailKeyList);
//设置可用的分桶缓存列表
bucketLocalCache.setAvailableList(bucketCacheBOList);
//设置不可用或者已下线的分桶缓存列表
bucketLocalCache.setUndistributedList(undistributedList);
return bucketLocalCache;
}
...
}
public class InventorBucketUtil {
private static final int MAX_SIZE = 100000;
//生成对应的槽位key,默认的
//@param key 卖家Id+商品skuId
//@param bucketNum 分桶配置数量
//@return 预先保留的槽位集合
public static List<String> createBucketNoList(String key, Integer bucketNum) {
return createBucketNoList(key, bucketNum, "%06d");
}
//生成对应的槽位key,明细使用,多使用一位区分
//@param key 卖家Id+商品skuId
//@param bucketNum 分桶配置数量
//@return 预先保留的槽位集合
public static List<String> createBucketNoList(String key, Integer bucketNum, String format) {
Map<Long, String> cacheKey = new HashMap<>(bucketNum);
//bucketNoList用来存放每个桶对应的hashKey
List<String> bucketNoList = new ArrayList<>(bucketNum);
//分配桶的编号
for (int i = 1; i <= MAX_SIZE; i++) {
String serialNum = String.format(format, i);
//卖家ID + 商品SKU ID + 序号
String hashKey = key + serialNum;
//一致性哈希算法murmur
long hash = HashUtil.murMurHash(hashKey.getBytes());
//对分桶数量进行取模运算
long c = (hash %= bucketNum);
//确保被选中的hashKey都能哈希到不同的分桶
if (cacheKey.containsKey(c)) {
continue;
}
cacheKey.put(c, hashKey);
bucketNoList.add(hashKey);
if (cacheKey.size() >= bucketNum) {
break;
}
}
return bucketNoList;
}
...
}
(3)库存分桶操作记录写DB + 中心桶库存写缓存
//库存分桶业务实现类
@Service
public class InventoryBucketServiceImpl implements InventoryBucketService {
@Resource
private TairLock tairLock;
@Resource
private InventoryRepository inventoryRepository;
@Resource
private InventoryBucketCache inventoryBucketCache;
@Resource
private TairCache tairCache;
...
//初始化分桶库存
private void initInventoryBucket(InventorBucketRequest request, InventoryBucketConfigDO inventoryBucketConfig) {
//计算出分桶的元数据信息
BucketLocalCache bucketLocalCache = calcInventoryBucket(request, inventoryBucketConfig);
//库存分桶的元数据信息入库
inventoryRepository.saveBucketDetail(null, bucketLocalCache, BucketOperateEnum.INIT.getCode(), bucketLocalCache.getAvailableList(), null);
//写入中心桶的剩余库存信息
log.info("中心桶剩余库存:{}", bucketLocalCache.getResidueNum());
//获取中心桶剩余库存的key
String key = buildSellerInventoryKey(bucketLocalCache.getSellerId(), bucketLocalCache.getSkuId());
//设置中心桶剩余库存 = 本次需要入库的库存数量 - 本次最多可以放入分桶的库存数量
boolean setFlag = tairCache.set(key, bucketLocalCache.getResidueNum().toString(), 0);
if (!setFlag) {
//中心桶剩余库存写入失败,回滚事务
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
}
}
...
}
@Repository
public class InventoryRepository {
...
//保存库存分桶的元数据信息
//@param operateId 操作id
//@param bucketLocalCache 变更之后的元数据信息
//@param operateType 操作类型
//@param bucketCacheBOList 变动的分桶列表
//@param inventoryNum 变动的库存数量
public void saveBucketDetail(String operateId, BucketLocalCache bucketLocalCache, Integer operateType,
List<BucketCacheBO> bucketCacheBOList, Integer inventoryNum) {
//变动的分桶为空,则不必要保存
if (CollectionUtils.isEmpty(bucketCacheBOList)) {
return;
}
if (!StringUtils.hasLength(operateId)) {
operateId = SnowflakeIdWorker.getCode();
}
InventoryBucketOperateDO inventoryBucketOperateDO = InventoryBucketOperateDO.builder()
.operateId(operateId)
.sellerId(bucketLocalCache.getSellerId())
.skuId(bucketLocalCache.getSkuId())
.bucket(JSON.toJSONString(bucketCacheBOList))
.operateType(operateType)
.feature(JSON.toJSONString(bucketLocalCache))
.inventoryNum(inventoryNum)
.build();
int count = inventoryBucketOperateMapper.insert(inventoryBucketOperateDO);
if (count <= 0) {
throw new BaseBizException(InventoryExceptionCode.INVENTORY_SQL);
}
}
...
}
(4)库存分桶元数据缓存更新使用自缓存一致性服务的DB + 消息双写方案
InventoryBucketCache.setBucketLocalCache()方法设置库存分桶元数据到本地缓存时,就用到了缓存一致性框架。也就是使用了缓存一致性服务的@CacheRefresh注解,因为库存分桶元数据属于热点数据,对实时性要求比较高。在一台机器的本地缓存了库存分桶元数据后,其他机器也应缓存该数据。
@Component
@Data
public class InventoryBucketCache {
@Autowired
private Cache cache;
@Resource
private TairCache tairCache;
private ThreadLocal<BucketLocalCache> bucketLocalCacheThreadLocal = new ThreadLocal<>();
//本地存储关于分桶信息
@CacheRefresh(
cacheKey = "bucketKey",
mqCacheKey = CacheConstant.INVENTORY_SKU_KEY,
index = "1",
messageType = CacheConstant.MESSAGE_TYPE_HOT,
cacheType = CacheConstant.TAIR_CACHE_TYPE
)
public void setBucketLocalCache(String bucketKey, BucketLocalCache bucketLocalCache) {
String bucketLocalKey = TairInventoryConstant.SELLER_BUCKET_PREFIX + bucketLocalCache.getSellerId() + bucketLocalCache.getSkuId();
synchronized (bucketLocalKey.intern()) {
log.info("保存本地缓存元数据 key:{}, value:{}", bucketKey, JSON.toJSONString(bucketLocalCache));
BucketLocalCache bucketCache = getTairBucketCache(bucketKey);
log.info("远程缓存元数据 key:{}, value:{}", bucketKey, JSON.toJSONString(bucketCache));
//如果本地缓存没有就直接写入
if (Objects.isNull(bucketCache)) {
setBucketCache(bucketKey, bucketLocalCache);
cache.put(bucketKey, bucketLocalCache);
return;
}
//本地缓存的元数据覆盖,考虑到是并发执行的,这里需要上内存级别的锁,并进行diff处理
if (bucketLocalCache.getOperationType().equals(BucketStatusEnum.AVAILABLE_STATUS.getCode())) {
diffCacheOnline(bucketCache, bucketLocalCache);
} else if (bucketLocalCache.getOperationType().equals(BucketStatusEnum.OFFLINE_STATUS.getCode())) {
diffCacheOffline(bucketCache, bucketLocalCache);
}
setBucketCache(bucketKey, bucketCache);
cache.put(bucketKey, bucketCache);
log.info("实际保存本地缓存元数据 key:{}, value:{}", bucketKey, JSON.toJSONString(bucketCache));
}
}
...
}
//刷新缓存的自定义注解
@Aspect
@Component
public class CacheRefreshAspect {
@Autowired
private DataRefreshProducer producer;
@Autowired
private CacheRefreshConverter cacheRefreshConverter;
@Autowired
private CacheQueue cacheQueue;
//切入点,@CacheRefresh注解标注的
@Pointcut("@annotation(com.demo.eshop.cache.annotation.CacheRefresh)")
public void pointcut() {
}
//环绕通知,在方法执行前后
//@param point 切入点
//@return 结果
@Around("pointcut() && @annotation(cacheRefresh)")
public Object around(ProceedingJoinPoint point, CacheRefresh cacheRefresh) throws Throwable {
//签名信息
Signature signature = point.getSignature();
//强转为方法信息
MethodSignature methodSignature = (MethodSignature) signature;
//参数名称
String[] parameterNames = methodSignature.getParameterNames();
//参数值
Object[] parameterValues = point.getArgs();
Object response;
try {
//先执行本地方法再执行异步的操作
response = point.proceed();
} catch (Throwable throwable) {
log.error("执行方法: {}失败,异常信息: {}", methodSignature.getMethod().getName(), throwable);
throw throwable;
}
try {
MessageCache messageCache = new MessageCache();
for (int i = 0; i < parameterValues.length; i++) {
if (parameterNames[i].equals(cacheRefresh.cacheKey())) {
messageCache.setCacheKey(String.valueOf(parameterValues[i]));
}
if (Integer.valueOf(cacheRefresh.index()) == i) {
messageCache.setCacheJSON(JSONObject.toJSONString(parameterValues[i]));
}
}
messageCache.setOperationType(Integer.valueOf(cacheRefresh.operationType()));
//给定一个有序的版本号(默认统一的工作ID和数据中心ID)
messageCache.setVersion(SnowflakeIdWorker.getVersion());
messageCache.setMessageType(Integer.valueOf(cacheRefresh.messageType()));
messageCache.setCacheType(Integer.valueOf(cacheRefresh.cacheType()));
messageCache.setCreateDate(new Date());
//将缓存数据写入读写队列
//缓存数据写入读写队列后,会定时每秒批量写入数据库(缓存数据写入DB只用于兜底,所以偶尔出现丢失并不影响)
DataRefreshDetailDO dataRefreshDetailDO = cacheRefreshConverter.converter(messageCache);
cacheQueue.submit(dataRefreshDetailDO);
//发送MQ消息去处理缓存数据,比如将缓存数据更新到缓存上
//一般来说,热点缓存会比普通缓存少很多,所以普通缓存的更新会比较多,热点缓存的更新会比较少
//此外,热点缓存的更新会对时效性要求比较高,通过消息去异步处理本来就已存在一定的延迟
//所以这里将普通缓存和热点缓存的更新进行分开处理,减少时效性高的消息的处理延迟
if (CacheConstant.MESSAGE_TYPE_HOT.equals(cacheRefresh.messageType())) {
producer.sendMessage(RocketMqConstant.DATA_HOT_RADIO_TOPIC, JSONObject.toJSONString(messageCache), "热点缓存消息发送");
} else {
producer.sendMessage(RocketMqConstant.DATA_MESSAGE_CACHE_TOPIC, JSONObject.toJSONString(messageCache), cacheRefresh.mqCacheKey(), "通用缓存消息发送");
}
} catch (Exception e) {
log.error("处理缓存同步:{}失败,异常信息:{}", methodSignature.getMethod().getName(), e);
}
return response;
}
}
(5)库存分桶元数据作为热点数据更新到各机器节点的本地缓存
setBucketLocalCache()方法的@CacheRefresh注解描述缓存是热点类型。该方法被执行后,会被AOP切面切入,将需要缓存的数据发送到MQ。接着MQ会对这种热点类型的消息进行广播处理,也就是每台机器都会执行CacheRefreshListener的方法。
@Configuration
public class ConsumerBeanConfig {
...
//刷新本地缓存
@Bean("cacheRefresTopic")
public DefaultMQPushConsumer cacheRefresTopic(CacheRefreshListener cacheRefreshListener) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.REFRESH_CACHE_GROUP);
//设置为广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
consumer.subscribe(RocketMqConstant.DATA_HOT_RADIO_TOPIC, "*");
consumer.registerMessageListener(cacheRefreshListener);
consumer.start();
return consumer;
}
...
}
//刷新分布式节点的本地缓存
@Component
public class CacheRefreshListener implements MessageListenerConcurrently {
//本地缓存
@Autowired
private Cache cache;
@Resource
private InventoryBucketCache inventoryBucketCache;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try {
for (MessageExt messageExt : list) {
String msg = new String(messageExt.getBody());
log.info("刷新本地缓存,消息内容:{},消费时间:{}", msg, DateFormatUtil.formatDateTime(new Date()));
MessageCache messageCache = JsonUtil.json2Object(msg, MessageCache.class);
BucketLocalCache bucketLocalCache = JsonUtil.json2Object(messageCache.getCacheJSON(), BucketLocalCache.class);
synchronized (messageCache.getCacheKey().intern()) {
String bucketLocalKey = TairInventoryConstant.SELLER_BUCKET_PREFIX + bucketLocalCache.getSellerId() + bucketLocalCache.getSkuId();
//获取远程缓存的分桶元数据信息
BucketLocalCache bucketCache = inventoryBucketCache.getTairBucketCache(bucketLocalKey);
if (Objects.isNull(bucketCache)) {
cache.put(messageCache.getCacheKey(), JsonUtil.json2Object(messageCache.getCacheJSON(), BucketLocalCache.class));
log.info("更新本地缓存,本次更新内容:{},更新时间:{}", messageCache.getCacheJSON(), DateFormatUtil.formatDateTime(new Date()));
} else {
//进行diff数据处理
if (bucketLocalCache.getOperationType().equals(BucketStatusEnum.AVAILABLE_STATUS.getCode())) {
inventoryBucketCache.diffCacheOnline(bucketCache, bucketLocalCache);
} else if (bucketLocalCache.getOperationType().equals(BucketStatusEnum.OFFLINE_STATUS.getCode())) {
inventoryBucketCache.diffCacheOffline(bucketCache, bucketLocalCache);
}
cache.put(messageCache.getCacheKey(), bucketCache);
log.info("更新本地缓存,本次更新内容:{},更新时间:{}", JsonUtil.object2Json(bucketCache), DateFormatUtil.formatDateTime(new Date()));
}
}
}
} catch (Exception e) {
//本地缓存只有参数转换会出错,这种错误重试也没什么作用
log.error("consume error, 刷新本地缓存失败", e);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
3.库存分桶上线改造
当向库存分桶增加库存时,会调用分桶上线接⼝,也就是会调⽤InventoryBucketServiceImpl的writeBucketCache()⽅法,writeBucketCache()⽅法会实现具体的分桶上线任务。
InventoryBucketServiceImpl的bucketOnline()方法,适⽤场景是在商品库存⼊桶时,分桶上线中存在上线失败的分桶。此时运营⼈员就可以通过bucketOnline()方法⼿动执⾏分桶的上线。从而防⽌上线的分桶过少,承担的并发压⼒⼤。
其中的缓存一致性方案是:首先将分桶元数据写入库存分桶操作表,然后再由定时任务扫描处理。
也是先把分桶元数据写⼊到数据库中,然后再操作中⼼桶缓存数据。数据库写⼊成功和缓存写⼊成功,则本次操作成功。数据库写⼊失败或者缓存写⼊失败,都会回滚数据库,本次操作失败。
//库存分桶业务实现类
@Service
public class InventoryBucketServiceImpl implements InventoryBucketService {
@Resource
private TairLock tairLock;
@Resource
private InventoryRepository inventoryRepository;
@Resource
private InventoryBucketCache inventoryBucketCache;
@Resource
private TairCache tairCache;
...
//商品库存入桶分配
@Override
@Transactional(rollbackFor = Exception.class)
public void inventorBucket(InventorBucketRequest request) {
//1.验证入参必填项
checkInventorParams(request);
//锁key = 卖家ID + SKU的ID
String key = buildBucketLockKey(request.getSellerId(), request.getSkuId());
String value = SnowflakeIdWorker.getCode();
//注意这里需要锁定中心桶库存
boolean lock = tairLock.tryLock(key, value);
//分配库存时,这个卖家的sku是不允许其他相关操作的
if (lock) {
try {
//2.插入库存入库的记录信息
//由于申请的库存业务编号是一个唯一key,所以可以避免重复请求
//也就是会校验库存单号是否已经存在了,保证⼀次库存变更⾏为只能执⾏⼀次
inventoryRepository.saveInventoryAllotDetail(request);
//3.将库存数据写入缓存
inventoryBucketCache(request);
} catch (Exception e) {
e.printStackTrace();
} finally {
tairLock.unlock(key, value);
}
} else {
throw new BaseBizException("请求繁忙,稍后重试!");
}
}
//将库存数据写入缓存
private void inventoryBucketCache(InventorBucketRequest request) {
//获取中心桶库存的key
String key = buildSellerInventoryKey(request.getSellerId(), request.getSkuId());
//1.先验证是否已缓存分桶元数据信息,先查本地缓存,再查远程缓存
BucketLocalCache bucketLocalCache = inventoryBucketCache.getBucketLocalCache(request.getSellerId() + request.getSkuId());
try {
//缓存不存在,则进行初始化操作
if (Objects.isNull(bucketLocalCache)) {
//2.获取库存分桶的配置模板
InventoryBucketConfigDO inventoryBucketConfig = inventoryRepository.getInventoryBucketConfig(request.getTemplateId());
//初始化分桶库存
initInventoryBucket(request, inventoryBucketConfig);
} else {
//3.缓存已存在,直接把库存加到中心桶里面,并返回中心桶库存
Integer residueNum = tairCache.incr(key, request.getInventoryNum());
if (residueNum < 0) {
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
}
//4.尝试将库存分配到新的分桶上(注意,先将中心桶的库存加上去)
InventorOnlineRequest onlineRequest = inventoryConverter.converterRequest(request);
//5.构建新的分桶元数据信息,并写入
writeBucketCache(onlineRequest, residueNum);
}
} catch (Exception e) {
log.error("分桶库存初始化出现失败", e);
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
} finally {
inventoryBucketCache.threadLocalRemove();
}
}
//分桶上线接口
@Override
@Transactional(rollbackFor = Exception.class)
public void bucketOnline(InventorOnlineRequest request) {
//1.验证入参必填
checkInventorOnlineParams(request);
//2.注意这里需要锁定中心桶库存
String key = buildBucketLockKey(request.getSellerId(), request.getSkuId());
String value = SnowflakeIdWorker.getCode();
boolean lock = tairLock.tryLock(key, value);
if (lock) {
try {
//3.获取中心桶的库存,并校验是否可上线分桶
Integer residueNum = checkBucketOnlineNum(key);
//4.构建新的分桶元数据信息,并写入
writeBucketCache(request, residueNum);
} catch (Exception e) {
e.printStackTrace();
} finally {
tairLock.unlock(key, value);
}
} else {
throw new BaseBizException("请求繁忙,稍后重试!");
}
}
...
//构建新的分桶元数据信息
//@param request 分桶上线对象
//@param residueNum 中心桶剩余库存
private void writeBucketCache(InventorOnlineRequest request, Integer residueNum) {
String key = request.getSellerId() + request.getSkuId();
//获取到本地的缓存列表
BucketLocalCache bucketLocalCache = inventoryBucketCache.getBucketLocalCache(key);
try {
if (!Objects.isNull(bucketLocalCache)) {
//获取当前可上线的分桶列表信息
List<BucketCacheBO> bucketCacheBOList = buildBucketList(request.getBucketNoList(), bucketLocalCache.getAvailableList(), bucketLocalCache.getUndistributedList(), bucketLocalCache.getInventoryBucketConfig(), residueNum);
//当前可上线的分桶为空,直接返回
if (CollectionUtils.isEmpty(bucketCacheBOList)) {
return;
}
//中心桶被扣减掉的库存(上线的分桶库存总和)
Integer descInventoryNum = bucketCacheBOList.stream().mapToInt(BucketCacheBO::getBucketNum).sum();
//构建返回新的元数据模型返回
buildBucketLocalCache(bucketLocalCache, bucketCacheBOList, residueNum - descInventoryNum);
//分桶信息入库
inventoryRepository.saveBucketDetail(null, bucketLocalCache, BucketOperateEnum.ONLINE.getCode(), bucketCacheBOList, descInventoryNum);
//扣减中心桶剩余库存,如果扣减失败了,直接抛异常
Integer decr = tairCache.decr(buildSellerInventoryKey(request.getSellerId(), request.getSkuId()), descInventoryNum);
if (decr < 0) {
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
}
}
} catch (Exception e) {
log.error("分桶构建初始化失败", e);
throw new BaseBizException(InventoryExceptionCode.INVENTORY_CACHE);
} finally {
inventoryBucketCache.threadLocalRemove();
}
}
//获取可上线的分桶列表信息以及具体上线库存
//@param bucketNoList 上线分桶编号列表
//@param availableList 上线正在使用的分桶编号列表
//@param undistributedList 下线或者未使用的分桶编号列表
//@param inventoryBucketConfigDO 当前分桶的配置模板信息
//@param residueNum 中心桶的剩余可分配库存
//@return 可上线的分桶列表以及具体分桶库存
private List<BucketCacheBO> buildBucketList(List<String> bucketNoList, List<BucketCacheBO> availableList, List<BucketCacheBO> undistributedList, InventoryBucketConfigDO inventoryBucketConfigDO, Integer residueNum) {
//1.如果入参选择了上线的分桶编号列表,则从缓存中配置的未使用分桶列表进行比对处理
List<String> bucketCacheList = null;
if (!CollectionUtils.isEmpty(bucketNoList)) {
Map<String, BucketCacheBO> bucketCacheMap = undistributedList.stream().collect(Collectors.toMap(BucketCacheBO::getBucketNo, Function.identity()));
//过滤返回可用的分桶编号
bucketCacheList = bucketNoList.stream().filter(bucketNo -> bucketCacheMap.containsKey(bucketNo)).collect(Collectors.toList());
} else {
//直接返回下线的不可用分桶列表
bucketCacheList = undistributedList.stream().map(BucketCacheBO::getBucketNo).collect(Collectors.toList());
}
//可上线的分桶列表为空
if (CollectionUtils.isEmpty(bucketCacheList)) {
return Lists.newArrayList();
}
//2.根据中心桶的可分配库存,处理返回具体上线的分桶配置信息
return calcOnlineBucket(availableList, bucketCacheList, residueNum, inventoryBucketConfigDO);
}
//构建新的元数据模型
//@param bucketLocalCache 本地分桶元数据信息
//@param bucketCacheBOList 上线的分桶列表
//@param residueNum 中心桶剩余库存
private void buildBucketLocalCache(BucketLocalCache bucketLocalCache, List<BucketCacheBO> bucketCacheBOList, Integer residueNum) {
//填充中心桶剩余库存
bucketLocalCache.setResidueNum(residueNum);
//添加新上线的分桶列表
bucketLocalCache.getAvailableList().addAll(bucketCacheBOList);
Map<String, BucketCacheBO> bucketCacheMap = bucketCacheBOList.stream().collect(Collectors.toMap(BucketCacheBO::getBucketNo, Function.identity()));
List<BucketCacheBO> undistributedList = bucketLocalCache.getUndistributedList().stream().filter(bucketCacheBO ->
//在上线的分桶列表,需要移除掉
!bucketCacheMap.containsKey(bucketCacheBO.getBucketNo())).collect(Collectors.toList());
//从不可用的分桶列表重移除
bucketLocalCache.setUndistributedList(undistributedList);
bucketLocalCache.setOperationType(BucketStatusEnum.AVAILABLE_STATUS.getCode());
}
...
}
@Repository
public class InventoryRepository {
...
//保存库存分桶的元数据信息
//@param operateId 操作id
//@param bucketLocalCache 变更之后的元数据信息
//@param operateType 操作类型
//@param bucketCacheBOList 变动的分桶列表
//@param inventoryNum 变动的库存数量
public void saveBucketDetail(String operateId, BucketLocalCache bucketLocalCache, Integer operateType,
List<BucketCacheBO> bucketCacheBOList, Integer inventoryNum) {
//变动的分桶为空,则不必要保存
if (CollectionUtils.isEmpty(bucketCacheBOList)) {
return;
}
if (!StringUtils.hasLength(operateId)) {
operateId = SnowflakeIdWorker.getCode();
}
InventoryBucketOperateDO inventoryBucketOperateDO = InventoryBucketOperateDO.builder()
.operateId(operateId)
.sellerId(bucketLocalCache.getSellerId())
.skuId(bucketLocalCache.getSkuId())
.bucket(JSON.toJSONString(bucketCacheBOList))
.operateType(operateType)
.feature(JSON.toJSONString(bucketLocalCache))
.inventoryNum(inventoryNum)
.build();
int count = inventoryBucketOperateMapper.insert(inventoryBucketOperateDO);
if (count <= 0) {
throw new BaseBizException(InventoryExceptionCode.INVENTORY_SQL);
}
}
...
}
文章转载自: 东阳马生架构