- 如何处理第二阶段的失败消息?
在本地事务执行完成后会向MQServer发送Commit或Rollback操作,此时如果在发送消息的时候生产者出故障了,那么要保证这条消息最终被消费,MQServer会像服务端发送回查请求,确认本地事务的执行状态。
当然了rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,RocketMQ默认回滚该消息。
- 消息状态
事务消息有三种状态:
TransactionStatus.CommitTransaction:提交事务消息,消费者可以消费此消息
TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
TransactionStatus.Unknown :中间状态,它代表需要检查消息队列来确定状态。
业务需求:用户请求订单微服务 order-service
接口删除订单(退货),删除订单时需要调用 account-service
的方法给账户增加余额,一个典型的分布式事务问题。
基础配置
在开始代码之前首先需要搭建好的RocketMQ环境,如果有不会的可以参考下面这篇文章:
http://javadaily.cn/articles/2020/04/07/1586248405351.html 非常详细
- 在Order-Service和Account-Service中引入Rocket消息组件
org.apache.rocketmq
rocketmq-spring-boot-starter
- 在配置中心添加RocketMQ的相关配置
rocketmq:
name-server: xxx.xx.x.xx:9876
producer:
group: cloud-group
- 在OrderService服务中建立一张事务日志表rocketmq_transaction_log(作用稍后说)
发送半消息
Order-Service作为分布式事务开始的入口,在Service层我们给RocketMQ发送一条半消息
- OrderController入口
/**
-
根据订单号删除订单
-
@param orderNo 订单编号
*/
@PostMapping(“/order/delete”)
public ResultData delete(@RequestParam String orderNo){
log.info(“delete order id is {}”,orderNo);
orderService.delete(orderNo);
return ResultData.success(“订单删除成功”);
}
直接调用orderService的delete方法
- OrderServiceImpl业务逻辑
@Override
public void delete(String orderNo) {
Order order = orderMapper.selectByNo(orderNo);
//如果订单存在且状态为有效,进行业务处理
if (order != null && CloudConstant.VALID_STATUS.equals(order.getStatus())) {
String transactionId = UUID.randomUUID().toString();
//如果可以删除订单则发送消息给rocketmq,让用户中心消费消息
rocketMQTemplate.sendMessageInTransaction(“add-amount”,
MessageBuilder.withPayload(
UserAddMoneyDTO.builder()
.userCode(order.getAccountCode())
.amount(order.getAmount())
.build()
)
.setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
.setHeader(“order_id”,order.getId())
.build()
,order
);
}
}
首先校验一下订单状态,然后使用rocketMQTemplate.sendMessageInTransaction()
发送事务消息。
sendMessageInTransaction方法有三个参数:
-
destination:目的地(主题),这里发送给
add-amount
这个topic -
message:发送给消费者的消息体,需要使用
MessageBuilder.withPayload()
来构建消息 -
arg:参数
注意,这里我们生成了一个transactionId,并放在header中跟消息一起发送(这里实际也可以构造成一个对象,放在arg里进行发送),作用后面再讲!
- 消息封装实体UserAddMoneyDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserAddMoneyDTO {
/**
- 用户编码
*/
private String userCode;
/**
- 金额
*/
private BigDecimal amount;
}
这个类生产这和消费者都需要用到,所以我直接丢到common包中,大家根据项目实际情况决定放哪。
执行本地事务与回查
MQServer收到半消息后会告诉生产者order-service确认收到半消息,这时候order-service需要执行本地事务,执行完本地事务后再告诉MQServer本地事务的执行状态,确认此消息究竟是Commit还是Rollback。
RocketMQ提供了 RocketMQLocalTransactionListener
接口,本地事务监听器,这个接口类的实现如下:
第一个方法executeLocalTransaction
为执行本地事务;
第二个方法checkLocalTransaction
为检查本地事务的执行状态,也就是回查动作。
我们需要实现RocketMQLocalTransactionListener
接口,在executeLocalTransaction
方法中执行本地事务,在执行checkLocalTransaction
回查方法时告诉RocketMQ到底该提交还是回滚。
这里大家思考一个问题,本地事务已经执行完成了,怎么去回查本地事务的执行结果呢?
答案如下:我们可以在执行本地事务的时候同时生成一条事务日志,让本地事务与日志事务在同一个方法中,同时添加
@Transactional
注解,保证两个操作事务是一个原子操作。这样如果事务日志表中有这个本地事务的信息,那就代表本地事务执行成功,需要Commit,相反如果没有对应的事务日志,则表示执行失败,需要Rollback。这就是为什么我们上面在OrderService中需要建立一张事务日志表的原因。
- 实现
RocketMQLocalTransactionListener
接口,完成事务执行逻辑
/**
-
监听事务消息
-
@author javadaily
*/
@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddUserAmountListener implements RocketMQLocalTransactionListener {
private final OrderService orderService;
private final RocketMqTransactionLogMapper rocketMqTransactionLogMapper;
/**
- 执行本地事务
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
log.info(“执行本地事务”);
MessageHeaders headers = message.getHeaders();
//获取事务ID
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
Integer orderId = Integer.valueOf((String)headers.get(“order_id”));
log.info(“transactionId is {}, orderId is {}”,transactionId,orderId);
try{
//执行本地事务,并记录日志
orderService.changeStatuswithRocketMqLog(orderId, CloudConstant.INVALID_STATUS,transactionId);
//执行成功,可以提交事务
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
- 本地事务的检查,检查本地事务是否成功
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
MessageHeaders headers = message.getHeaders();
//获取事务ID
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
log.info(“检查本地事务,事务ID:{}”,transactionId);
//根据事务id从日志表检索
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq(“transaction_id”,transactionId);
RocketmqTransactionLog rocketmqTransactionLog = rocketMqTransactionLogMapper.selectOne(queryWrapper);
if(null != rocketmqTransactionLog){
return RocketMQLocalTransactionState.COMMIT;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/4264fb7daa846b31f3d61ec96e106977.jpeg)
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
更多笔记分享
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
[外链图片转存中…(img-yNidZrAo-1711827115648)]
[外链图片转存中…(img-dvSVOFQl-1711827115648)]
更多笔记分享
[外链图片转存中…(img-8atkJF1H-1711827115649)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!