上一部分使用异步消息的方式将Redis库存和对应的数据库内的库存达到了最终的一致性
落单减库存对应的decreaseStock操作做了改造,改造结果为从Redis中扣,扣完之后发送异步消息给库存的一个异步回调,然后在异步回调内扣减对应的数据库。若异步消息发生任何异常,把对应的Redis库存加了回来,或者Redis库存更新失败,也将对应的Redis库存加了回来
问题是:decreaseStock依赖于外部的一个事务,如果说外部已经有了事务,@Transactional标签就会沿用外部事务,也就是和外部事务同时成功同时失败。但是因为整个下单操作是属于同一个 transaction事务的,如果用户下单成功,但是之后订单入库或返回前端的过程中发生错误了,事务回滚,会导致少卖的现象,有可能造成库存堆积。
少卖也就是事务 (@Transactional
) 回滚只针对mysql数据库,而对redis不起作用,就会出现redis减库存了,而mysql数据库事务回滚后,库存并没有减少。
解决方法就是异步消息的发送要在整个事务完全成功后再Redis扣减,然后发送。
一.事务型消息
1.在ItemService.java中添加方法
//库存回滚
boolean increaseStock(Integer itemId, Integer amount) throws BusinessException;
//异步更新库存
boolean asyncDecreaseStock(Integer itemId, Integer amount);
2.ItemServiceImpl.java
@Override
@Transactional
public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
//将stock库存表中对应的商品id的对应库存数量减去购买数量
//increment(-3)等于减去3,返回的是计算后的值
long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue() * -1);
if (result >= 0) {
//更新库存成功,发送消息,减数据库的库存
//更新库存成功直接true
return true;
} else {
// 更新库存失败,需要回滚redis库存,将库存重新补回去
increaseStock(itemId, amount);
return false;
}
}
// redis库存回补
@Override
public boolean increaseStock(Integer itemId, Integer amount) throws BusinessException {
redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue());
return true;
}
// 异步更新库存,使用rocketmq实现
@Override
public boolean asyncDecreaseStock(Integer itemId, Integer amount) {
//更新库存成功,发送消息,减数据库的库存
boolean mqResult = mqProducer.asyncReduceStock(itemId, amount);
return mqResult;
}
3.OrderServiceImpl.java
//加上商品销量
itemService.increaseSales(itemId,amount);
//异步更新库存
boolean mqResult =itemService.asyncDecreaseStock(itemId,amount);
if(!mqResult){
itemService.increaseStock(itemId,amount);
throw new BusinessException(EmBusinessError.MQ_SEND_FAIL);
}
//4.返回前端
return orderModel;
4.OrderController.java
@Autowired
private MqProducer mqProducer;
if (userModel == null) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
}
// OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId, promoId, amount);
// 创建订单,开启异步发送事务型消息的操作
if (!mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId, promoId, amount)) {
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败");
}
return CommonReturnType.create(null);
}
5.MqProducer.java
private OrderService orderService;
@PostConstruct
public void init() throws MQClientException {
// 做mq producer的初始化
producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr(nameAddr);
producer.start();
transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.start();
transactionMQProducer.setTransactionListener(new TransactionListener() {
// 发送事务型消息,消息的类型是prepare,不会被consumer立即执行
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
// 真正要做的事,创建订单
Integer userId = (Integer) ((Map) arg).get("userId");
Integer itemId = (Integer) ((Map) arg).get("itemId");
Integer promoId = (Integer) ((Map) arg).get("promoId");
Integer amount = (Integer) ((Map) arg).get("amount");
try {
// 这里进行订单的创建
orderService.createOrder(userId, itemId, promoId, amount);
} catch (BusinessException e) {
e.printStackTrace();
// 如果订单创建失败则事务回滚
// ROLLBACK_MESSAGE表示将之前的prepare消息撤回,相当于没发送消息
return LocalTransactio