16 秒杀系统 | 交易性能优化 | 库存缓存化(三)RocketMQ 事务型消息让 MySQL 同步 Redis 中的库存...

现存代码问题分析

  • decreaseStock 方法被 @Transactional 标注,并且调用 decreaseStock 的方法 createOrder 也被 @Transactional 标注,根据 Spring 的事务传播机制,默认 decreaseStock 会沿用 createOrder 的事务,也就是说和 createOrder 的事务同时成功或同时失败;
  • 原先 decreaseStock 代码是 MySQL 操作,意味着,如果 decreaseStock 之后的大事务中的代码报错,decreaseStock 中对 MySQL 的更改是可以回滚的;但是现在改用 Redis 和 MQ 之后,如果之后的大事务失败(比如订单入库失败、销量增加失败),对 Redis 的更改以及对 MQ 发出的消息和造成的 MySQL 的更改是无法恢复的,返回给用户的下单失败,但是库存就损失掉了,虽然不会造成超卖,但是会造成少卖,库存莫名其妙的少了,但是又找不到对应的订单,导致货物积压;
  • 问题的本质其实是分布式事务的问题,RocketMQ 是提供了事务型消息的支持的;

    
    
  1. @Override
  2. @Transactional
  3. public boolean decreaseStock( Integer itemId, Integer amount) {
  4. // int affectedRows = itemStockDOMapper.decreaseStock(itemId, amount);
  5. long result = redisTemplate. opsForValue(). increment( "promo_item_stock_" + itemId, amount * - 1);
  6. if (result >= 0) {
  7. boolean mqResult = mqProducer. asyncReduceStock(itemId, amount);
  8. if (!mqResult) {
  9. redisTemplate. opsForValue(). increment( "promo_item_stock_" + itemId, amount);
  10. return false;
  11. }
  12. return true;
  13. } else {
  14. redisTemplate. opsForValue(). increment( "promo_item_stock_" + itemId, amount);
  15. return false;
  16. }
  17. }
小知识:Spring 提供le在事务 Commit 成功之后再做点事情的能力
  • 如果 afterCommit 方法执行失败,那么事务中已经提交成功的数据是不能回滚的;

     
     
  1. @Transactional
  2. public void createOrder( ) {
  3. // some operation in transaction
  4. TransactionSynchronizationManager. registerSynchronization( new TransactionSynchronizationAdapter() {
  5. // 这个方法会在最近的一个 @Transactional 标签被成功 Commit 之后执行
  6. @Override
  7. public void afterCommit( ) {
  8. super. afterCommit();
  9. }
  10. });
  11. }

RocketMQ 的事务型消息

在 Producer 的封装中,增加发送事务型消息的方法
  • 事务型消息的发送逻辑为:
    1. 先发送 Prepared 状态的消息到 Broker 中;
    2. 再执行本地事务(下单),本地事务的执行在回调方法 executeLocalTransaction 中;
    3. 根据本地事务(下单)的成功与否决定提交 Broker 中的消息还是撤回;

    
    
  1. package com. lixinlei. miaosha. mq;
  2. import com. alibaba. fastjson. JSON;
  3. import com. lixinlei. miaosha. error. BusinessException;
  4. import com. lixinlei. miaosha. service. OrderService;
  5. import com. lixinlei. miaosha. service. model. OrderModel;
  6. import org. apache. rocketmq. client. exception. MQBrokerException;
  7. import org. apache. rocketmq. client. exception. MQClientException;
  8. import org. apache. rocketmq. client. producer.*;
  9. import org. apache. rocketmq. common. message. Message;
  10. import org. apache. rocketmq. common. message. MessageExt;
  11. import org. apache. rocketmq. remoting. exception. RemotingException;
  12. import org. springframework. beans. factory. annotation. Autowired;
  13. import org. springframework. beans. factory. annotation. Value;
  14. import org. springframework. stereotype. Component;
  15. import javax. annotation. PostConstruct;
  16. import java. nio. charset. Charset;
  17. import java. util. HashMap;
  18. import java. util. Map;
  19. @Component
  20. public class MqProducer {
  21. private DefaultMQProducer producer;
  22. private TransactionMQProducer transactionMQProducer;
  23. @Value( "${mq.nameserver.addr}")
  24. private String nameAddr;
  25. @Value( "${mq.topicname}")
  26. private String topicName;
  27. @Autowired
  28. private OrderService orderService;
  29. /**
  30. * 在 Bean 初始化完成之后调用
  31. */
  32. @PostConstruct
  33. public void init() throws MQClientException {
  34. producer = new DefaultMQProducer( "producer_group");
  35. producer. setNamesrvAddr(nameAddr);
  36. producer. start();
  37. transactionMQProducer = new TransactionMQProducer( "transaction_producer_group");
  38. transactionMQProducer. setNamesrvAddr(nameAddr);
  39. transactionMQProducer. start();
  40. transactionMQProducer. setTransactionListener( new TransactionListener() {
  41. /**
  42. * 消息以 Prepared 状态被保存进 Broker 后执行
  43. * @param message
  44. * @param args `transactionMQProducer.sendMessageInTransaction(message, argsMap)` 中传入的 `argsMap`
  45. * @return
  46. */
  47. @Override
  48. public LocalTransactionState executeLocalTransaction( Message message, Object args) {
  49. // 真正要执行的操作:创建订单
  50. Integer userId = ( Integer)(( Map) args). get( "userId");
  51. Integer itemId = ( Integer)(( Map) args). get( "itemId");
  52. Integer promoId = ( Integer)(( Map) args). get( "promoId");
  53. Integer amount = ( Integer)(( Map) args). get( "amount");
  54. try {
  55. OrderModel orderModel = orderService. createOrder(userId, itemId, promoId, amount);
  56. } catch ( BusinessException e) {
  57. e. printStackTrace();
  58. return LocalTransactionState. ROLLBACK_MESSAGE;
  59. }
  60. return LocalTransactionState. COMMIT_MESSAGE;
  61. }
  62. /**
  63. * 如果 `OrderModel orderModel = orderService.createOrder(userId, itemId, promoId, amount)` 执行成功了,但是
  64. * Tomcat 和 MySQL 中的连接断了,既走不到 ROLLBACK_MESSAGE,也走不到 COMMIT_MESSAGE,那么这个事务型消息的状态就是
  65. * UNKNOW,在 UNKNOW 的情况下,Broker 会定期回调本方法;
  66. * @param msg
  67. * @return
  68. */
  69. @Override
  70. public LocalTransactionState checkLocalTransaction( MessageExt msg) {
  71. return null;
  72. }
  73. });
  74. }
  75. /**
  76. * 事务型让 MySQL 同步 Redis 中库存扣减的消息
  77. * @param itemId
  78. * @param amount
  79. * @return
  80. */
  81. public boolean transactionAsyncReduceStock( Integer userId, Integer promoId, Integer itemId, Integer amount) {
  82. Map< String, Object> bodyMap = new HashMap<>();
  83. bodyMap. put( "itemId", itemId);
  84. bodyMap. put( "amount", amount);
  85. Map< String, Object> argsMap = new HashMap<>();
  86. argsMap. put( "itemId", itemId);
  87. argsMap. put( "amount", amount);
  88. argsMap. put( "userId", userId);
  89. argsMap. put( "promoId", promoId);
  90. Message message = new Message(
  91. topicName,
  92. "increase",
  93. JSON. toJSON(bodyMap). toString(). getBytes( Charset. forName( "UTF-8")));
  94. TransactionSendResult sendResult = null;
  95. try {
  96. /**
  97. * 事务型消息有一个二阶段提交的概念:
  98. * 消息发出后,Broker 的确可以收到消息,但是状态是不可被消费的状态,而是 Prepared 状态;
  99. * 消息发出后,会回调 Producer 端的 executeLocalTransaction 方法;
  100. */
  101. sendResult = transactionMQProducer. sendMessageInTransaction(message, argsMap);
  102. } catch ( MQClientException e) {
  103. e. printStackTrace();
  104. return false;
  105. }
  106. if (sendResult. getLocalTransactionState() == LocalTransactionState. ROLLBACK_MESSAGE) {
  107. return false;
  108. } else if (sendResult. getLocalTransactionState() == LocalTransactionState. COMMIT_MESSAGE) {
  109. return true;
  110. } else {
  111. return false;
  112. }
  113. }
  114. }
不再是 Controller 直接调用下单的 Service
  • Controller 直接发送事务型消息给 Broker,下单操作作为本地事务在回调中执行;

    
    
  1. @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
  2. @ResponseBody
  3. public CommonReturnType createOrder( @RequestParam(name = "itemId") Integer itemId,
  4. @RequestParam(name = "amount") Integer amount,
  5. @RequestParam(name = "promoId", required = false) Integer promoId) throws BusinessException {
  6. String token = httpServletRequest.getParameterMap(). get( "token")[ 0];
  7. if (StringUtils.isEmpty(token)) {
  8. throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
  9. }
  10. UserModel userModel = (UserModel) redisTemplate.opsForValue(). get(token);
  11. if (userModel == null) {
  12. throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
  13. }
  14. // OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
  15. if(!mqProducer.transactionAsyncReduceStock(userModel.getId(), promoId, itemId, amount)) {
  16. throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败");
  17. }
  18. return CommonReturnType.create( null);
  19. }
减库存的操作中不再给 MQ 发消息
  • 给 Broker 发扣减库存的消息不再跟在 Redis 操作(减 Redis 中的库存)之后,而是作为事务型消息,由 Controller 发送;

    
    
  1. @Override
  2. @Transactional
  3. public boolean decreaseStock( Integer itemId, Integer amount) {
  4. // int affectedRows = itemStockDOMapper.decreaseStock(itemId, amount);
  5. long result = redisTemplate. opsForValue(). increment( "promo_item_stock_" + itemId, amount * - 1);
  6. if (result >= 0) {
  7. // boolean mqResult = mqProducer.asyncReduceStock(itemId, amount);
  8. // if (!mqResult) {
  9. // redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount);
  10. // return false;
  11. // }
  12. return true;
  13. } else {
  14. redisTemplate. opsForValue(). increment( "promo_item_stock_" + itemId, amount);
  15. return false;
  16. }
  17. }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值