5-rocketmq-事务消息

两阶段提交协议

两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。
(1)协议参与者

在两阶段提交协议中,系统一般包含两类机器(或节点):一类为协调者(coordinator),通常一个系统中只有一个;另一类为事务参与者(participants,cohorts或workers),一般包含多个,在数据存储系统中可以理解为数据副本的个数。协议中假设每个节点都会记录写前日志(write-ahead log)并持久性存储,即使节点发生故障日志也不会丢失。协议中同时假设节点不会发生永久性故障而且任意两个节点都可以互相通信。

img

(2)两个阶段的执行

1.请求阶段(commit-request phase,或称表决阶段,voting phase)
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。

2.提交阶段(commit phase)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。
当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。
参与者在接收到协调者发来的消息后将执行响应的操作。

(3)两阶段提交的缺点

1.同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。
当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

2.单点故障。由于协调者的重要性,一旦协调者发生故障。
参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3.数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。
而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

(4)两阶段提交无法解决的问题

当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。
考虑协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。
那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

事务消息TransactionProducer

基于可靠消息服务的分布式事务最终一致性解决方案

  1. 生产者执行本地事务,修改订单支付状态,并且提交事务

  2. 生产者发送事务消息到broker上,消息发送到broker上在没有确认之前,消息对于consumer是不
    可见状态

  3. 生产者确认事务消息,使得发送到broker上的事务消息对于消费者可见

  4. 消费者获取到消息进行消费,消费完之后执行ack进行确认

  5. 这里可能会存在一个问题,生产者本地事务成功后,发送事务确认消息到broker上失败了怎么
    办?这个时候意味着消费者无法正常消费到这个消息。所以RocketMQ提供了消息回查机制,如果
    事务消息一直处于中间状态,broker会发起重试去查询broker上这个事务的处理状态。一旦发现
    事务处理成功,则把当前这条消息设置为可见

image-20200710001025520

实践:

producer

public class TransactionProducer {

    public static void main(String[] args) throws Exception {
        TransactionMQProducer transactionProducer=new
                TransactionMQProducer("tx_producer_group");
        transactionProducer.setNamesrvAddr("10.211.55.4:9876");
        ExecutorService executorService = newFixedThreadPool(10);
        //自定义线程池,用于异步执行事务操作
        transactionProducer.setExecutorService(executorService);
        transactionProducer.setTransactionListener(new TransactionListenerLocal());
        transactionProducer.start();
        for(int i=0;i<20;i++) {
            String orderId= UUID.randomUUID().toString();
            String body="{'operation':'doOrder','orderId':'"+orderId+"'}";
            Message message = new Message("pay_tx_topic", "TagA",orderId,body.getBytes(RemotingHelper.DEFAULT_CHARSET));
            transactionProducer.sendMessageInTransaction(message,orderId+"&"+i);
            Thread.sleep(1000);
        }
    }

}

TransactionListener

public class TransactionListenerLocal  implements TransactionListener {
    private static final Map<String,Boolean> results=new ConcurrentHashMap<>();
    //执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object
            arg) {
        System.out.println(":执行本地事务:"+arg.toString());
        String orderId=arg.toString();
        boolean rs=saveOrder(orderId);//模拟数据入库操作
        return rs?LocalTransactionState.COMMIT_MESSAGE:LocalTransactionState.UNKNOW;
        // 这个返回状态表示告诉broker这个事务消息是否被确认,允许给到consumer进行消费
        // LocalTransactionState.ROLLBACK_MESSAGE 回滚
        //LocalTransactionState.UNKNOW 未知
    }
    //提供事务执行状态的回查方法,提供给broker回调
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String orderId=msg.getKeys();
        System.out.println("执行事务执行状态的回查,orderId:"+orderId);
        boolean rs=Boolean.TRUE.equals(results.get(orderId));
        System.out.println("回调:"+rs);
        return rs?LocalTransactionState.COMMIT_MESSAGE:LocalTransactionState.ROLLBACK_MESSAGE;
    }

    private boolean saveOrder(String orderId){
        //如果订单取模等于0,表示成功,否则表示失败
        boolean success=Math.abs(Objects.hash(orderId))%2==0;
        results.put(orderId,success);
        return success;
    }

}

comsumer

public class TransactionConsumer {

    public static void main(String[] args) throws MQClientException, IOException
    {
        DefaultMQPushConsumer defaultMQPushConsumer=new
                DefaultMQPushConsumer("tx_consumer_group");
        defaultMQPushConsumer.setNamesrvAddr("10.211.55.4:9876");
        defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        defaultMQPushConsumer.subscribe("pay_tx_topic","*");
        defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently)
                (msgs, context) -> {
                    msgs.stream().forEach(messageExt -> {
                        try {
                            String orderId=messageExt.getKeys();
                            String body=new String(messageExt.getBody(),
                                    RemotingHelper.DEFAULT_CHARSET);
                            System.out.println("收到消息:"+body+",开始扣减库存:"+orderId);
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    });
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                });
        defaultMQPushConsumer.start();
        System.in.read();
    }


}

RocketMQ事务消息的三种状态

  1. ROLLBACK_MESSAGE:回滚事务

  2. COMMIT_MESSAGE: 提交事务

  3. UNKNOW: broker会定时的回查Producer消息状态,直到彻底成功或失败。

  • 当executeLocalTransaction方法返回ROLLBACK_MESSAGE时,表示直接回滚事务;
  • 当返回COMMIT_MESSAGE提交事务;
  • 当返回UNKNOW时,Broker会在一段时间之后回查checkLocalTransaction,根据checkLocalTransaction返回状态执行事务的操作(回滚或提交),如示例中,当返回ROLLBACK_MESSAGE时消费者不会收到消息,且不会调用回查函数,当返回COMMIT_MESSAGE时事务提交,消费者收到消息,当返回UNKNOW时,在一段时间之后调用回查函数,并根据status判断返回提交或回滚状态,返回提交状态的消息将会被消费者消费,所以此时消费者可以消费部分消息;
posted @ 2021-01-07 21:10  link_ed  阅读(185)  评论(0编辑  收藏  举报