RocketMQ事务消息实现支付红包功能
一、事务消息介绍
RocketMQ的事务消息,是指发送消息事件和其他事件需要同时成功或同时失败。比如支付红包功能,订单支付成功,更新订单状态为“已完成”,发送"订单支付成功"这个消息给红包系统,红包系统接收到消息,给用户派发红包。这个操作要么同时成功,要么同时失败。
RocketMQ采用两阶段提交的方式实现事务消息,TransactionMQProducer处理上面情况的流程如下:
(1)发送方向RocketMQ发送“待确认”消息。
(2)RocketMQ将收到的“待确认”消息持久化成功后,向发送方回复消息已经发送成功,此时第一阶
段消息发送完成。
(3)发送方开始执行本地事件逻辑。
(4)发送方根据本地事件执行结果向RocketMQ发送二次确认(Commit或是Rollback)消息,RocketMQ收到Commit状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;收到Rollback状态则删除第一阶段的消息,订阅方接收不到该消息。
(5)如果出现异常情况,步骤(4)提交的二次确认最终未到达RocketMQ,服务器在经过固定时间段后将对“待确认”消息发起回查请求。
(6)发送方收到消息回查请求后(如果发送一阶段消息的Producer不能工作,回查请求将被发送到和Producer在同一个Group里的其他Producer),通过检查对应消息的本地事件执行结果返回Commit或Roolback状态。
(7)RocketMQ收到回查请求后,按照步骤(4)的逻辑处理。
二、事务消息流程概要
上面介绍了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1)发送half消息。
(2)服务端响应消息写入结果。
(3)根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地事务逻辑不执行)。
(4)根据本地事务状态执行Commit或者Rollback(Commit之后,会从half topic里进入到用户指定的topic中,这条消息就对消费者可见了)
2.补偿流程:
(1)对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2)Producer收到回查消息,检查回查消息对应的本地事务的状态
(3)根据本地事务状态,重新Commit或者Rollback
三、代码落地实现
OrderServiceImpl
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
public void pay(Order order) {
String orderRequest = JSON.toJSONString(order);
Message message = MessageBuilder.withPayload(orderRequest).build();
try {
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"myTransactionGroup",
"orderPaySuccessTopic",
message,
null);
System.out.println(sendResult);
} catch (MessagingException e) {
e.printStackTrace();
// 发生异常,状态变更为“已退款”
this.updateOrderState(order.getId(), OrderEnums.REFUND.getCode());
}
}
}
TransactionListenerImpl
@RocketMQTransactionListener(txProducerGroup = "myTransactionGroup")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
private ConcurrentHashMap<String, RocketMQLocalTransactionState> localTrans = new ConcurrentHashMap<>();
private Logger logger = LoggerFactory.getLogger(TransactionListenerImpl.class);
@Autowired
private OrderService orderService;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("half消息确认。。。");
String orderStr = new String((byte[]) message.getPayload());
Order order = JSON.parseObject(orderStr, Order.class);
String transId = (String) message.getHeaders().get("rocketmq_" + RocketMQHeaders.TRANSACTION_ID);
// 更新订单状态
int count = orderService.updateOrderState(order.getId(), OrderEnums.PAIED.getCode());
try {
if (count > 0) {
localTrans.put(transId, RocketMQLocalTransactionState.COMMIT);
}
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
logger.error("发生异常,异常信息:{}", e.getMessage());
// 发生异常,回滚事务
orderService.updateOrderState(order.getId(), OrderEnums.CANCEL.getCode());
localTrans.put(transId, RocketMQLocalTransactionState.ROLLBACK);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
System.out.println("事务消息回查。。。");
String transId = (String) message.getHeaders().get("rocketmq_" + RocketMQHeaders.TRANSACTION_ID);
System.out.println("transId:" + transId);
return localTrans.get(transId);
}
}
RedPacketListener
@Component
@RocketMQMessageListener(topic = "orderPaySuccessTopic", consumerGroup = "redPacketGroup")
public class RedPacketListener implements RocketMQListener<String> {
@Autowired
private RedPacketService redPacketService;
@Override
public void onMessage(String msg) {
System.out.println("接收到订单系统发来的消息。。。");
Order order = JSON.parseObject(msg, Order.class);
RedPacket redPacket = new RedPacket();
redPacket.setUserId(order.getUserId());
redPacket.setOrderId(order.getId());
redPacket.setTotalFee(order.getTotalFee());
redPacket.setGmtCreate(new Date());
redPacket.setGmtModified(new Date());
redPacketService.sellRedPacket(redPacket);
}
}
具体代码下载地址:https://gitee.com/guo974691001/rocketmq-tx
思考:使用事务消息机制,消息会丢失吗?
现在我们来思考,如果我们生产消息的时候,用了事务消息之后,可以保证数据不会丢失吗?
答案肯定是会丢失的,假设订单系统通过事务消息的机制,通过half消息+commit的方式,把消息提交到MQ了,对于MQ而言,这条消息已经进入他的存储层了,可以被红包系统看到了。
看下图的示意
这条消息在commit之后,会从half topic进入ORDER_FINISH_TOPIC中,但是此时仅仅是消息进入你预定的Topic中而已,仅仅是可以被红包系统看到,可能红包系统还没来得及去获取这条消息。
然而恰巧在此时,这条消息还没来得及刷到ConsumeQueue磁盘文件里去,此时红包系统突然宕机了,此时必然会导致消息丢失,红包系统再没机会读到这条消息了。
思考:消息进入磁盘了,消息还会丢失吗?
即使消息已经进入磁盘文件了,但是这个时候红包系统还没来得及消费这条消息,然后此时这台机器的磁盘突然坏了,还是会导致消息丢失。
思考:如何保证消息写入MQ之后,数据不会丢失?
说到这里,我们就不得不说一下RocketMQ的同步刷盘和异步刷盘了。
同步刷盘: 如上图所示,只有在消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。
异步刷盘:能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。
解决这一问题的关键点,就是将异步刷盘调整为同步刷盘。在异步刷盘的模式下,写入消息的吞吐量是极高的,但是这种情况下,就可能会导致数据的丢失。
所以如果一定要确保数据零丢失的话,可以将异步刷盘模式调整为同步刷盘,需要调整broker的配置文件,将其中的flushDiskType配置为SYNC_FLUSH,默认值是ASYNC_FLUSH。
思考:如何避免磁盘故障导致的丢失?
对于这个问题,我们就不得不说一下RocketMQ的分布式集群模式了。
RocketMQ的分布式集群模式
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
对于RocketMQ的集群模式有两种:多Master多Slave模式-异步复制和多Master多Slave模式-同步同步双写,具体选择可根据你的业务需要来进行选择。
我们必须要对Broker使用主从架构模式,必须让一个Master Broker有一个Slave Broker去同步数据,一条消息写入成功,必须让Slave Broker也写入成功,保证数据有多个副本的冗余。这样一来,主从两个Broker都有这条数据,如果你的Master Broker的磁盘坏了,但是Slave Broker上至少还是有数据的,数据是不会因为磁盘故障而丢失的。
总结:确保MQ数据零丢失的方案
通过本次分析,我们知道了,只需要把Broker的刷盘策略调整为同步刷盘,那么绝对不会因为机器宕机而导致数据丢失。只要采用主从架构的Broker集群,那么一条消息写入成功,就意味着多个Broker机器都写入了,此时任何一台机器的磁盘故障,数据也不会丢失。只要Broker保证写入的数据不丢失,那就一定可以让红包系统消费到这条消息。

浙公网安备 33010602011771号