RocketMQ学习-事务消息

本文深入探讨了RocketMQ中事务消息的实现原理,包括两阶段提交的过程及其代码实现细节。此外,还介绍了如何通过补偿机制确保分布式系统的一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RocketMQ学习:事务消息

发表于2015 年 9 月 2 日考拉哥

源代码版本是3.2.6,还是直接跑源代码。rocketmq事务消息是发生在Producer和Broker之间,是二阶段提交。

二阶段提交过程看图:
事务逻辑

第一阶段是:步骤1,2,3。
第二阶段是:步骤4,5。

具体说明:

只有在消息发送成功,并且本地操作执行成功时,才发送提交事务消息,做事务提交。

其他的情况,例如消息发送失败,直接发送回滚消息,进行回滚,或者发送消息成功,但是执行本地操作失败,也是发送回滚消息,进行回滚。

事务消息原理实现过程:

一阶段:
Producer向Broker发送1条类型为TransactionPreparedType的消息,Broker接收消息保存在CommitLog中,然后返回消息的queueOffset和MessageId到Producer,MessageId包含有commitLogOffset(即消息在CommitLog中的偏移量,通过该变量可以直接定位到消息本身),由于该类型的消息在保存的时候,commitLogOffset没有被保存到consumerQueue中,此时客户端通过consumerQueue取不到commitLogOffset,所以该类型的消息无法被取到,导致不会被消费。

一阶段的过程中,Broker保存了1条消息。

二阶段:
Producer端的TransactionExecuterImpl执行本地操作,返回本地事务的状态,然后发送一条类型为TransactionCommitType或者TransactionRollbackType的消息到Broker确认提交或者回滚,Broker通过Request中的commitLogOffset,获取到上面状态为TransactionPreparedType的消息(简称消息A),然后重新构造一条与消息A内容相同的消息B,设置状态为TransactionCommitType或者TransactionRollbackType,然后保存。其中TransactionCommitType类型的,会放commitLogOffset到consumerQueue中,TransactionRollbackType类型的,消息体设置为空,不会放commitLogOffset到consumerQueue中。

二阶段的过程中,Broker也保存了1条消息。

总结:事务消息过程中,broker一共保存2条消息。

贴代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

<properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <logback.version>1.0.13</logback.version>

    <rocketmq.version>3.2.6</rocketmq.version>

</properties>

 

<dependencies>

    <dependency>

        <groupId>ch.qos.logback</groupId>

        <artifactId>logback-classic</artifactId>

        <version>1.0.13</version>

    </dependency>

    <dependency>

        <groupId>ch.qos.logback</groupId>

        <artifactId>logback-core</artifactId>

        <version>1.0.13</version>

    </dependency>

    <dependency>

        <groupId>com.alibaba.rocketmq</groupId>

        <artifactId>rocketmq-client</artifactId>

        <version>${rocketmq.version}</version>

    </dependency>

    <dependency>

        <groupId>junit</groupId>

        <artifactId>junit</artifactId>

        <version>4.10</version>

        <scope>test</scope>

    </dependency>

</dependencies>

TransactionCheckListenerImpl.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

package com.zoo.quickstart.transaction;

 

import java.util.concurrent.atomic.AtomicInteger;

 

import com.alibaba.rocketmq.client.producer.LocalTransactionState;

import com.alibaba.rocketmq.client.producer.TransactionCheckListener;

import com.alibaba.rocketmq.common.message.MessageExt;

 

 

/**

 * 未决事务,服务器回查客户端,broker端发起请求代码没有被调用,所以此处代码可能没用。

 */

public class TransactionCheckListenerImpl implements TransactionCheckListener {

    private AtomicInteger transactionIndex = new AtomicInteger(0);

 

 

    @Override

    public LocalTransactionState checkLocalTransactionState(MessageExt msg) {

        System.out.println("server checking TrMsg " + msg.toString());

 

        int value = transactionIndex.getAndIncrement();

        if ((value % 6) == 0) {

            throw new RuntimeException("Could not find db");

        }

        else if ((value % 5) == 0) {

            return LocalTransactionState.ROLLBACK_MESSAGE;

        }

        else if ((value % 4) == 0) {

            return LocalTransactionState.COMMIT_MESSAGE;

        }

 

        return LocalTransactionState.UNKNOW;

    }

}

本地操作类TransactionExecuterImpl.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

package com.zoo.quickstart.transaction;

 

import java.util.concurrent.atomic.AtomicInteger;

 

import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter;

import com.alibaba.rocketmq.client.producer.LocalTransactionState;

import com.alibaba.rocketmq.common.message.Message;

 

/**

 * 执行本地事务

 */

public class TransactionExecuterImpl implements LocalTransactionExecuter {

    private AtomicInteger transactionIndex = new AtomicInteger(1);

 

 

    @Override

    public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {

        int value = transactionIndex.getAndIncrement();

 

        if (value == 0) {

            throw new RuntimeException("Could not find db");

        }

        else if ((value % 5) == 0) {

            return LocalTransactionState.ROLLBACK_MESSAGE;

        }

        else if ((value % 4) == 0) {

            return LocalTransactionState.COMMIT_MESSAGE;

        }

 

        return LocalTransactionState.UNKNOW;

    }

}

Producer类:TransactionProducer.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

package com.zoo.quickstart.transaction;

 

import com.alibaba.rocketmq.client.exception.MQClientException;

import com.alibaba.rocketmq.client.producer.SendResult;

import com.alibaba.rocketmq.client.producer.TransactionCheckListener;

import com.alibaba.rocketmq.client.producer.TransactionMQProducer;

import com.alibaba.rocketmq.common.message.Message;

 

 

/**

 * 发送事务消息例子

 *

 */

public class TransactionProducer {

    public static void main(String[] args) throws MQClientException, InterruptedException {

 

        TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();

        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");

        // 事务回查最小并发数

        producer.setCheckThreadPoolMinSize(2);

        // 事务回查最大并发数

        producer.setCheckThreadPoolMaxSize(2);

        // 队列数

        producer.setCheckRequestHoldMax(2000);

        producer.setTransactionCheckListener(transactionCheckListener);

        producer.setNamesrvAddr("192.168.0.104:9876");

        producer.start();

 

        String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" };

        TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();

        for (int i = 0; i < 1; i++) {

            try {

                Message msg =

                        new Message("TopicTest", tags[i % tags.length], "KEY" + i,

                            ("Hello RocketMQ " + i).getBytes());

                SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);

                System.out.println(sendResult);

 

                Thread.sleep(10);

            }

            catch (MQClientException e) {

                e.printStackTrace();

            }

        }

 

        for (int i = 0; i < 100000; i++) {

            Thread.sleep(1000);

        }

 

        producer.shutdown();

 

    }

}

此条目发表在编程语言分类目录,贴了javarocketmq两阶段事务提交标签。将固定链接加入收藏夹。

 

http://www.2cto.com/kf/201606/521538.html

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?

RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图: 
这里写图片描述

总结:据笔者的了解,各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。比较遗憾的是,RocketMQ事务消息部分的代码也并未开源,需要自己去实现。

其他补偿方式

做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。 
其实这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似。

一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,那么这种重试机制其实是非常有效的。

当然,考虑个比较极端的场景,假如系统自身有bug或者程序逻辑有问题,那么重试1W次那也是无济于事的。那岂不是就发生了“明明已经付款,却显示未付款不发货”类似的悲剧?

其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。

在某些特殊的情况下,还会有“人工补偿”的,这也是最后一道屏障。

小结

上诉的几种方案中,笔者也大致总结了其设计思路,优势,劣势等,相信读者已经有了一定的理解。其实分布式系统的事务一致性本身是一个技术难题,目前没有一种很简单很完美的方案能够应对所有场景。具体还是要使用者根据不同的业务场景去抉择。

转载于:https://my.oschina.net/tantexian/blog/729732

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值