- 截止到目前,下单方案
- 验证下单状态(下单商品是否存在、用户是否合法、购买数量是否正确)
- 验证秒杀活动信息
- 落单减库存(减redis库存和回补redis库存、rocketmq消息异步写入数据库)
- 订单入库
- 更改销量
-
目前存在问题
如果落单减库存操作成功,但是之后的操作中,比如订单入库失败,由于rocketmq消息已经消费成功也不能回滚,则会导致多扣库存的问题. -
改进方案
那么把rocketmq异步发送消息放在事务的最后面呢?
还有可能存在由于内存不足等原因导致事务commit失败的问题,事务回滚,但是消息已经被消费,还是会多扣库存. -
再次改进
那么把rocketmq异步发送消息放在事务提交之后呢?
如果发送消息发送失败,但是事务已经提交不能再回滚,那么会导致库存没有扣减成功,但是订单已经生成.最后导致订单数多于库存数. -
再次改进
- 方案
采用2PC(two-phase commit protocol两段式提交协议)+补偿机制(事务回查)的分布式事务功能,通过消息队列 RocketMQ 版事务消息能达到分布式事务的最终一致。 - 2PC两段式提交协议流程图
- 秒杀环境中的具体实现方案
- 将库存扣减信息和增加销量信息发送半消息(将消息写入到一个特定的topic中,而不是真正的消费端接收的topic中)
- 执行创建订单的事务,创建订单中的扣减库存和增加销量的操作都是在redis中
- 如果创建订单事务操作成功,返回COMMIT_MESSAGE,则投递消息,消费端消费消息
- 如果创建订单事务操作失败,返回ROLLBACK_MESSAGE,事务回滚,消息放弃
- 如果既没有返回COMMIT_MESSAGE也没有返回ROLLBACK_MESSAGE,而是处于UNKNOW状态,则使用checkLocalTransaction()方法进行回查,最多回查15次,间隔1分钟,如果仍然处于UNKNOW状态,则放弃该消息.
- checkLocalTransaction()的回查机制具体是通过消息中的item_id查看库存流水中的状态信息,状态1表示初始状态,返回UNKNOW; 2表示下单扣减库存成功,返回COMMIT_MESSAGE; 3表示下单回滚, 返回ROLLBACK_MESSAGE.
- 库存数据库最终一致性保证
- 方案
- 引入库存操作流水
- 引入rocketmq事务型消息机制
- 存在的问题
- redis不可用时如何处理
- 扣减流水错误如何处理
- 根据业务场景决定高可用技术的实现
- 宁可少卖,不能超卖原则(实在没货,也无法调货,超卖发不了货会导致投诉)
- 方案
- redis可以比实际数据库中少
redis若产生问题,不能回源到数据库,因为这时数据库的状态不一定正常,可能还有数据没有异步写入到数据库. - 超时释放
在rocketmq事务型消息创建订单时,若卡在这,既没有返回COMMIT_MESSAGE也没有返回ROLLBACK_MESSAGE,那么等一段时间后,就需要超时释放,将库存加回去.
- redis可以比实际数据库中少
- 库存售罄
- 当前存在的问题
即使库存售罄,还是可以执行初始化库存流水initStockLog()的操作 - 解决方案
- 设置库存售罄标识,并将该标识达到缓存上
- 售罄后不去操作后续流程,包括初始化流水操作,直接返回前端库存售罄,下单失败
- 售罄后通知各系统售罄
rocketmq异步消息通知的方式实现,各系统清除商品缓存,并回源获取数据库库存. - 回补上新
当商家补库存的时候,要清除售罄标识.
- 核心代码具体实现
-
MqProducer.java消息生产者代码
@PostConstruct public void init() throws MQClientException { transactionMQProducer = new TransactionMQProducer("transaction_producer_group"); transactionMQProducer.setNamesrvAddr(nameAddr); transactionMQProducer.start(); transactionMQProducer.setTransactionListener(new TransactionListener() { @Override // 在发送消息后调用,用于执行本地事务,如果本地事务执行成功,rocketmq再提交消息 public LocalTransactionState executeLocalTransaction(Message message, Object args) { Integer userId = (Integer) ((Map)args).get("userId"); Integer promoId = (Integer) ((Map)args).get("promoId"); Integer itemId = (Integer) ((Map)args).get("itemId"); Integer amount = (Integer) ((Map)args).get("amount"); String stockLogId = (String) ((Map)args).get("stockLogId"); // 真正要做的事:创建订单 try { orderService.createOrder(userId, itemId, promoId, amount, stockLogId); } catch (BusinessException e) { e.printStackTrace(); // 抛出任何异常都要回滚 // 设置对应的stocklog为回滚状态 StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId); stockLogDO.setStatus(3); stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO); return LocalTransactionState.ROLLBACK_MESSAGE; } return LocalTransactionState.COMMIT_MESSAGE; } // 如果消息发送状态是UNKNOW,则调用该方法进行回查,最多回查15次,间隔1分钟 @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 根据是否扣减库存成功,来判断是否返回ROLLBACK_MESSAGE还是COMMIT_MESSAGE,或者是继续UNKNOWN String jsonString = new String(msg.getBody()); Map<String, Object> map = JSON.parseObject(jsonString, Map.class); String stockLogId = (String) map.get("stockLogId"); StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId); if(stockLogDO == null){ return LocalTransactionState.UNKNOW; } // 2表示下单扣减库存成功 if(stockLogDO.getStatus() == 2){ return LocalTransactionState.COMMIT_MESSAGE; // 1表示初始状态,说明卡在orderService.createOrder()那了 }else if(stockLogDO.getStatus() == 1){ return LocalTransactionState.UNKNOW; } return LocalTransactionState.ROLLBACK_MESSAGE; } }); } // 事务型同步库存扣减消息 public Boolean transactionAsyncReduceStockAndAddSales(Integer userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) { // bodyMap为rocketmq要传输的消息 Map<String, Object> bodyMap = new HashMap<>(); bodyMap.put("itemId", itemId); bodyMap.put("amount", amount); bodyMap.put("stockLogId", stockLogId); // argsMap为其他的一些参数 Map<String, Object> argsMap = new HashMap<>(); argsMap.put("userId", userId); argsMap.put("promoId", promoId); argsMap.put("itemId", itemId); argsMap.put("amount", amount); argsMap.put("stockLogId", stockLogId); Message message = new Message(topicName, "increase", JSON.toJSON(bodyMap).toString().getBytes(StandardCharsets.UTF_8)); TransactionSendResult transactionSendResult = null; try { transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, argsMap); } catch (MQClientException e) { e.printStackTrace(); return false; } if(transactionSendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE){ return false; }else if(transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){ return true; } return false; }
-
OrderServiceImpl.java创建订单代码
@Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) throws BusinessException { // 1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确 // ItemModel itemModel = itemService.getItemById(itemId); ItemModel itemModel = itemService.getItemByIdInCache(itemId); if(itemModel == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } // UserModel userModel = userService.getUserById(userId); UserModel userModel = userService.getUserByIdInCache(userId); if(userModel == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户未注册"); } if(amount <= 0 || amount > 99){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不正确"); } // 校验秒杀活动信息 if(promoId != null){ // 校验该商品是否有活动 if(promoId.intValue() != itemModel.getPromoModel().getId()){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确"); // 校验活动是否正在进行中 }else if(itemModel.getPromoModel().getStatus() != 2){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动未开始"); } } // 2.落单减库存 boolean result = itemService.decreaseStock(itemId, amount); if(!result){ throw new BusinessException((EmBusinessError.STOCK_NOT_ENOUGH)); } // 3.订单入库 OrderModel orderModel = new OrderModel(); orderModel.setItemId(itemId); orderModel.setUserId(userId); orderModel.setAmount(amount); if(promoId != null){ orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice()); }else{ orderModel.setItemPrice(itemModel.getPrice()); } orderModel.setPromoId(promoId); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount))); // 生成交易流水号(订单号) orderModel.setId(generateOrderNo()); OrderDO orderDO = convertFromOrderModel(orderModel); orderDOMapper.insertSelective(orderDO); // 4. 商品销量增加,先增加到缓存中,然后通过rocketmq事务消息机制发送消息 itemService.increaseSales(itemId, amount); // 设置库存流水状态为成功 StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId); if(stockLogDO == null){ throw new BusinessException(EmBusinessError.UNKNOWN_ERROR); } // status为2表示扣减库存成功 stockLogDO.setStatus(2); stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO); // 5. 返回前端 return orderModel; }
-
decreaseStock()落单减库存代码
@Override @Transactional public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException { // int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount); // 减缓存中的库存操作,返回剩余库存数量 long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1); if(result > 0){ return true; // 打上库存售罄标识 }else if(result == 0){ redisTemplate.opsForValue().set("promo_item_stock_invalid_" + itemId, "true"); // 返回更新库存成功 return true; }else{ // 更新库存失败,将库存补回去 increaseStock(itemId, amount); return false; } }
-
increaseSales()增加销量代码
@Override @Transactional public void increaseSales(Integer itemId, Integer amount) { redisTemplate.opsForValue().increment("promo_item_sales_" + itemId, amount); }
-
MqConsumer.java消息消费者代码
package com.kenai.mq; import com.alibaba.fastjson.JSON; import com.kenai.dao.ItemDOMapper; import com.kenai.dao.ItemStockDOMapper; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.List; import java.util.Map; @Component public class MqConsumer { private DefaultMQPushConsumer consumer; @Resource private ItemStockDOMapper itemStockDOMapper; @Resource private ItemDOMapper itemDOMapper; @Value("${mq.nameserver.addr}") private String nameAddr; @Value("${mq.topicname}") private String topicName; @PostConstruct public void init() throws MQClientException { consumer = new DefaultMQPushConsumer("stock_consumer_group"); // consumer连接nameserver consumer.setNamesrvAddr(nameAddr); // consumer订阅所有stock topic消息 consumer.subscribe(topicName, "*"); // 当消息推送过来之后的处理方式 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { // 实现库存真正到数据库内扣减的逻辑 Message msg = msgs.get(0); String jsonString = new String(msg.getBody()); Map<String, Object> map = JSON.parseObject(jsonString, Map.class); Integer itemId = (Integer) map.get("itemId"); Integer amount = (Integer) map.get("amount"); itemStockDOMapper.decreaseStock(itemId, amount); itemDOMapper.increaseSales(itemId, amount); // 说明该消息已经被消费 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); } }
-
publishPromo()发布促销活动及初始化redis销量和库存代码
/** * 发布促销活动 * @param promoId */ @Override public void publishpromo(Integer promoId) { // 通过活动id获取活动 PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId); if(promoDO.getItemId() == null || promoDO.getItemId() == 0){ return; } ItemModel itemModel = itemService.getItemById(promoDO.getItemId()); // 将库存同步到redis中 redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock()); // 将销量同步到redis中 redisTemplate.opsForValue().set("promo_item_sales_" + itemModel.getId(), itemModel.getSales()); }