✅为啥不要在事务中做外部调用?

典型回答

很多时候,我们经常会在数据库事务的过程中做一些网络调用,比如Redis的更新、MQ的发送,RPC接口的调用等,其实这是非常不建议的。

@Transactional
public void order(OrderDTO orderDTO){
  //本地数据库操作
    orderServive.createOrder(orderDTO);
  //远程外部调用
    mqService.send(orderDTO);
}

这么做会带来几个问题:

1、增加事务持续时间:外部的网络调用会显著增加事务的执行时间,特别是网络延迟或外部服务响应慢的情况下。事务持续时间的增加会占用数据库资源更长时间,如锁定资源,这会影响数据库的并发处理能力和整体性能。

本来你操作完数据库就可以提交了,事务和数据库链接也就释放了。但是因为有外部调用,那么就会导致这个事务的时间边长,链接释放时间也变长。

2、死锁和资源竞争:长时间持有事务期间的锁资源会增加死锁的可能性,尤其是当系统负载高时。因为一旦在事务中开启了锁,比如select for update,需要事务提交或回滚后才能释放锁,这就会导致更多的锁冲突。

3、提高事务失败的风险:外部调用增加了事务失败的可能性。如果外部服务调用失败或超时,可能需要回滚事务,这不仅增加了复杂性,还可能导致数据不一致的问题。

这么做非常不好!

比如说在一些场景中,举个例子在用户下单过程,我们要在下单操作成功后发个MQ,但是因为你做了个事务,消息发送失败后,异常被捕获到,就会导致数据库回滚。大家想想,用户下单都成功了,就因为发MQ失败了,就把订单操作给回滚了,这根本就不合理啊!所以,这就会导致很多不在核心链路上的操作失败而导致核心链路回滚。

4、难以保证原子性事务的一个关键属性是原子性,意味着事务内的操作要么全部成功,要么全部失败。当事务中包含外部调用时,很难保证事务和外部系统之间的原子性,因为外部系统的操作可能不易于回滚。

有这样一种情况,当我们调外部接口的时候,因为网络延迟, 我们拿到了一个timeoutException,这时候我们本地事务回滚了。

但是,远程操作虽然超时了,但是不代表他没执行,所以很可能他已经已经成功了!那么这就导致了数据的不一致!

所以,为了避免这些问题,通常建议的做法是:

  • 将事务和外部调用分离:尽可能在事务提交或完成后,再执行外部调用。这样可以减少事务持续时间,降低失败风险,并保持数据库事务的简洁性和高效性。
  • 使用补偿事务:如果外部调用必须与数据库操作紧密集成,可以设计补偿事务来处理失败情况。补偿事务是一种逻辑上的回滚操作,用于撤销已经执行的外部操作。
  • 异步处理:考虑使用异步消息队列或事件驱动架构来处理需要外部调用的操作。这样可以在不阻塞数据库事务的情况下,异步地执行外部服务调用。

原文: https://www.yuque.com/hollis666/xkm7k3/gxnzfaxighqtaxod