大家好,我是苍何。
说起事务消息,你可能和我一样一开始有一些懵逼,但说起事务和分布式事务,我想对于八股选手来说,再熟悉不过了。
事务消息就是利用消息队列实现分布式事务的一种解决方案,而 RocketMQ 又有自己的实现方式。
一次性抛出来太多名词啦,让我们依次剖析,通过本文全面了解 RocketMQ 的事务消息,助力八股选手在线拷打面试官😀
什么是事务
我们先想象一个场景:
假设用户 A 向用户 B 转账 100 元,这个操作包括以下步骤:
- 从用户 A 的账户中扣减 100 元。
- 向用户 B 的账户中增加 100 元。
这两个步骤要么都成功,要么都失败。如果在步骤 1 扣减了100元,但步骤 2 增加时发生了错误,我们希望,用户A的账户金额恢复到原来的状态。
上面是一个典型的事务场景,什么是事务?,简而言之,事务是逻辑上的一组操作,要么都执行,要么都不执行。
下面有一道经典的八股,衡量事务的四大特性的了解程度,理解的背一下就好了。
事务的四大特性(ACID)
- 原子性(Atomicity):
- 事务中的所有操作要么全部成功,要么全部失败。即使在系统故障的情况下,事务也能保证不会只执行一部分操作。
- 例子:银行转账操作中,从一个账户扣钱并在另一个账户加钱,这两步操作要么都成功,要么都失败。
- 一致性(Consistency):
- 事务执行前后,数据库都必须处于一致的状态。所有事务必须使数据库从一个一致状态变换到另一个一致状态。
- 例子:转账后,两个账户的总金额应该保持不变。
- 隔离性(Isolation):
- 并发事务之间互不影响,一个事务的中间状态对其他事务不可见。不同事务之间的操作是相互独立的。
- 例子:同时进行的两个转账操作不会互相干扰,每个操作都看不到对方的中间状态。
- 持久性(Durability):
- 一旦事务提交,其结果是永久性的,即使系统崩溃,事务的结果也不会丢失。
- 例子:转账成功后,系统崩溃重启,账户金额的变动依然存在
什么是本地事务
在单体应用中的事务其实都属于本地事务,比如在 springboot 中在方法上加 @Transactional 注解,其实走的也是一种本地事务。
在单体中,通常一个操作,可能会涉及多张表,但都位于同一个数据库中,比如在 MySQL 中,一个事务可能包含多条 sql 语句的执行,这些语句要么执行成功,要么都执行失败,MySQL 支持事务的引擎是我们常用的 InnoDB,而 MyISAM 是不支持事务的。
MySQL 中主要是通过 redo log 和 undo log 来控制事务,redo log 是在事务提交前回滚,保证事务的持久性, undo log 是在事务提交后回滚,保证事务的原子性,具体可以看下面这张图:
什么是分布式事务
在分布式微服务系统中,原先的单体系统被拆成了多个微服务,比如 PmHub 中拆分了系统服务,项目服务、流程服务等,在实际应用中,每一个微服务可能会部署在不同的机器上,并且微服务的数据库是隔离的,比如 PmHub 中的 pmhub-project 微服务用的是 pmhub-project 数据库,pmhub-workflow 服务用的是 pmhub-workflow 数据库,
这种情况下,一个操作可能会涉及多个机器、多个服务、多个数据库。比如 PmHub 中的添加项目任务场景。
那么如何保证同一个操作,要么全部执行成功,要么全部执行失败呢?
那就需要用到分布式事务解决方案,本地事务解决的是单数据源的数据一致性问题,分布式事务解决的是多数据源数据一致性问题。
什么是事务消息
解决分布式事务,有多种成熟的方案,比如 2 PC、3 PC、TCC、本地消息、事务消息等。(这里的每一种方案要放细了说可以来个十万八千字,感兴趣的小伙伴可以先查资料学习一波)
我们今天的角儿是要通过事务消息来解决分布式事务,那什么是事务消息?
事务消息是一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
而 RocketMQ 高级特性之一就是支持事务消息,基于 RocketMQ 实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的最终一致性。
我来做个浅显的总结吧,事务消息也是消息的一种,只是比普通消息多了 plus 功能,那就是支持二阶段提交和回滚能力。
RocketMQ 事务消息实现原理
RocketMQ 采用了 2 PC 的方案来提交事务消息。
第一阶段,Producer 向 Broker 发送预处理消息(也称半事务消息),此时消息还未被投递出去,Consumer 不能消费,第二阶段,Producer 向 Broker 发送提交或回滚消息。
下面是具体的流程:
提交事务消息流程
- 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
- Producer 开始执行本地事务
- 如果本地事务执行成功,Producer 发送提交事务消息
- 消息被投递给 Consumer,如下图所示:
回滚事务消息流程
- 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
- Producer 开始执行本地事务
- 如果本地事务执行异常,Producer 发送提交回滚事务消息
- 服务端将回滚事务,消息不会被投递给 Consumer,如下图所示:
回查事务消息流程
如果本地事务状态为未知 Unknown,或者在断网或者是 Producer 应用重启等特殊情况下,若 Broker 未收到 Producer 提交的二次确认结果,一定时间后,Broker 会向 Producer 发起消息回查,来确认是提交还是回滚,如果回查也查不到,就需要人工介入了。
消息回查有点像是上学那会我们主动向老师提交作业,如果我们没提交,老师就会反过头来查我们,颇有一番相似。
实战——如何发送事务消息
在 PmHub 中通过 Seata 来处理添加任务过程中的分布式事务逻辑,这是原先逻辑流程:
Seata 实现核心代码:
@Override
@GlobalTransactional(name = "pmhub-project-addTask",rollbackFor = Exception.class) //seata分布式事务,AT模式
public String add(TaskReqVO taskReqVO) {
// xid 全局事务id的检查(方便查看)
String xid = RootContext.getXID();
log.info("---------------开始新建任务: "+"\t"+"xid: "+xid);
if (ProjectStatusEnum.PAUSE.getStatus().equals(projectTaskMapper.queryProjectStatus(taskReqVO.getProjectId()))) {
throw new ServiceException("归属项目已暂停,无法新增任务");
}
// 1、添加任务
ProjectTask projectTask = new ProjectTask();
if (StringUtils.isNotBlank(taskReqVO.getTaskId())) {
projectTask.setTaskPid(taskReqVO.getTaskId());
}
BeanUtils.copyProperties(taskReqVO, projectTask);
projectTask.setCreatedBy(SecurityUtils.getUsername());
projectTask.setCreatedTime(new Date());
projectTask.setUpdatedBy(SecurityUtils.getUsername());
projectTask.setUpdatedTime(new Date());
projectTaskMapper.insert(projectTask);
// 2、添加任务成员
insertMember(projectTask.getId(), 1, SecurityUtils.getUserId());
// 3、添加日志
saveLog("addTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(), "参与了任务", null);
// 将执行人加入
if (taskReqVO.getUserId() != null && !Objects.equals(taskReqVO.getUserId(), SecurityUtils.getUserId())) {
insertMember(projectTask.getId(), 0, taskReqVO.getUserId());
// 添加日志
saveLog("invitePartakeTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(), "邀请 " + projectMemberMapper.selectUserById(Collections.singletonList(taskReqVO.getUserId())).get(0).getNickName() + " 参与任务", taskReqVO.getUserId());
}
// 4、任务指派消息提醒
extracted(taskReqVO.getTaskName(), taskReqVO.getUserId(), SecurityUtils.getUsername(), projectTask.getId());
// 5、添加或更新审批设置(远程调用 pmhub-workflow 微服务)
ApprovalSetDTO approvalSetDTO = new ApprovalSetDTO(projectTask.getId(), ProjectStatusEnum.TASK.getStatusName(),
taskReqVO.getApproved(), taskReqVO.getDefinitionId(), taskReqVO.getDeploymentId());
R<?> result = wfDeployService.insertOrUpdateApprovalSet(approvalSetDTO, SecurityConstants.INNER);
if (Objects.isNull(result) || Objects.isNull(result.getData())
|| R.fail().equals(result.getData())) {
throw new ServiceException("远程调用审批服务失败");
}
log.info("---------------结束新建任务: "+"\t"+"xid: "+xid);
return projectTask.getId();
}
现在我们也可以借助 RocketMQ 的事务消息来实现 PmHub 中任务管理的分布式事务。
1、先定义一个事务消息的Producer:
@Component
public class TaskTransactionProducer {
private TransactionMQProducer producer;
private TaskService taskService;
public TaskTransactionProducer(TaskService taskService) {
this.taskService = taskService;
producer = new TransactionMQProducer("task_transaction_producer");
producer.setNamesrvAddr("localhost:9876"); // 设置NameServer地址
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务
return taskService.addTaskLocal((TaskReqVO) arg);
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查本地事务状态
return taskService.checkTaskTransaction(msg);
}
});
try {
producer.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendTaskMessage(TaskReqVO taskReqVO) throws Exception {
Message msg = new Message("TaskTopic", "TaskTag", taskReqVO.toString().getBytes());
producer.sendMessageInTransaction(msg, taskReqVO);
}
}
然后,我们修改TaskService类:
@Service
public class TaskService {
@Autowired
private ProjectTaskMapper projectTaskMapper;
@Autowired
private ProjectMemberMapper projectMemberMapper;
// ... 其他需要的依赖
// 本地事务执行
public LocalTransactionState addTaskLocal(TaskReqVO taskReqVO) {
try {
// 检查项目状态
if (ProjectStatusEnum.PAUSE.getStatus().equals(projectTaskMapper.queryProjectStatus(taskReqVO.getProjectId()))) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 1. 添加任务
ProjectTask projectTask = new ProjectTask();
if (StringUtils.isNotBlank(taskReqVO.getTaskId())) {
projectTask.setTaskPid(taskReqVO.getTaskId());
}
BeanUtils.copyProperties(taskReqVO, projectTask);
projectTask.setCreatedBy(SecurityUtils.getUsername());
projectTask.setCreatedTime(new Date());
projectTask.setUpdatedBy(SecurityUtils.getUsername());
projectTask.setUpdatedTime(new Date());
projectTaskMapper.insert(projectTask);
// 2. 添加任务成员
insertMember(projectTask.getId(), 1, SecurityUtils.getUserId());
// 3. 添加日志
saveLog("addTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(), "参与了任务", null);
// 将执行人加入
if (taskReqVO.getUserId() != null && !Objects.equals(taskReqVO.getUserId(), SecurityUtils.getUserId())) {
insertMember(projectTask.getId(), 0, taskReqVO.getUserId());
saveLog("invitePartakeTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(),
"邀请 " + projectMemberMapper.selectUserById(Collections.singletonList(taskReqVO.getUserId())).get(0).getNickName() + " 参与任务",
taskReqVO.getUserId());
}
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 事务消息回查
public LocalTransactionState checkTaskTransaction(MessageExt msg) {
// 实现事务状态回查逻辑
// 这里需要根据消息内容检查任务是否已经成功创建
// 如果任务已创建,返回 COMMIT_MESSAGE
// 如果任务未创建,返回 ROLLBACK_MESSAGE
// 如果无法确定,返回 UNKNOW
// 示例:
String taskId = new String(msg.getBody());
if (projectTaskMapper.selectById(taskId) != null) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 处理远程操作
public void processRemoteOperations(TaskReqVO taskReqVO, String taskId) {
// 4. 任务指派消息提醒
extracted(taskReqVO.getTaskName(), taskReqVO.getUserId(), SecurityUtils.getUsername(), taskId);
// 5. 添加或更新审批设置(远程调用 pmhub-workflow 微服务)
ApprovalSetDTO approvalSetDTO = new ApprovalSetDTO(taskId, ProjectStatusEnum.TASK.getStatusName(),
taskReqVO.getApproved(), taskReqVO.getDefinitionId(), taskReqVO.getDeploymentId());
R<?> result = wfDeployService.insertOrUpdateApprovalSet(approvalSetDTO, SecurityConstants.INNER);
if (Objects.isNull(result) || Objects.isNull(result.getData()) || R.fail().equals(result.getData())) {
throw new ServiceException("远程调用审批服务失败");
}
}
// ... 其他辅助方法(如insertMember, saveLog, extracted等)
}
最后,修改原来的add方法:
@Autowired
private TaskTransactionProducer taskTransactionProducer;
public String add(TaskReqVO taskReqVO) throws Exception {
// 发送事务消息
taskTransactionProducer.sendTaskMessage(taskReqVO);
return "Task creation initiated";
}
1、预提交:在TaskTransactionProducer中的sendTaskMessage方法发送事务消息时触发。
2、本地事务:在executeLocalTransaction方法中执行,包括添加任务、添加任务成员和添加日志。
3、提交或回滚:根据本地事务的执行结果,在executeLocalTransaction方法返回相应的状态。
4、回查:在checkLocalTransaction方法中实现,用于处理本地事务执行结果未知的情况。
5、远程操作:在消息被确认提交后,消费者会处理消息并执行远程操作(任务指派消息提醒和添加或更新审批设置)。
这种方式将原来的操作分为了本地事务和远程操作两部分,通过RocketMQ的事务消息机制来保证整个过程的一致性。如果本地事务失败,消息不会被发送;如果远程操作失败,可以通过重试机制来保证最终一致性。
最后
其实 RocketMQ 通过事务消息实现分布式事务保证最终一致性,是性能比较好的解决方案,因其原理是 2 阶段提交,先预提交,然后根据本地事务的状态来决定最终是提交还是回滚。
刚开始接触可能会被很多新鲜的概念吓到,其实原理并不复杂。
需要特别注意 RocketMQ 事务的超时机制,即半事务消息被生产者发送到 Broker 后,如果在指定时间内服务端无法确认提交或者回滚状态,则消息默认会被回滚。
默认的话是 4 个小时,超过 4 个小时还没确定状态,半事务消息则会强制回滚,此时 broker 中的消息不回投递给消费者。
好啦,有关 RocketMQ 的事务消息就到这啦,有疑惑和问题欢迎大家留言讨论。
我是苍何,这是图解 RocketMQ 教程的第 10 篇,我们下篇见~