大家好,我是苍何。

说起事务消息,你可能和我一样一开始有一些懵逼,但说起事务和分布式事务,我想对于八股选手来说,再熟悉不过了。

事务消息就是利用消息队列实现分布式事务的一种解决方案,而 RocketMQ 又有自己的实现方式。

一次性抛出来太多名词啦,让我们依次剖析,通过本文全面了解 RocketMQ 的事务消息,助力八股选手在线拷打面试官😀

什么是事务

我们先想象一个场景:

假设用户 A 向用户 B 转账 100 元,这个操作包括以下步骤:

  1. 从用户 A 的账户中扣减 100 元。
  2. 向用户 B 的账户中增加 100 元。

这两个步骤要么都成功,要么都失败。如果在步骤 1 扣减了100元,但步骤 2 增加时发生了错误,我们希望,用户A的账户金额恢复到原来的状态。

图解RocketMQ之如何实现事务消息_分布式事务

上面是一个典型的事务场景,什么是事务?,简而言之,事务是逻辑上的一组操作,要么都执行,要么都不执行

下面有一道经典的八股,衡量事务的四大特性的了解程度,理解的背一下就好了。

事务的四大特性(ACID)

  1. 原子性(Atomicity)
  • 事务中的所有操作要么全部成功,要么全部失败。即使在系统故障的情况下,事务也能保证不会只执行一部分操作。
  • 例子:银行转账操作中,从一个账户扣钱并在另一个账户加钱,这两步操作要么都成功,要么都失败。
  1. 一致性(Consistency)
  • 事务执行前后,数据库都必须处于一致的状态。所有事务必须使数据库从一个一致状态变换到另一个一致状态。
  • 例子:转账后,两个账户的总金额应该保持不变。
  1. 隔离性(Isolation)
  • 并发事务之间互不影响,一个事务的中间状态对其他事务不可见。不同事务之间的操作是相互独立的。
  • 例子:同时进行的两个转账操作不会互相干扰,每个操作都看不到对方的中间状态。
  1. 持久性(Durability)
  • 一旦事务提交,其结果是永久性的,即使系统崩溃,事务的结果也不会丢失。
  • 例子:转账成功后,系统崩溃重启,账户金额的变动依然存在

什么是本地事务

在单体应用中的事务其实都属于本地事务,比如在 springboot 中在方法上加 @Transactional 注解,其实走的也是一种本地事务。

在单体中,通常一个操作,可能会涉及多张表,但都位于同一个数据库中,比如在 MySQL 中,一个事务可能包含多条 sql 语句的执行,这些语句要么执行成功,要么都执行失败,MySQL 支持事务的引擎是我们常用的 InnoDB,而 MyISAM 是不支持事务的。

MySQL 中主要是通过 redo log 和 undo log 来控制事务,redo log 是在事务提交前回滚,保证事务的持久性undo log 是在事务提交后回滚,保证事务的原子性,具体可以看下面这张图:

图解RocketMQ之如何实现事务消息_分布式事务_02

什么是分布式事务

在分布式微服务系统中,原先的单体系统被拆成了多个微服务,比如 PmHub 中拆分了系统服务,项目服务、流程服务等,在实际应用中,每一个微服务可能会部署在不同的机器上,并且微服务的数据库是隔离的,比如 PmHub 中的 pmhub-project 微服务用的是 pmhub-project 数据库,pmhub-workflow 服务用的是 pmhub-workflow 数据库,

这种情况下,一个操作可能会涉及多个机器、多个服务、多个数据库。比如 PmHub 中的添加项目任务场景。

那么如何保证同一个操作,要么全部执行成功,要么全部执行失败呢

那就需要用到分布式事务解决方案,本地事务解决的是单数据源的数据一致性问题,分布式事务解决的是多数据源数据一致性问题。

什么是事务消息

解决分布式事务,有多种成熟的方案,比如 2 PC、3 PC、TCC、本地消息、事务消息等。(这里的每一种方案要放细了说可以来个十万八千字,感兴趣的小伙伴可以先查资料学习一波)

我们今天的角儿是要通过事务消息来解决分布式事务,那什么是事务消息?

事务消息是一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。

而 RocketMQ 高级特性之一就是支持事务消息,基于 RocketMQ 实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的最终一致性

我来做个浅显的总结吧,事务消息也是消息的一种,只是比普通消息多了 plus 功能,那就是支持二阶段提交和回滚能力。

图解RocketMQ之如何实现事务消息_分布式事务_03

RocketMQ 事务消息实现原理

RocketMQ 采用了 2 PC 的方案来提交事务消息。

第一阶段,Producer 向 Broker 发送预处理消息(也称半事务消息),此时消息还未被投递出去,Consumer 不能消费,第二阶段,Producer 向 Broker 发送提交或回滚消息。

下面是具体的流程:

提交事务消息流程

  • 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
  • Producer 开始执行本地事务
  • 如果本地事务执行成功,Producer 发送提交事务消息
  • 消息被投递给 Consumer,如下图所示:

图解RocketMQ之如何实现事务消息_Java_04

回滚事务消息流程

  • 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
  • Producer 开始执行本地事务
  • 如果本地事务执行异常,Producer 发送提交回滚事务消息
  • 服务端将回滚事务,消息不会被投递给 Consumer,如下图所示:

图解RocketMQ之如何实现事务消息_rocketmq_05

回查事务消息流程

如果本地事务状态为未知 Unknown,或者在断网或者是 Producer 应用重启等特殊情况下,若 Broker 未收到 Producer 提交的二次确认结果,一定时间后,Broker 会向 Producer 发起消息回查,来确认是提交还是回滚,如果回查也查不到,就需要人工介入了。

消息回查有点像是上学那会我们主动向老师提交作业,如果我们没提交,老师就会反过头来查我们,颇有一番相似。

图解RocketMQ之如何实现事务消息_Java_06

实战——如何发送事务消息

在 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 篇,我们下篇见~