可靠性消息最终一致性
事务发起方在提交本地事务之后,发送一条可靠性消息,到其他事务的参与方,其他事务参与方确保接受到消息并成功处理事务,达到数据最终一致性。方案中强调一旦消息发送,事务的参与方一定要达到最终一致性。
可靠性消息需要解决的问题
-
本地事务与消息发送的原子性
事务发起方在本地事务执行成功之后消息必须发出去,要么都成功,要么都失败。
其中可能存在两种可能性
- 先执行数据库操作,后发送MQ消息:数据库操作成功,但是MQ消息发送失败,导致不一致
- 先发送MQ,后操作数据,MQ消息发送成功,数据库操作失败回滚,导致不一致
-
事务参与方消息接受可靠性
事务参与方必须能从MQ接受到消息,如果消息收到失败可以重复接受消息
-
消息重复消费
网络延迟、重复投递等情况下,导致了消息重复消费
具体实现方案
-
本地消息表
通过本地事务保证数据操作和消息的一致性,然后通过定时任务将消息发送到消息中间件中,待消息中间件确保投递成功之后再从本地消息表中删除消息,事务参与方确认消息并成功处理之后回复消息中间件ACK保证消息不会重复发送
大致流程以用户注册送积分为例
-
用户注册之后往本地消息表中写入一条消息记录,这两条操作在同一个事务具备原子性
-
单起定时任务线程从本地消息记录表中取得消息内容发送给MQ中间件,待收到MQ反馈投递成功之后删除本地消息表记录
-
积分微服务再收到MQ消息的之后进行本地事务处理,处理完成之后将利用消息中间件的ACK机制,当MQ收到消费者的ACK确认之后,MQ将不会再想消费者发送这条消息,由于存在消息可能会存在重复投递的情况,积分服务必须要实现幂等性
-
RocketMQ事务消息方案
RocketMQ提供的事务消息功能主要是为了解决Producer端的消息发送与本地事务的原子性问题,RocketMQ中的broker与producer有双向通信能力,broker可以作为事务协调者存在,RocketMQ提供的存储机制为事务消息提供持久化能力,RocketMQ的高可用和可靠性消息设计为事务消息在系统发生异常之后依然能提供事务最终一致性
RocketMQ提供了事务消息能力,类似是本地消息表的封装,将本地消息表移动到了MQ内部维护,解决producer消息发送与本地事务的原子性问题
流程分析
- 事务发起方Producer发送事务消息到MQ
- MQ接受到消息之后broker中发送Half消息 此时消息对Consumer事务参与方不可见
- MQ回应事务发起方Producer消息发送成功
- 事务发起发Producer接受到MQ的投递成功的标识之后开始执行本地事务
- 完成本地事务之后通知MQ Commit 或者 Rollback,Commit此时Half消息对Consumer可见,此时Consumer事务参与者接受到MQ消息进行本地事务的处理,如果是Rollback MQ删除消息Consumer接受不到消息
- 如果出现超时异常情况,MQ会回查Producer的本地事务,判断是否需要对消息进行投递从而再进行判断消息是Commit还是Rollback
- Consumer接受到消息成功消费之后向MQ回应ACK,此时消息不会再次投递
RocketMQ提供了RocketMQLocalTransactionListener来实现事务消息
public interface RocketMQLocalTransactionListener {
RocketMQLocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
RocketMQLocalTransactionState checkLocalTransaction(final Message msg);
}
代码实践
引入pom
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
事务发起方Producer
-
添加RocketMQ配置
rocketmq: name-server: 192.168.209.100:9876 producer: group: user-producer
-
发送事务消息,消息发送成功之后会回调RocketMQLocalTransactionListener#executeLocalTransaction 执行本地事务
@Autowired private RocketMQTemplate rocketMQTemplate; @Autowired private ObjectMapper objectMapper; public void createWithTransactionMessage(String username) throws JsonProcessingException { User user = new User(); user.setId(ThreadLocalRandom.current().nextLong(1000)); user.setName(username); String userJson = objectMapper.writeValueAsString(user); //destination destination formats: `topicName:tags` Message<String> message = MessageBuilder.withPayload(userJson).build(); TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("txMsg:user", message, null); log.info("send tx message body={},result= {}", message.getPayload(), sendResult.getSendStatus()); }
-
添加事务监听器
@Slf4j @Component @RocketMQTransactionListener public class UserTxMessageListener implements RocketMQLocalTransactionListener { @Autowired private ObjectMapper objectMapper; @Autowired private UserMapper userMapper; @Transactional @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { String userJson = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8); log.info("执行本地事务,消息:{}",userJson ); // 执行本地事务 User user = null; try { user = objectMapper.readValue(userJson, User.class); } catch (JsonProcessingException e) { e.printStackTrace(); } int insert = userMapper.insert(user); return insert > 0 ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.ROLLBACK; } @Transactional @Override public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { String userJson = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8); log.info("回查本地事务,消息:{}", userJson); // 回查本地事务 User user = null; try { user = objectMapper.readValue(userJson, User.class); } catch (JsonProcessingException e) { e.printStackTrace(); } User dbUser = userMapper.selectById(user.getId()); return dbUser != null ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.UNKNOWN; } }
Consumer端
-
添加配置
rocketmq: name-server: 192.168.209.100:9876
-
实现消息监听器
@Slf4j @Component @RocketMQMessageListener(topic = "txMsg", consumerGroup = "pointGroup", selectorExpression = "user", selectorType = SelectorType.TAG) public class UserMQListener implements RocketMQListener<String> { @Autowired private ObjectMapper objectMapper; @Autowired private PointMapper pointMapper; @SneakyThrows @Override public void onMessage(String message) { log.info("收到MQ消息:{}", message); Map userMap = objectMapper.readValue(message, Map.class); Point point = new Point(); point.setPoint(5); point.setUserId(Long.valueOf(userMap.get("id").toString())); pointMapper.insert(point); } }
总结
可靠消息最终一致性保证了消息从生产方到消息中间件到消费者的一致性,解决了如下问题
- 本地事务与消息发送原子性
- 事务参与方接受消息的可靠性
适用执行周期较长且实时性要求不高的场景,将同步的事务操作变成基于消息的执行异步操作,避免分布式事务中同步阻塞的影响,实现了服务解耦