在分布式的场景下,我们经常能遇到事务一致性问题,如何去保证事务的一致性,是分布式场景下不可避免的一个重要话题。
我们很难去做到事务的强一致性(有方案),因此会退一步去保证事务的最终一致性。
如何去保证事务的最终一致性呢?我们可以利用RocketMQ的事务消息来保证事务的最终一致性。
RocketMQ如何保证事务的一致性呢?原来,RocketMQ通过将应用本地事务和发送消息操作定义到全局事务中,要么同时成功,要么同时失败。这样就保证了本地事务和消息发送的一致性:
事务消息的原理可以参考事务消息以及收发事务消息,接下来我们需要将其应用到我们的SpringBoot应用中去。
首先引入RocketMQ的starter依赖(gradle):
//省略其他依赖
implementation 'org.apache.rocketmq:rocketmq-spring-boot-stater:2.2.0'
rocketmq-spring-boot-stater提供了Spring环境下的RocketMQ的集成,它使RocketMQ使用起来更加的方便。不同于上述的收发事务消息里的实现,我们可以通过实现RocketMQLocalTransactionListener来简化我们的操作。然后可以可以通过TransactionMQProducer#sendMessageInTransaction来发送消息
通过上述的操作,最终所有的回调都会进入RocketMQLocalTransactionListener#executeLocalTransaction这个方法。然而这样的话,我们如何去做不同的业务,然后提交事务呢?我们注意到sendMessageInTransaction方法的第二个参数是一直透传到executeLocalTransaction方法的第二个参数的,这个参数的作用RocketMQ的解释是与本地事务执行器一起使用的参数,因此我们可以利用这个参数来实现业务操作。
我们设计一个本地事务执行器,将这个执行器作为sendMessageInTransaction的第二个参数,然后就可以在executeLocalTransaction方法里面执行执行器的操作了:
public interface LocalTransactionExecutor<R> {
/**
* 执行本地业务并提交事务
* @author zhaosheng
*/
void execute();
/**
* 返回结果或抛异常
* @author zhaosheng
*/
R getResult() throws Exception;
}
然后提供一个默认的实现:
public class DefaultLocalTransactionExecutor<R> implements LocalTransactionExecutor<R> {
private MyException myException;
private Exception exception;
private R result;
private Runnable runnable;
private Supplier<R> supplier;
public DefaultLocalTransactionExecutor(Runnable runnable) {
this.runnable = runnable;
}
public DefaultLocalTransactionExecutor(Supplier<R> supplier) {
this.supplier = supplier;
}
@Override
public final void execute() {
try {
if (null != runnable) {
runnable.run();
} else {
result = supplier.get();
}
} catch (MyException m) {
this.slcException = m;
} catch (Exception e) {
this.exception = e;
}
}
@Override
public final R getResult() throws Exception {
if (null != myException) {
throw myException;
}
if (null != exception) {
throw exception;
}
return result;
}
}
提供一个工厂类来获取具体的默认实现:
public class LocalTransactionExecutorFactory {
//无返回的执行器
public static LocalTransactionExecutor<Void> run(Runnable runnable) {
return new DefaultLocalTransactionExecutor<>(runnable);
}
//有返回的执行器
public static <R> LocalTransactionExecutor<R> get(Supplier<R> supplier) {
return new DefaultLocalTransactionExecutor<>(supplier);
}
}
以上是我们设计的去在executeLocalTransaction方法下执行业务的一个执行器,下面举个例子来演示如何使用:
就拿经典的银行转账的场景来示范,假如A要向B转账100元,A与B的账户处在不同的银行:
1、A的账户扣减100元
2、A通知B增加100元
3、B增加100元
这时A需要操作的业务如下:
@Service
public class AService {
@Transactional(rollbackFor = Exception.class)
public void decr100(String account) {
System.out.println(account + " 减少100元");
}
}
发送消息的逻辑即可如下:
@Service
public class TestSendMsg {
@Autowired
private TransactionMQProducer transactionMQProducer;
@Autowired
private AService aService;
public void send() throws Exception {
Message message = new Message();
message.setTopic("topic1");
message.setBody("B增加100元".getBytes());
LocalTransactionExecutor<Void> aServiceExecutor = LocalTransactionExecutorFactory.run(() -> aService.decr100("A"));
transactionMQProducer.sendMessageInTransaction(message, aServiceExecutor);
aServiceExecutor.getResult();
}
}
这时我们在executeLocalTransaction方法里面执行业务就好了,但是执行完业务如何在回查的时候知道事务是否已经提交了呢。这时我们需要引入一张本地事务记录表,来提供事务是否完成的查询的作用,当我们做业务的时候,同时向这张表插一条数据,然后保证他们在同一个事务里就好了。
@Service
public class LocalTransactionService {
public void insert(String txId) {
System.out.println("保存txId");
}
public boolean exist(String txId) {
System.out.println("查询事务是否提交");
return true;
}
}
具体的RocketMQLocalTransactionListener 实现如下:
@Slf4j
@Component
public class MyRocketMQLocalTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private LocalTransactionRecordService localTransactionRecordService;
@Override
@Transactional
//这里加事务的作用在于我们需要保证
// localTransactionRecordService.insert(txId)和((DefaultLocalTransactionExecutor) arg).execute()这两行逻辑在一个事务里
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//我们需要在这里操作们的业务并且提交事务,然后返回相应的状态来通知RocketMQ推送or丢弃发送的消息
if (arg instanceof DefaultLocalTransactionExecutor) {
//我们可以利用RocketMQ的transactionId来记录本次的事务,
//也可以自己定义事务ID,通过ThreadLocal传过来
String txId = getTxId(msg);
try {
localTransactionRecordService.insert(txId);
((DefaultLocalTransactionExecutor) arg).execute();
}catch (Exception e) {
log.error("executeLocalTransaction failed msg:", e);
return RocketMQLocalTransactionState.ROLLBACK;
}
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
//RocketMQ通过这里来回查上面的事务的状态,默认1min回查一次,一共回查15次,
//一般而言,这个方法是不会被回调的,因为上面的方法里我们已经返回了状态
String txId = getTxId(msg);
boolean exist = localTransactionRecordService.exist(txId);
//这里查不到的时候返回UNKNOW在于,有可能事务还没有提交,回查就开始了
return exist ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.UNKNOWN;
}
private String getTxId(Message msg) {
//一般不会为null
Object txId = msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
if (null == txId) {
return null;
}
return txId.toString();
}
}
以上就是一个完整的案例了,这种方式简便,可以保证消息和本地事务的一致性,然而,这种方式的最大问题在于,他要预发送消息,很多情况下我们的消息体可能需要经过一系列的业务计算才能得出,此时就会导致两个问题:
1、重复查询/计算
第一次查询/计算是为了得到消息体的内容,第二次查询/计算是为了执行本地事务
2、数据不一致
第一次查询/计算得出消息体,再去执行本地业务的时候,数据被其他流程改变了,也就是说此时执行业务的时候,数据不是之前获取消息体时的数据。