文章目录
- 事务的相关理论
- 事务ACID特性
- CAP 理论
- BASE 理论
- 事务消息应用场景
- MQ 事务消息处理处理逻辑
- RocketMQ 事务消息处理流程
- 官网事务消息流程图
- rocketmq-client-java 示例(gRPC 协议)
- 创建事务主题
- 生产者
- 消费者
- rocketmq-client 示例(Remoting 协议)
- 生产者
- 消费者
事务的相关理论
事务ACID特性
- Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复到事务开始前的状态,就像这个事务从来没有执行过一样。
- Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义的等约束不会被破坏。
- Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
CAP 理论
CAP 定理(也称为 Brewer 定理),指的是在分布式系统环境下,有3个核心的需求:
- 一致性(Consistency):在分布式系统中,所有实例节点同一时间看到是相同的数据
- 可用性(Availability):不管是否成功,确保每一个请求都能接收到响应
- 分区容错性(Partition Tolerance):系统任意分区后,在网络故障、应用掉线时,仍能正常操作
分布式系统不可能同时满足上面三种,最多同时满足其中两种,也就是CA、CP、AP
- CA:放弃分区容错性。保证数据的强一致性,在分布式系统环境下,分区容错性一般是必要保障,否则任何一个服务或应用出文档,都将导致系统不可用。所以我们不会选择 CA,意味着我们必选 P。
- CP:放弃可用性。意味着,不能确保每个请求都能收到响应,意味着用户操作会出现长时间无反馈或者无正确反馈的问题。
- AP:放弃一致性。准确的说是放弃强一致性,在用户操作之后一定时间内,各个节点数据可能有不一致的情况,但最终会一致。例如:支付宝向银行转账,支付宝转账成功后。过一段时间在银行账户上才到账。在实际应用中,AP 是最常采用的方案。MQ 即是实现该方案的常用中间件。
BASE 理论
- Basically Available(基本可用):分布式系统在出现不可预知的故障时,允许损失部分可用性。
- Soft state(软状态):在保证系统基本可用的前提下,允许数据到部分不一致的状态。
- Eventually consistent(最终一致性):数据不一致的状态进过一定的时间,最终都会一致。
事务消息应用场景
顾名思义,事务消息主要用于分布式应用中,解决分布式事务的问题,且是采用的最终一致的方案。
此处我们采用官网案例:用户支付订单操作,此业务操作的处理分支包括:
- 主分支订单系统状态更新:由未支付变更为支付成功。
- 物流系统状态新增:新增待发货物流记录,创建订单物流记录。
- 积分系统状态变更:变更用户积分,更新用户积分表。
- 购物车系统状态变更:清空购物车,更新用户购物车记录。
此操作涉及这么多的下游系统,如果采用强一致性事务来实现,首先会导致事务控制时间太长,事务控制的范围太大,进一步导致系统并发效率低下,系统性能也低。
MQ 事务消息处理处理逻辑
此图我们还是按照用户订单支付操作的例子来说明,订单支付操作是订单系统的操作,其对应一个本地事务(branch 2),其余下游系统都是在这个支付事务之后需要执行的事务(branch 2.1、branch 2.2、branch 2.3)。
RocketMQ 事务消息,保证的是 branch 2 的事务如果成功,则 MQ 服务端就一定会有一条对应的半事务消息。如果 branch 2 的事务回滚,则 MQ 服务端也会回滚对应的半事务消息,此需要生产者来保证。
branch 2.1、branch 2.2、branch 2.3 是由各个子系统中对应的事务消息的消费者来实现的,只要 branch 2 成功,那么对应的 2.1 、2.2 、2.3 都必须要执行成功,此需要消费者来保证。
RocketMQ 事务消息处理流程
图中黄色线条为特殊情况下的状态回查流程。
-
生产者将消息发送至Apache RocketMQ服务端。
-
Apache RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
-
生产者开始执行本地事务逻辑。
-
生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
-
在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 说明 服务端回查的间隔时间和最大回查次数,请参见参数限制。
-
生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
-
生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
官网事务消息流程图
rocketmq-client-java 示例(gRPC 协议)
创建事务主题
$> ./mqadmin updatetopic -n localhost:9876 -c DefaultCluster -t MY_TRANSACTION_TOPIC -a +message.type=TRANSACTION
生产者
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.commons.collections.MapUtils;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.Transaction;
import org.apache.rocketmq.client.apis.producer.TransactionResolution;import java.util.Map;public class TransactionProducerDemo {public static void main(String[] args) {// 用于提供:生产者、消费者、消息对应的构建类 BuilderClientServiceProvider provider = ClientServiceProvider.loadService();// 构建配置类(包含端点位置、认证以及连接超时等的配置)ClientConfiguration configuration = ClientConfiguration.newBuilder()// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081.setEndpoints(MyMQProperties.ENDPOINTS).build();// 构建生产者Producer producer = null;try {producer = provider.newProducerBuilder()// Topics 列表:生产者和主题是多对多的关系,同一个生产者可以向多个主题发送消息.setTopics("MY_TRANSACTION_TOPIC").setClientConfiguration(configuration)// 设置回查对象 TransactionChecker(注意:此方法回查的是订单系统本地事务是否成功,以决定当前消息事务是否提交或回滚).setTransactionChecker(messageView -> {Map<String,String> p = messageView.getProperties();if(MapUtils.isEmpty(p)){// 说明回查的消息有误,直接回滚(此处是回滚的消息事务,半事务消息将不会投递)return TransactionResolution.ROLLBACK;}String orderId = p.get("orderId");String status = p.get("status");// 验证订单系统本地数据库事务是否成功return checkOrderStatus(orderId,status) ? TransactionResolution.COMMIT : TransactionResolution.ROLLBACK;})// 构建生产者,此方法会抛出 ClientException 异常.build();} catch (ClientException e) {throw new RuntimeException(e);}// 开启消息事务final Transaction transaction = beginTransaction(producer);// 定义消息Message message = provider.newMessageBuilder()// 设置消息发送到的主题.setTopic("MY_TRANSACTION_TOPIC")// 设置消息索引键,可根据关键字精确查找某条消息。其一般为业务上的唯一值。如:订单id.setKeys("order_id_10")// 设置消息Tag,当前为订单支付.setTag("ORDER_PAY")// 添加回查校验需要的信息.addProperty("order_id","10").addProperty("status","paid")// 消息体,单条消息的传输负载不宜过大。所以此处的字节大小最好有个限制.setBody(("{\"success\":true,\"msg\": 事务消息发送成功}").getBytes()).build();try {// 发送半事务消息(此处不要使用异步发送,因为执行的顺序即为半事务消息发送后执行本地事务逻辑)producer.send(message,transaction);// 执行本地数据库事务doLocalTransaction();// 本地事务执行成功,提交发送消息事务commitTransaction(transaction);} catch (ClientException e) {e.printStackTrace();// 半事务消息发送失败或者本地数据库事务执行失败,都回滚消息事务rollbackTransaction(transaction);throw new RuntimeException(e);} catch (Exception e){e.printStackTrace();rollbackTransaction(transaction);throw new RuntimeException(e);}}/*** 验证订单系统当前的订单状态* @param orderId 订单id* @param status 当前对应的状态* @return*/public static final boolean checkOrderStatus(String orderId,String status){// 通过 sql 或代码进行业务状态验证,检查订单系统本地数据库事务是否成功// 比如数据库信息如下:String dbOrderId = "10";String dbStatus = "paid";if(dbOrderId.equals(orderId) && dbStatus.equals(status)){return true;}return false;}public static final void rollbackTransaction(Transaction transaction){try {transaction.rollback();} catch (ClientException e) {e.printStackTrace();throw new RuntimeException(e);}}public static final void commitTransaction(Transaction transaction){try {transaction.commit();} catch (ClientException e) {e.printStackTrace();throw new RuntimeException(e);}}public static final Transaction beginTransaction(Producer producer){try {return producer.beginTransaction();} catch (ClientException e) {e.printStackTrace();throw new RuntimeException(e);}}/*** 本地事务方法* 实际应用中,此方法应该是定义在 Service 中* 且进行本地事务控制,一般情况下出现异常回滚事务,正常情况提交事务*/public static final void doLocalTransaction(){// 本地订单系统业务相关操作代码// throw new RuntimeException("本地事务失败");}}
如果 doLocalTransaction 发生异常,则半事务消息会回滚。
消费者
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;import java.nio.ByteBuffer;
import java.util.Collections;public class TranscationConsumerDemo {public static void main(String[] args) throws ClientException {// 用于提供:生产者、消费者、消息对应的构建类 BuilderClientServiceProvider provider = ClientServiceProvider.loadService();// 构建配置类(包含端点位置、认证以及连接超时等的配置)ClientConfiguration configuration = ClientConfiguration.newBuilder()// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081.setEndpoints(MyMQProperties.ENDPOINTS).build();// 设置过滤条件(这里为使用 tag 进行过滤)String tag = "ORDER_PAY";FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);// 构建消费者PushConsumer pushConsumer = provider.newPushConsumerBuilder().setClientConfiguration(configuration)// 设置消费者分组.setConsumerGroup("MY_TRANSACTION_GROUP")// 设置主题与消费者之间的订阅关系.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression)).setMessageListener(messageView -> {System.out.println(messageView);System.out.println(messageView.getProperties());ByteBuffer rs = messageView.getBody();byte[] rsByte = new byte[rs.limit()];rs.get(rsByte);System.out.println("Message body:" + new String(rsByte));// 处理消息并返回消费结果。System.out.println("Consume message successfully, messageId=" + messageView.getMessageId());return ConsumeResult.SUCCESS;}).build();// 如果不需要再使用 PushConsumer,可关闭该实例。// pushConsumer.close();}}
我们的消费者代码和普通消息的消费者是一样的,无需特殊的处理。只不过,我们的下游有多个子系统,就需要多个消费者,生产者和消费者之间是一对多的订阅关系,我们在入门介绍一章中已经介绍,为消费者定义不同的消费分组即可。
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;import java.nio.ByteBuffer;
import java.util.Collections;public class TranscationConsumerDemo {public static void main(String[] args) throws ClientException {// 用于提供:生产者、消费者、消息对应的构建类 BuilderClientServiceProvider provider = ClientServiceProvider.loadService();// 构建配置类(包含端点位置、认证以及连接超时等的配置)ClientConfiguration configuration = ClientConfiguration.newBuilder()// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081.setEndpoints(MyMQProperties.ENDPOINTS).build();// 设置过滤条件(这里为使用 tag 进行过滤)String tag = "ORDER_PAY";FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);// 模拟物流子系统消费者provider.newPushConsumerBuilder().setClientConfiguration(configuration)// 设置消费者分组.setConsumerGroup("MY_TRANSACTION_WMS_GROUP")// 设置主题与消费者之间的订阅关系.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression)).setMessageListener(messageView -> {System.out.println(messageView);System.out.println(messageView.getProperties());ByteBuffer rs = messageView.getBody();byte[] rsByte = new byte[rs.limit()];rs.get(rsByte);System.out.println("物流子系统:Message body:" + new String(rsByte));// 处理消息并返回消费结果。System.out.println("物流子系统:Consume message successfully, messageId=" + messageView.getMessageId());return ConsumeResult.SUCCESS;}).build();// 模拟积分子系统消费者provider.newPushConsumerBuilder().setClientConfiguration(configuration)// 设置消费者分组.setConsumerGroup("MY_TRANSACTION_UPS_GROUP")// 设置主题与消费者之间的订阅关系.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression)).setMessageListener(messageView -> {System.out.println(messageView);System.out.println(messageView.getProperties());ByteBuffer rs = messageView.getBody();byte[] rsByte = new byte[rs.limit()];rs.get(rsByte);System.out.println("积分子系统:Message body:" + new String(rsByte));// 处理消息并返回消费结果。System.out.println("积分子系统:Consume message successfully, messageId=" + messageView.getMessageId());return ConsumeResult.SUCCESS;}).build();// 如果不需要再使用 PushConsumer,可关闭该实例。// pushConsumer.close();}}
rocketmq-client 示例(Remoting 协议)
生产者
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class TransactionProducerDemo {/*** 生产者分组*/private static final String PRODUCER_GROUP = "TRANSCATION_PRODUCT_GROUP";/*** 主题*/private static final String TOPIC = "MY_TRANSCATION_TOPIC";public static void main(String[] args) {// 注意:事务消息使用 TransactionMQProducerTransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);// 设置 事务回查的线程池ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100,TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {Thread thread = new Thread(r);thread.setName("client-transaction-msg-check-thread");return thread;});producer.setExecutorService(executorService);// 设置监听producer.setTransactionListener(new TransactionListener(){/*** 半事务消息发送成功后,执行本地事务的方法* @param msg 半事务消息* @param arg 执行本地事务需要的业务参数* @return*/@Overridepublic LocalTransactionState executeLocalTransaction(Message msg, Object arg) {// 模拟本地数据库事务执行try {System.out.println("执行本地事务:" + msg);System.out.println("执行本地事务:" + arg);doLocalTransaction();}catch (Exception e){e.printStackTrace();return LocalTransactionState.ROLLBACK_MESSAGE;}return LocalTransactionState.COMMIT_MESSAGE;}/*** MQ 服务端未收到消息提交或回滚的确认,二次检查本地事务是否执行成功的方法* @param msg 要回查的消息* @return*/@Overridepublic LocalTransactionState checkLocalTransaction(MessageExt msg) {String orderId = msg.getProperty("orderId");String status = msg.getProperty("status");if(StringUtils.isEmpty(orderId) || StringUtils.isEmpty(status)){// 说明回查的消息有误,直接回滚(此处是回滚的消息事务,半事务消息将不会投递)return LocalTransactionState.ROLLBACK_MESSAGE;}// 通过 sql 或代码进行业务状态验证,检查订单系统本地数据库事务是否成功// 比如数据库信息如下:String dbOrderId = "10";String dbStatus = "paid";if(dbOrderId.equals(orderId) &&dbStatus.equals(status)){return LocalTransactionState.COMMIT_MESSAGE;}return LocalTransactionState.ROLLBACK_MESSAGE;}});/** NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876*/producer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);/** 发送消息超时时间,默认即为 3000*/producer.setSendMsgTimeout(3000);try {producer.start();} catch (MQClientException e) {throw new RuntimeException(e);}// 发送事务消息Message msg = new Message();msg.setTopic(TOPIC);// 设置消息索引键,可根据关键字精确查找某条消息。msg.setKeys("order_id_10");// 设置消息Tag,用于消费端根据指定Tag过滤消息。msg.setTags("ORDER_PAY");// 设置消息体msg.setBody(("{\"success\":true,\"msg\": Remoting 协议事务消息发送成功}").getBytes());// 添加 Propertiesmsg.putUserProperty("orderId","10");msg.putUserProperty("status","paid");try {// 发送事务消息producer.sendMessageInTransaction(msg,"业务参数对象");} catch (MQClientException e) {throw new RuntimeException(e);}}/*** 本地事务方法* 实际应用中,此方法应该是定义在 Service 中* 且进行本地事务控制,一般情况下出现异常回滚事务,正常情况提交事务*/public static final void doLocalTransaction(){// 本地订单系统业务相关操作代码// throw new RuntimeException("本地事务失败");}}
消费者
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;public class TransactionConsumerDemo {/*** 设置消费者分组*/public static final String CONSUMER_GROUP = "TRANSCATION_CONSUMER_GROUP";/*** 主题*/public static final String TOPIC = "MY_TRANSCATION_TOPIC";public static void main(String[] args) throws MQClientException {/** 通过消费者分组,创建消费者*/DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);/** NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876*/consumer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);/** 指定从哪一个消费位点开始消费 CONSUME_FROM_FIRST_OFFSET 表示从第一个开始*/consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);/** 消费者订阅的主题,和过滤条件* 我们这里使用 * 表示,消费者消费主题下的所有消息,多个tag 使用 || 隔开*/consumer.subscribe(TOPIC, "ORDER_PAY");/** 注册消费监听*/consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;});/** 启动消费者.*/consumer.start();System.out.printf("Consumer Started.%n");// 如果消费者不再使用,关闭// consumer.shutdown();}}