问题描述
这个问题本身是一个伪命题,因为spring的事务,也是基于ThreadLocal设计的;不同线程间,无法处理事务】
有时候,我们为了解决部分性能问题,采用了spring 的ApplicationListener【发布与订阅】,对原有方法进行解耦,分离弱关系处理逻辑。 当采用异步监听的时候,如果涉及到事务的时候,我们的处理方式就会出现问题。
发布与订阅-异步
在使用 【发布与订阅】时, 我们可以采用同步或者异步
/**
*
* @desc XX数据处理 监听处理
* @create 2022-01-04 09:10
**/
@Component
@Slf4j
public class XXXListener implements ApplicationListener<XYXEvent> {
/**
* 在onApplicationEvent上添加注解Async,则进行异步处理;否则是同步处理
*
*/
@Override
@Async("xxxxasyncThreadPool")
public void onApplicationEvent(XYXEvent event) {
StopWatch sw = new StopWatch("WorkOrderEvent");
sw.start();
log.info("XXXListener,ID:{}", event.getId());
//do-something
//1、根据主表的物理ID获取对应数据
// 由于前面的session的事务还没有提交,查不到对应数据;
//2、根据数据建立中间表关联数据入库
sw.stop();
log.info("XXXListener,TotalTimeMillis:{}", sw.getTotalTimeMillis());
}
}
问题场景:
@Resource
private WebApplicationContext webApplicationContext;
//伪代码
@Transactional(rollbackFor = {Exception.class})
public Boolean edit(AuditStandardSaveReq req) {
//A、保存主表数据
this.save(entity);
//B、发送邮件
XYXEvent event1 = new XYXEvent(this, req);
webApplicationContext.publishEvent(event1);
}
我们想解决2个事务的一致性问题:
A保存成功,B发送邮件,
如果监听器Listener 中使用 Async ,会存在 A保存失败,B 却把邮件发送出去的问题。
原因:spring 的事务是建立在同一个session中间的,并且是在同一线程副本下的一致性。
异步处理,相当于要新开一个线程处理。
解决方案
方案1:异步变同步-多事务合并到同一个事务中
去掉 在ApplicationListener方法onApplicationEvent上的注解Async;这个时候,是与调用方的线程是同一线程。
缺点:损失了性能
方案2:去掉edit方法的事务注解
缺点:无法保证异步方法的事务 与 edit方法的事务的一致性【相当于方法内各事务都是自动提交,不严谨】
方案3:显式使用session,解决异步线程事务问题
/**
* org.mybatis.spring.SqlSessionTemplate
*
* 定义SqlContext,方便获取session
*/
@Component
public class SqlContext {
@Resource
private SqlSessionTemplate sqlSessionTemplate;
public SqlSession getSqlSession(){
SqlSessionFactory sqlSessionFactory = sqlSessionTemplate.getSqlSessionFactory();
return sqlSessionFactory.openSession();
//自动提交事务
//return sqlSessionFactory.openSession(true);
}
}
。。。
try{
SqlSession sqlSession = sqlContext.getSqlSession();
Connection connection = sqlSession.getConnection();
connection.setAutoCommit(false);
connection.commit();
}
catch (Exception e){
try {
connection.rollback();
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
log.info("error",e);
throw new XXXXXException(500,"xxxxx");
}finally {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
。。。
不推荐:使用sqlSession控制手动提交事务,
方案4:高级解决方案
Spring事务事件控制,解决业务异步操作
1、使用注解@TransactionalEventListener ,并且支持 多线程异步处理
2、使用TransactionSynchronizationManager 和 TransactionSynchronizationAdapter
3、使用 around 建议连接适配器和注释
4.1 使用@TransactionalEventListener
*使用场景
我们要完成2件事情,但是第2件事情需要等待第1件事情完成,才可以执行。
另外第2件事情,可能远程调用(发邮件或者其他操作)。
下面的注解,可以完美的解决这样的场景。
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT, classes = OrderExtSaveEvent.class)
Tips
Listener中的处理,不支持事务。所以一般是远程调用,然别人去控制这个。
// ApplicationEventPublisher - 发送Spring内部事件
@AllArgsConstructor
@Data
public class OrderExtSaveEvent {
private final String email;
}
@Component
public class OrderExtSaveEventListener {
private final XXXXXConfig focusGroupConfig;
private final XXXXXService xXXXXService;
/**
* 当OrderExt保存成功,则监听并发送邮件
* Async 可支持异步线程处理,提升服务性能
* phase = TransactionPhase.AFTER_COMMIT :默认 阶段设置,前面的事务提交后,进行相关操作
*
*
*/
@Async
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT, classes = OrderExtSaveEvent.class)
//@EventListener
public void processUserCreatedEvent(OrderExtSaveEvent event) {
List<Msg> msgList = Lists.newArrayList();
Msg msg = new Msg();
msgList.add(msg);
msg.setReceiveMail(event.getEmail());
String auditStatus = "x";
xXXXXService.sendToMessage(msgList,auditStatus);
}
}
//某服务的方法
@Resource
private WebApplicationContext webApplicationContext;
//@Autowired
//private ApplicationEventPublisher applicationEventPublisher;
@Override
@Transactional(rollbackFor = Exception.class)
public void insertEntity() throws Exception{
saveData();
String email = "13613015502";
OrderExtSaveEvent event = new OrderExtSaveEvent(email);
webApplicationContext.publishEvent(event);
}
@Transactional(rollbackFor = Exception.class)
public void saveData()throws Exception{
try{
OrderExt orderExt = new OrderExt();
orderExt.setId(IdWorker.get32UUID());
orderExt.setOrderId("orderExt-test");
orderExt.setCreatedTime(new Date());
this.insert(orderExt);
//故意抛出异常
throw new Exception("xxxx");
}catch (Exception e){
throw e;
}
}
//单元测试
@Autowired
private XXXOrderExtService xxOrderExtService;
@Test
public void test7() throws Exception {
xxOrderExtService.insertEntity();
}
//A 执行异常,不会执行B
4.1 @TransactionalEventListener 与 @EventListener 差异
TransactionalEventListener是对EventListener的增强,被注解的方法可以在事务的不同阶段去触发执行,如果事件未在激活的事务中发布,除非显式设置了 fallbackExecution() 标志为true,否则该事件将被丢弃;如果事务正在运行,则根据其 TransactionPhase 处理该事件。
Notice:你可以通过注解@Order去排序所有的Listener,确保他们按自己的设定的预期顺序执行。
我们先看看TransactionPhase有哪些:
AFTER_COMMIT - 默认设置,在事务提交后执行
AFTER_ROLLBACK - 在事务回滚后执行
AFTER_COMPLETION - 在事务完成后执行(不管是否成功)
BEFORE_COMMIT - 在事务提交前执行
//代码原理,可参看
总结
现在我们做一个总结,如果你遇到这样的业务,操作B需要在操作A事务提交后去执行,那么TransactionalEventListener是一个很好地选择。这里需要特别注意的一个点就是:当B操作有数据改动并持久化时,并希望在A操作的AFTER_COMMIT阶段执行,那么你需要将B事务声明为PROPAGATION_REQUIRES_NEW。这是因为A操作的事务提交后,事务资源可能仍然处于激活状态,如果B操作使用默认的PROPAGATION_REQUIRED的话,会直接加入到操作A的事务中,但是这时候事务A是不会再提交,结果就是程序写了修改和保存逻辑,但是数据库数据却没有发生变化,解决方案就是要明确的将操作B的事务设为PROPAGATION_REQUIRES_NEW。