在rocketMQ中生产者有三种角色 NormalProducer(普通)、OrderProducer(顺序)、TransactionProducer(事务)
根据名字大概可以看出各个代表着什么作用,我们这里用 TransactionProducer(事务)来解决问题。
先举个列子来说明下我们解决方案的设计方式吧:最经典的莫过于银行转账了,网上到处都有,时序图如下
下面贴一下测试代码:
(1) 执行业务逻辑的部分
/**
* @Date: Created in 2018/2/12 15:55
执行本地事务
*/
public class TransactionExecuterimpl implements LocalTransactionExecuter{
@Override
public LocalTransactionState executeLocalTransactionBranch(final Message message, final Object o) {
try{
//DB操作 应该带上事务 service -> dao
//如果数据操作失败 需要回滚 同事返回RocketMQ一个失败消息 意味着 消费者无法消费到这条失败的消息
//如果成功 就要返回一个rocketMQ成功的消息,意味着消费者将读取到这条消息
//o就是attachment
//测试代码
if(new Random().nextInt(3) == 2){
int a = 1 / 0;
}
System.out.println(new Date()+"===> 本地事务执行成功,发送确认消息");
}catch (Exception e){
System.out.println(new Date()+"===> 本地事务执行失败!!!");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
(2) 处理事务回查的代码部分
/**
* @Date: Created in 2018/2/12 15:48
* 未决事务,服务器端回查客户端
*/
public class TransactionCheckListenerImpl implements TransactionCheckListener {
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt messageExt) {
System.out.println("服务器端回查事务消息: "+messageExt.toString());
//由于RocketMQ迟迟没有收到消息的确认消息,因此主动询问这条prepare消息,是否正常?
//可以查询数据库看这条数据是否已经处理
return LocalTransactionState.COMMIT_MESSAGE;
}
}
(3) 启动生产者
/**
* @Date: Created in 2018/2/12 15:24
* 测试本地事务
*/
public class TestTransactionProducer {
public static void main(String[] args){
//事务回查监听器
TransactionCheckListenerImpl checkListener = new TransactionCheckListenerImpl();
//事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("transactionProducerGroup");
//MQ服务器地址
producer.setNamesrvAddr("192.168.56.105:9876;192.168.106:9876");
//注册事务回查监听
producer.setTransactionCheckListener(checkListener);
//本地事务执行器
TransactionExecuterimpl executerimpl = null;
try {
//启动生产者
producer.start();
executerimpl = new TransactionExecuterimpl();
Message msg1 = new Message("TransactionTopic", "tag", "KEY1", "hello RocketMQ 1".getBytes());
Message msg2 = new Message("TransactionTopic", "tag", "KEY2", "hello RocketMQ 2".getBytes());
SendResult sendResult = producer.sendMessageInTransaction(msg1, executerimpl, null);
System.out.println(new Date() + "msg1"+sendResult);
sendResult = producer.sendMessageInTransaction(msg1, executerimpl, null);
System.out.println(new Date() + "msg2"+sendResult);
} catch (MQClientException e) {
e.printStackTrace();
}
producer.shutdown();
}
}
(4) 消费之消费消息
/**
* @Date: Created in 2018/2/11 15:37
*/
public class TestConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("192.168.56.105:9876;192.168.56.106:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//消费普通消息
// consumer.subscribe("TopicTest","*");
//消费事务消息
consumer.subscribe("TransactionTopic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt ext:msgs) {
try {
System.out.println(new Date() + new String(ext.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Start............");
}
}
重点来了:3.2.6之前的版本这样写就可以了,但是之后的版本被关于事务回查这个借口被阉割了,不会在进行事务回查操作。
那么第五步向MQ发送消息如果失败的话,会造成A银行扣款成功而B银行收款未成功的数据不一致的情况
这里只需要考虑本地事务执行成功后的情况(因为本地事务失败不管确认消息发送成功与失败MQ集群都不会再发送消息到消费者):
上面三种情况的本质是一样的,就是生产者本地事务成功后,COMMIT_MESSGE消息是否送达rocketmq集群;所以可以看做同一种情况.
下面是按key值查询事务成功提交COMMIT_MESSGE
消息后的返回信息:
QueryResult
[indexLastUpdateTimestamp=1516002830440,
messageList=[
MessageExt [queueId=1, storeSize=291, queueOffset=0, sysFlag=4, bornTimestamp=1516002831147, bornHost=/192.168.88.1:6313, storeTimestamp=1516002830323, storeHost=/192.168.88.133:10911, msgId=C0A8588500002A9F0000000000000246, commitLogOffset=582, bodyCRC=1229495611, reconsumeTimes=0, preparedTransactionOffset=0, toString()=
Message
[topic=pay, flag=0, properties=
{KEYS=5cf5dd03-5811-4b7f-b97c-186598e6d08b, TRAN_MSG=true, UNIQ_KEY=C0A8080B2080085EDE7B4B824F2A0000, PGROUP=transaction-pay, TAGS=tag},
body=67]],
MessageExt [queueId=1, storeSize=291, queueOffset=0, sysFlag=8, bornTimestamp=1516002831147, bornHost=/192.168.88.1:6313, storeTimestamp=1516002830440, storeHost=/192.168.88.133:10911, msgId=C0A8588500002A9F0000000000000369, commitLogOffset=873, bodyCRC=1229495611, reconsumeTimes=0, preparedTransactionOffset=582, toString()=
Message
[topic=pay, flag=0, properties=
{KEYS=5cf5dd03-5811-4b7f-b97c-186598e6d08b, TRAN_MSG=true, UNIQ_KEY=C0A8080B2080085EDE7B4B824F2A0000, PGROUP=transaction-pay, TAGS=tag},
body=67]]
]
]
共返回两条消息:两条消息中大部分数据是一样的,但sysFlag
、storeTimestamp
、msgId
、commitLogOffset
、preparedTransactionOffset
字段是不一样的:其中第1条为prepared发送的消息,第2条只有在提交COMMIT_MESSGE消息成功后产生。
注意sysFlag
、preparedTransactionOffset
字段与prepared
消息的区别,当提交COMMIT_MESSGE消息成功后,推测MQ集群做了如下动作:1. 读取prepared消息,修改sysFlag
、preparedTransactionOffset
值,2. 在存入commitlog日志文件,设置consumerqueue序列;因为当作一条新的消息处理,所以toreTimestamp
、msgId
、commitLogOffset
字段自然也就变了。所以按照发送的prepared消息的返回结果显示的msgId
查看sysFlag
状态只是prepared消息的sysFlag
状态,RocketMQ4.2版本的话要用key值去查询,才能查看事务提交成功的消息标志sysFlag=8
。
下面是按key值查询事务失败提交ROLLBACK_MESSAGE
消息后的返回信息:
QueryResult
[indexLastUpdateTimestamp=1516002830440,
messageList=[
MessageExt [queueId=1, storeSize=291, queueOffset=0, sysFlag=4, bornTimestamp=1516002831147, bornHost=/192.168.88.1:6313, storeTimestamp=1516002830323, storeHost=/192.168.88.133:10911, msgId=C0A8588500002A9F0000000000000246, commitLogOffset=582, bodyCRC=1229495611, reconsumeTimes=0, preparedTransactionOffset=0, toString()=
Message
[topic=pay, flag=0, properties=
{KEYS=5cf5dd03-5811-4b7f-b97c-186598e6d08b, TRAN_MSG=true, UNIQ_KEY=C0A8080B2080085EDE7B4B824F2A0000, PGROUP=transaction-pay, TAGS=tag},
body=67]]
]
]
由此我们可以根据syyFlag判断我们在提交事务以后, 消息发送是否成功
下面是逻辑分析图
一、在执行本地事务commit前向回查表插入消息的KEY值。
二、在生产者集群上设置一个定时任务(根据自身分布式事务流程执行的时间设定)。
这里是用COUNT来避免刚刚发送消息就开始判断sysFlag的情况,相当于预留了一点rabbitmq的消息发送延迟时间,这个也可以使用CREATE_TIME来代替,比如判断记录列表中CREATE_TIME在当前时间前3s以前,未成功发送的消息
这只能算其中一种,还有一些手动从生产者和消费者两端进行数据库查询的方法,有兴趣的自己可以去了解