先问一个问题:RocketMQ是如何保证消息与数据库事务的一致性?
第一时间可能会想到RocketMQ的事务消息
我们以日常开发中的案例来进行分析:下单送积分。用户在下单后,订单系统保存订单数据,然后发送消息到MQ,积分系统订阅这个消息,然后给用户加积分。这就引出了一个问题,从生产者订单系统角度看,到底是先写库还是先发消息 呢?那我们接下来就分别看下这两种情况。
1. 先写库后发消息
我们先通过一段伪代码来分析下:
public void createOrder(final Order order) throws Exception {
//模拟spirng的tx模板
transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
//本地数据插入
orderMapper.save(order);
orderDetailMapper.save(order.getOrderDetail());
//模拟 mq 发送消息
SendResult send = producer.send(orderMessage);;
if (send.getSendStatus() == SendStatus.SEND_OK) {
status.setRollbackOnly();
}
return Boolean.TRUE;
}
});
}
我们来分析下它的过程:
首先,执行本地数据库事务,插入数据,注意此时还没有commit, 紧接着发送消息到MQ, 这中间可能由于网络波动等原因,导致生产者迟迟没有收到broker的响应结果,比如5s内都没有返回SendResult给生产者,这也就意味着这5s内本地数据库事务是无法commit的,如果在高并发的场景下,数据库连接资源很快就会被耗尽,后续的请求则无法处理,最终系统将会崩溃。
既然我们知道了先写库后发消息有这样的问题,那么如果是先发消息后写库呢?
2.先发消息后写库
我们还是先看下代码:
public void createOrder(Order order) {
try {
//先发送消息
SendResult send = producer.send(orderMessage);
if (send.getSendStatus() == SendStatus.SEND_OK) {
orderMapper.save(order);
orderDetailMapper.save(order.getOrderDetail());
//提交事务
connection.commit();
}
} catch (Exception e) {
//回滚
connection.rollback();
}
}
这样也是有问题的:
- 首先他也存在
先写库后发消息
的问题,一旦MQ由于网络等原因长时间没有返回SendResult给生产者,将会导致本地事务无法被提交或回滚,高并发下资源将会被快速耗尽。 - 其次,生产者将消息发送出去并快速响应了,但是执行本地数据库事务时出现了错误,比如上述代码中的
orderMapper.save(order)
执行出错了,这也就意味着消息已经发送出去,消费者可以消费了,但是此时本地事务失败了,为了弥补错误,此时可能需要“回滚
”之前发送的消息,但是此时这条消息可能已经被消费了,就算没有被消费,每次我都在发送消息后判断是否出现了异常,如果出现了异常在发送条"回滚
"的消息,这无疑是增加了开发的复杂度,也显得冗余。
那么有没有什么更好的方式,既可以不阻塞本地数据库事务,还能保证最终一致性呢?
这就是接下来我们要说的RocketMQ的事务消息,它可以保证本地事务与MQ消息的最终一致性。
事务消息我们之前有分析过它的源码和流程,这里我们简单看下
知道了事务消息的大致流程后,接下来我们还是通过伪代码来看下它的实现过程。
3.事务消息
- 发送事务消息
发送的topic是 “tx_order_topic”,消费者订阅的也是这个,但是在发送到broker时,他会在内部将我们的topic做一次修改,这样对消费者就不可见了。
@Slf4j
@Controller
public class OrderCreateController {
//rocketmq 发送消息的模板
@Autowired
private RocketMQTemplate rocketMQTemplate;
@ResponseBody
@GetMapping("/order/{buyer}")
public String createOrder(@PathVariable String buyer) {
//@Accessors(chain = true)
OrderDetail orderDetail = new OrderDetail();
orderDetail.setPhone("18883858508").setAddress("上海外滩xxxxx").setOrderDetailId(UUID.randomUUID().toString());
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString()).setBuyer(buyer).setOrderDetail(orderDetail);
Message<Order> message = MessageBuilder.withPayload(order).build();
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction("tx_order_topic", message, null);
if (SendStatus.SEND_OK == result.getSendStatus()) {
log.info("发送消息成功, result: {}", result);
}
//回查订单表
return "order create success";
}
}
rocketMQTemplate.sendMessageInTransaction(...)要等本地事务执行完毕,才会返回 TransactionSendResult
- 执行本地事务
@Slf4j
@RocketMQTransactionListener
public class CreateOrderCheckerListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info("message: {}, args: {}", msg, arg);
String orderMsg = new String((byte[]) msg.getPayload());
final Order order = JSON.parseObject(orderMsg, Order.class);
log.info("order info : {}", order);
try {
//放到同一个本地事务中
this.transactionTemplate.executeWithoutResult(status -> {
this.orderMapper.saveOrder(order);
// int x = 1 / 0;
this.orderDetailMapper.saveOrderDetail(order.getOrderDetail());
});
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("保存订单失败", e);
//触发回查
return RocketMQLocalTransactionState.UNKNOWN;
//如果是ROLLBACK,则回滚消息,rocketmq将废弃这条消息
}
}
//先忽略回查的逻辑
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {}
}
如果本地事务执行成功(订单正常入库),producer将给Broker发送一个COMMIT
的标识,此时broker会将之前被替换了的topic给替换回去,这样消费者就可以消费了。
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "qiuguan_test_consumer_group", topic = "tx_order_topic")
public class RewardsPoints implements RocketMQListener<Order> {
@Override
public void onMessage(Order message) {
log.info("积分系统根据订单增加积分 : {}", message);
}
}
如果本地执行过程中发生了异常,比如网络抖动等,没有正常入库,此时给Broker发送一个UNKNOW的标识,broker收到UNKNOW
标识后,默认按照每分钟一次的频率发起回查。
- 消息回查
@Slf4j
@RocketMQTransactionListener
public class CreateOrderCheckerListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private TransactionTemplate transactionTemplate;
//执行本地事务逻辑
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {}
//回查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
log.info("执行本地事务回查:{}", LocalDateTime.now());
String orderMsg = new String((byte[]) msg.getPayload());
final Order order = JSON.parseObject(orderMsg, Order.class);
log.info("回查order: {}", order);
//回查次数
//int checkTimes = msg.getHeaders().get("TRANSACTION_CHECK_TIMES", Integer.class);
Order o = this.orderMapper.getOrder(order.getOrderId());
if (o == null) {
try {
this.transactionTemplate.executeWithoutResult(status -> {
this.orderMapper.saveOrder(order);
this.orderDetailMapper.saveOrderDetail(order.getOrderDetail());
});
} catch (Exception e) {
log.error("保存订单失败", e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
return RocketMQLocalTransactionState.COMMIT;
}
}
在回查的时候我们可以检查数据库是否插入了订单,如果没有,此时我们可以再次尝试入库,如果入库成功,则响应给Broker一个COMMIT
标识,此时该消息就可以被消费者消费了,如果依然入库失败,可以等待再次回查,或者回滚。如果是回滚,则Broker将丢弃该消费,消费者也将无法消费。
接下来我们分析下使用RocketMQ的事务消息有哪些问题:
- 生产者发送事务消息失败
这种情况就直接抛出异常即可,本地事务也不会执行,更不会存在数据不一致的问题。
- 生产者发送消息成功,但是本地事务执行失败
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info("message: {}, args: {}", msg, arg);
try {
this.transactionTemplate.executeWithoutResult(status -> {
this.orderMapper.saveOrder(order);
int x = 1 / 0;
this.orderDetailMapper.saveOrderDetail(order.getOrderDetail());
});
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("保存订单失败", e);
//回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
一旦本地事务执行失败,则数据库将会回滚,同时给broker发送ROLLBACK标识,broker收到该标识后,将废弃掉这条消息,消费者也无法消费这条消息,这样也不会出现数据不一致的问题。
- 生产者发送消息成功,本地事务也执行成功,但是在生产者将
COMMIT
标识发送给broker时,发生了网络抖动,没有及时收到COMMIT
指令。
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info("message: {}, args: {}", msg, arg);
try {
this.transactionTemplate.executeWithoutResult(status -> {
this.orderMapper.saveOrder(order);
this.orderDetailMapper.saveOrderDetail(order.getOrderDetail());
});
//网络抖动...
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("保存订单失败", e);
//回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
本地数据库事务执行成功,订单数据保存到表中,broker由于网络抖动没有及时收到
COMMIT
指令,此时消息还是一条半事务消息
,消费者还是无法消费,这样本地事务与RocketMQ消息的一致性就被破坏了。
RocketMQ为了解决这个问题,引入了消息回查机制,对于半事务消息,如果没有及时收到COMMIT/ROLLBACK
指令,它会尝试主动与broker进行通信,调用监听器的 checkLocalTransaction(..)
方法再次确认之前的本地事务是否成功。
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
log.info("执行本地事务回查:{}", LocalDateTime.now());
final Order order = JSON.parseObject(new String((byte[]) msg.getPayload()), Order.class);
log.info("回查order: {}", order);
/**
* 由于之前本地事务已经执行成功,数据插入了表中,只是在给broker发送COMMIT标识时发生了网络闪断
* 所以这里回查的时候,是可以从数据库表中查询到订单数据的,此时就可以给broker发送一个COMMIT标识
* 这样broker就会把这对消费者不可见的消息修改为可见,此时就可以消费了。
*/
Order o = this.orderMapper.getOrder(order.getOrderId());
/**
* 如果数据库中没有订单数据,说明之前的插入就是失败的,此时这里尝试再次插入或者直接回滚就可以了
*/
return o == null ? RocketMQLocalTransactionState.ROLLBACK : RocketMQLocalTransactionState.COMMIT;
}
不难发现,使用RocketMQ的事务消息具有以下好处:
将发送消息和本地事务分离开,如果发送消息失败,则整个流程失败,不会阻塞本地事务,如果本地事务执行失败,则可以直接回滚或者回查,不会影响消费者。
好了,关于RocketMQ的事务消息的实战就介绍到这里,欢迎大家批评指正。