Spring @TransactionalEventListener事件中的事务

在业务逻辑中,总会有一些耗时的操作,比如注册用户的时候,发送email或者短信;支付的时候,需要调用第三方支付;又或者采用了微服务,需要内部系统之间互相调用;
通常一些post操作,都会包在事务中,那么这些网络请求啊等,能放在事务中么?

当然不能!
⚠️
耗时操作不能放在事务中;http请求、rpc请求等本身不支持事务的代码也禁止放在事务中
举个🌰,在注册用户的事务中,加入了一段发送短信验证码的http请求,带来的问题有哪些呢?
  1. 调用外部缓慢,事务占有锁的时间过长,影响其他查询、更新
  1. 短信发送成功,事务提交失败,用户注册没有成功短信发出去了。http的请求不支持事务也不能回滚。
在Spring中,提供了TransactionSynchronizationManager 和更简单易用的@TransactionalEventListener
其原理,就是通过AOP,在提交事务的代码中,加了一个callback,在提交前、提交后、事务完成等时机,执行事件。
例如以下简化代码:
@Override @Transactional(rollbackFor = Throwable.class) public void save(User user) { userRepository.save(user); applicationContext.publishEvent(DomainChangeEvent.create(user)); } @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void onUserRegisterEvent(DomainChangeEvent<User> event) { messageService.sendSms("1234"); }
注意:整个过程是同步的,也即上述的事件处理是同步方法,在save方法的事务提交后在当前线程继续执行的。
这段代码,解决了上述问题2,一定会在用户注册成功后才会发送短信。对于问题1,可以加@Async注解或者使用线程池,实现耗时的接口。

Case closed了?no no no! 这段代码引入了一个新的问题:
假设,注册用户的事务提交成功了,接下来发送短信,发送短信失败了呢?事务已经提交了,验证码没有发出去。
这个就涉及到一致性的问题了,可以参考
分布式系统之一:分布式事务和一致性
Reliable Domain Events 。注意:Spring的事件本身不保证一致性。
这里提一下思路吧:
  1. 补偿策略保证最终一致性
    1. 例如注册用户的场景,一般会在页面提供按钮,收不到验证码,用户可以手动刷新,问题就解决了。某些不涉及用户的场景,比如管理后台进行某个操作后,发送短信,可以加一些状态,通过定时任务查询未完成的事件
  1. 使用RocketMQ的事务消息,这时候,就没有必要使用spring的事件了
  1. 二阶段提交等一致性方案

💡
spring事件也可用于对操作进行解耦,当某些操作不适合放在当前方法时,通过EventListener或者TransactionalEventListener可以进行解耦。
 
本文作者: Song 本文链接: https://www.jiangyuesong.me 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

© Song 2015 - 2024