在rocketMQ中生产者有三种角色 NormalProducer(普通)、OrderProducer(顺序)、TransactionProducer(事务)
根据名字大概可以看出各个代表着什么作用,我们这里用 TransactionProducer(事务)来解决问题。
先举个列子来说明下我们解决方案的设计方式吧:最经典的莫过于银行转账了,网上到处都有,时序图如下
我们的解决方案与这个大致没什么区别。
下面贴一下测试代码:
/**
* @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;
}
}
/**
* @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;
}
}
/**
* @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();
}
}
/**
* @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<MessageExt> 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.0.6之前的版本这样写就可以了,但是之后的版本被关于事务回查这个借口被阉割了,不会在进行事务回查操作。没有回查机制的话如上面那个时序图所示,我们在第五步向MQ发送消息如果失败的话,会造成A银行扣款成功而B银行收款未成功的数据不一致的情况,所以,关于事务回查这块的需要由我们自己来设计实现一下,保证数据的一致性。
事务回查机制
由于开源版本的rocketMQ3.0.6之后的版本被阉割了事务会回查机制,所以这部分的实现需要自己来实现。
梳理一下上图的流程:
正常的流程:A银行产生一条转账消息发往MQ(操作t1、t2表),MQ接收到的消息此时对B银行不可见,当A银行的本地事务提交后,再向MQ发送一条确认事务提交的消息,此时MQ接收到的消息对B银行可见,B银行来消费这条消息,完成B银行的转账操作(操作t3、t5表)。
异常的流程:如果A银行在第二阶段发送确认消息的时候没有发送成功,导致B银行不能消费到消息,这时候就需要用到t5和t2表来实现回查。t5表保存的转账日志肯定都是A银行已经操作成功的,我们需要将t5表一段时间内的数据发送给A银行来跟t2表做一个对账业务,发送的可以使两边共有的id这样的字段(目的是为了找出这一段时间内A银行确认消息发送失败的数据,然后再次向MQ发送确认消息).这一段时间怎么来确定呢,t4这时候派上用场了,B银行定时扫描t5表的定时任务每次启动的时候,取出存在t4表的time字段的时间命名为oldTime,然后将当前的系统时间更新到t4表的time,然后在t5表中取出大于oldTime时间的数据发送给A系统,既然取出数据是根据time判断的,那么表t2、t5肯定得有一个updateTime字段在操作数据的时候维护进去。