在进行秒杀项目的开发过程中,遇到了一个问题,下单-库存减少
异步处理库存,下单后,进行redis操作,订单入库,销量增加,异步操作数据库库存
一、直接使用Springboot提供方法
// Springboot 提供的事务操作方法 当前事务操作成功 方法调用
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 异步更新库存
boolean mqResult = itemService.asyncDecreaseStock(itemId, amount);
if (!mqResult) {
itemService.increaseStock(itemId, amount);
throw new BusinessException(EmBusinessError.MQ_SEND_FAIL);
}
}
});
二、通过库存流水进行状态标识,配合rocketmq事务
具体的操作逻辑
1、controller 进行 mq异步事务调用
if (redisTemplate.hasKey("promo_stock_invalid_" + itemId)) {
throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
}
// 加入库存流水
String stockLogResultId = itemService.initStockLog(itemId, amount);
// 完成下单事务消息
if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount, stockLogResultId)) {
throw new BusinessException(EmBusinessError.UNKNOW_ERROR,"下单失败");
}
2、通过消息队列 事务型异步扣减库存
// 事务型异步库存扣减消息
public boolean transactionAsyncReduceStock(String userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) {
Map bodyMap = new HashMap<>();
bodyMap.put("itemId", itemId);
bodyMap.put("amount", amount);
bodyMap.put("stockLogId", stockLogId);
Map argsMap = new HashMap<>();
argsMap.put("itemId", itemId);
argsMap.put("amount", amount);
argsMap.put("promoId", promoId);
argsMap.put("userId", userId);
argsMap.put("stockLogId", stockLogId);
Message message = new Message(topicName, "increase", JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
TransactionSendResult transactionSendResult = null;
try {
transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, argsMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if (transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
return true;
} else {
return false;
}
}
3、回到 transactionMQProducer的监听中 进行逻辑处理
transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.start();
transactionMQProducer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object args) {
// 真正要做的事 创建订单
String userId = (String)((Map)args).get("userId");
Integer itemId = (Integer)((Map)args).get("itemId");
Integer amount = (Integer)((Map)args).get("amount");
Integer promoId = (Integer)((Map)args).get("promoId");
String stockLogId = (String)((Map)args).get("stockLogId");
try {
orderService.createOrder(userId, itemId, promoId, amount, stockLogId);
} catch (BusinessException e) {
e.printStackTrace();
// 状态设置为回滚状态
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
stockLogDO.setStatus(3);
stockLogDOMapper.updateByPrimaryKey(stockLogDO);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
// 根据是否扣减库存成功 判断是否 commit rollback unknown
String jsonString = new String(messageExt.getBody());
Map map = JSON.parseObject(jsonString, Map.class);
Integer itemId = (Integer) map.get("itemId");
Integer amount = (Integer) map.get("amount");
String stockLogId = (String) map.get("stockLogId");
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
if (stockLogDO == null) {
return LocalTransactionState.UNKNOW;
}
if (stockLogDO.getStatus() == 2) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (stockLogDO.getStatus() == 1) {
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
3、库存流水状态标记 库存击穿
通过库存流水表 可以查到每一单的 订单状态 再进行库存的对于处理
// 初始化库存流水
@Override
public String initStockLog(Integer itemId, Integer amount) {
StockLogDO stockLogDO = new StockLogDO();
stockLogDO.setItemId(itemId);
stockLogDO.setAmount(amount);
stockLogDO.setStatus(1);
stockLogDO.setStockLogId(UUID.randomUUID().toString().replace("-", ""));
stockLogDOMapper.insert(stockLogDO);
return stockLogDO.getStockLogId();
}
// 加入库存流水
String stockLogResultId = itemService.initStockLog(itemId, amount);
@Override
@Transactional
public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
// 操作失败 结果是0
// int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
long result = redisTemplate.opsForValue().increment("promo_stock"+itemId, amount.intValue() * -1);
if (result > 0) {
return true;
} else if (result == 0) {
redisTemplate.opsForValue().set("promo_stock_invalid_" + itemId, "true");
return true;
} else {
increaseStock(itemId, amount);
return false;
}
}
if (redisTemplate.hasKey("promo_stock_invalid_" + itemId)) {
throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
}
4、注意
目前的流程,虽然说mysql库存的问题得到了解决,但是redis 的库存还是会减掉,并且会存在不一致性,这个后面就要讨论,根据运营进行调整