spring 发布与订阅 事务问题

问题描述

这个问题本身是一个伪命题,因为spring的事务,也是基于ThreadLocal设计的;不同线程间,无法处理事务

有时候,我们为了解决部分性能问题,采用了spring 的ApplicationListener【发布与订阅】,对原有方法进行解耦,分离弱关系处理逻辑。 当采用异步监听的时候,如果涉及到事务的时候,我们的处理方式就会出现问题。

发布与订阅-异步

在使用 【发布与订阅】时, 我们可以采用同步或者异步

/**
 * 
 * @desc XX数据处理 监听处理
 * @create 2022-01-04 09:10
 **/
@Component
@Slf4j
public class XXXListener implements ApplicationListener {

    /**
      * 在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 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 - 在事务提交前执行

//代码原理,可参看
https://juejin.cn/post/7011685509567086606

总结

现在我们做一个总结,如果你遇到这样的业务,操作B需要在操作A事务提交后去执行,那么TransactionalEventListener是一个很好地选择。这里需要特别注意的一个点就是:当B操作有数据改动并持久化时,并希望在A操作的AFTER_COMMIT阶段执行,那么你需要将B事务声明为PROPAGATION_REQUIRES_NEW。这是因为A操作的事务提交后,事务资源可能仍然处于激活状态,如果B操作使用默认的PROPAGATION_REQUIRED的话,会直接加入到操作A的事务中,但是这时候事务A是不会再提交,结果就是程序写了修改和保存逻辑,但是数据库数据却没有发生变化,解决方案就是要明确的将操作B的事务设为PROPAGATION_REQUIRES_NEW

你可能感兴趣的:(java基础,spring,mybatis,java)