Spring @Transactional踩坑记

Posted Secondworld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring @Transactional踩坑记相关的知识,希望对你有一定的参考价值。

@Transactional踩坑记

总述

? Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务控制。 然而看起来越是简单的东西,背后的实现可能存在很多默认规则和限制。而对于使用者如果只知道使用该注解,而不去考虑背后的限制,就可能事与愿违,到时候线上出了问题可能根本都找不出啥原因。


踩坑记

1. 多数据源

事务不生效

背景介绍

? 由于数据量比较大,项目的初始设计是分库分表的。于是在配置文件中就存在多个数据源配置。大致的配置类似下面:

<!-- 数据源A和事务配置 -->
<bean id="dataSourceA" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
    ....
</bean>
 <bean id="dataSourceTxManagerA"
       class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSourceA" />
</bean>
<!-- mybatis自动扫描生成Dao类的代码 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="annotationClass" value="com.rampage.mybatis.annotation.mapperDao" />
  <property name="nameGenerator" ref="sourceANameGenerator" />
  <property name="sqlSessionFactoryBeanName" value="sourceAsqlSessionFactory" />
  <property name="basePackage" value="com.rampage" />
</bean> 
<!-- 自定义的Dao名称生成器,prefix属性指定在bean名称前加上对应的前缀生成Dao -->
<bean id="sourceANameGenerator" class="com.rampage.mybatis.dao.namegenerator.MyNameGenerator">
  <property name="prefix" value="sourceA" />
</bean>
 <bean id="sourceAsqlSessionFactory" class="org.mybatis.spring.PathSqlSessionFactoryBean">
   <property name="dataSource" ref="dataSourceA" />
</bean>

<!-- 数据B和事务配置 -->
<bean id="dataSourceB" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
    ....
</bean>
 <bean id="dataSourceTxManagerB"
       class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSourceB" />
</bean>

? 但是在实际部署的时候,因为是单机部署的,多个数据源实际上对应的是同一个库,不存在分布式事务的问题。所以在代码编写的时候,直接通过在@Transactional注解来实现事务。具体代码样例大致如下:

@Service
public class UserService {
  @Resource("sourceBUserDao")       // 其实这时候Dao对应的是sourceB
  private UserDao userDao;
  
  @Transactional
  public void update(User user) {
    userDao.update(user);
    // Other db operations
    ...
  }
}

? 这中写法的代码一直在线上运行了一两年,没有出过啥问题.....反而是我在做一个需求的时候,考虑到@Transactional注解里面的 数据库操作,如果没有同时成功或者失败的话,数据会出现混乱的情况。于是自己测试了一下,开启了这段踩坑之旅.....

原因分析:

? 开始在网上搜了一下Transactional注解不支持多数据源, 于是我当时把所有数据库操作都采用sourceB作为前缀的Dao进行操作。结果测试一遍发现还是没有事务效果。没有什么是源码解决不了的,于是就开始debug源码,发现最终启动的事务管理器竟然是dataSourceTxManagerA。 难道和事务管理器声明的顺序有关?于是我调整了下xml配置文件中,事务管理器声明的顺序,发现事务生效了,因此得证。

? 具体来说原因有以下两点:

  • @Transactional注解不支持多数据源的情况
  • 如果存在多个数据源且未指定具体的事务管理器,那么实际上启用的事务管理器是最先在配置文件中指定的(即先加载的)
解决办法:

? 对于多数据下的事务解决办法如下:

  • @Transactional注解添加的方法内,数据库更新操作统一使用一个数据源下的Dao,不要出现多个数据源下的Dao的情况
  • 统一了方法内的数据源之后,可以通过@Transactional(transactionManager = "dataSourceTxManagerB")显示指定起作用的事务管理器,或者在xml中调节事务管理器的声明顺序

死循环问题

? 这个问题其实也是多数据源导致的,只是更难分析原因。具体场景是:假设我的货仓里有1000个货物,我现在要给用户发货。每批次只能发100个。我的货物有一个字段来标识是否已经发过了,对于已经发过的货不能重新发(否则只能哭晕在厕所)!代码的实现是外层有一个while(true)循环去扫描是否还有未发过的货物,而发货作为整体的一个事务,具体代码如下:

@Transactional
public void deliverGoods(List<Goods> goodsList) {   // 传入的参数是前面循环查出来的未发货的100个货物,作为一个批次统一发货
  updateBatchId(goodsList, batchId);    // 更新同批次货物的批次号字段
  // do other things
  updateGoodsStatusByBatchId(batchId, delivered);   // 根据前面更新的批次号取修改数据库相关货物的发送状态为已发送
}

? 从整体上来看,这段代码逻辑上没有任何问题。实际运行的时候却发现出现了死循环。还好测试及时发现,没有最终上线。那么具体原因是咋样的呢?

? 出现这个问题的时候,配置文件的配置还是同前面一个问题一样的配置。即实际上@Transactional注解默认起作用的事务是针对dataSourceA的。然后跟进updateBatchId方法,发现其最终调用的方法采用的Dao是sourceA为前缀的Dao,而updateGoodsStatusByBatchId方法最终调用的Dao是sourceB为前缀的Dao。细细分析,我终于知道为啥了 ??

  • 发货方法最终起作用的事务是针对sourceA的, 也就是updateBatchId方法实际上作为一个事务,他是要在方法执行完成之后才提交的

  • oracle默认的事务隔离级别是READ_COMMITTED, 所以在updateGoodsStatusByBatchId方法去更新的时候

其实还读取不到对应批次号的记录,也就不会做更新

? 解决办法这里就不说了,最终还是同前面一个问题,或者更新的时候根据货物列表去更新。


2. 内部调用

? 内部调用不生效的问题其实大部分大家都知道。举一个简单的例子:

? 假设我有一个类的定义如下:

@Service
public class UserService {
  
  public void updateUser(User user){
     // do somothing
    updateWithTransaction(user);
  }
  
  @Transactional
  public void updateWithTransaction(User user) {
    
  }
}

@Service
public class BusinessService {
  @Autowired
  private UserService userService;
  
  public void doUserUpdate(User user) {
    // do somothing
    userService.updateUser(user);
  }
}

? 这种情况下大家都知道事务最终是不会生效的。因为对于updateWithTransaction方法是通过内部调用的,这时候@Transactional注解压根就不会生效。但是有时候情况并不这么明显,考虑下面的代码:

@Service
public class UserService extends AbstractUserService {
  
  public void updateUser(User user){
     // do somothing
    updateWithTransaction(user);
  }

}

public abstract class AbstractUserService {
  protected abstract void updateUser(User user);
  
   @Transactional
  public void updateWithTransaction(User user) {
    // do update
  }
}

@Service
public class BusinessService {
  
  public void doUserUpdate(User user) {
    // do somothing
    AbstractUserService userService = getUserService();  // 假设最终得到是UserService类的实例
    userService.updateUser(user);
  }
}

? 这段代码初一分析,最终调用的updateUser方法是UserService的方法, 然后调用的updateWithTransaction是属于AbstractUserService类的。 好像是调用的不是同一个类的方法,按道理事务应该是可以生效的。其实并没有..... 原因其实还是是内部调用。其实这种场景我也是在项目中发现的(坑太多),当时的代码比这个复杂的多,Abstract类包含了一堆可以被子类重写的方法。原来的代码大致如下:

public class AbstractService {
  
  // 被外部调用的方法
  public void outMethod() {
    if (A) {
      transactionalMethod1();
    } else if (B) {
      transactionalMethod2();
    } else {
      transactionalMethod3();
    }  
  }
  
  @Transactional
  public void transactionalMethod1() {
    // do something
  }
  
   @Transactional
  public void transactionalMethod2() {
    // do something
  }
  
   @Transactional
  public void transactionalMethod3() {
    // do something
  }
}

? 其中三个事务方法都可能被子类重写,修改必须兼容老代码。思考了兼容和接口改造的方式,我最终实现如下:

public class AbstractService implements TransactionIntf {
  
  @Autowired
  private TransactionService transactionService;
  
  public void outMethod() {
     transactionService.setTransactionIntf(this);  // 最终数据库服务类注册为当前实现类
     transactionService.processIntransaction(); // 调用数据库操作
  }
  
  @Override
  public void transactionalMethod1() {
    // do something
  }
  
   @Override
  public void transactionalMethod2() {
    // do something
  }
  
   @Override
  public void transactionalMethod3() {
    // do something
  }
}

public interface TransactionIntf {
  void transactionalMethod1();
  void transactionalMethod2();
  void transactionalMethod3();
}

@Service
public class TransactionService {
  // 定义局部线程变量,存储对应的服务
  ThreadLocal<TransactionIntf> serviceLocal = new ThreadLocal<TransactionIntf>();
  
  @Transactional  // 在此处加注解
  public void processIntransaction() {
    try {
        if (A) {
          serviceLocal.get().transactionalMethod1();
        } else if (B) {
          serviceLocal.get().transactionalMethod2();
        } else {
          serviceLocal.get().transactionalMethod3();
        }  
    } finally {
      // 最终在本地线程局部变量移除
      serviceLocal.remove();
    }
  }
  
  // 设置服务到本地线程局部变量
  public void setTransactionIntf(TransactionIntf service) {
    this.serviceLocal.set(service);
  }
}

填坑总结

? 下面直接给出网站上关于@Transactional使用的注意点:

  • @Transactional annotations only work on public methods. If you have a private or protected method with this annotation there’s no (easy) way for Spring AOP to see the annotation. It doesn’t go crazy trying to find them so make sure all of your annotated methods are public.
  • Transaction boundaries are only created when properly annotated (see above) methods are called through a Spring proxy. This means that you need to call your annotated method directly through an @Autowired bean or the transaction will never start. If you call a method on an @Autowired bean that isn’t annotated which itself calls a public method that is annotated YOUR ANNOTATION IS IGNORED. This is because Spring AOP is only checking annotations when it first enters the @Autowired code.
  • Never blindly trust that your @Transactional annotations are actually creating transaction boundaries. When in doubt test whether a transaction really is active (see below)

? 另外附上验证是否是否开启的工具类源码(我只是搬运工):

class TransactionTestUtils {
  private static final boolean transactionDebugging = true;
  private static final boolean verboseTransactionDebugging = true;

  public static void showTransactionStatus(String message) {
      System.out.println(((transactionActive()) ? "[+] " : "[-] ") + message);
  }

  // Some guidance from: http://java.dzone.com/articles/monitoring-declarative-transac?page=0,1
  public static boolean transactionActive() {
      try {
          ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
          Class tsmClass = contextClassLoader.loadClass("org.springframework.transaction.support.TransactionSynchronizationManager");
          Boolean isActive = (Boolean) tsmClass.getMethod("isActualTransactionActive", null).invoke(null, null);

          return isActive;
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      } catch (IllegalArgumentException e) {
          e.printStackTrace();
      } catch (SecurityException e) {
          e.printStackTrace();
      } catch (IllegalAccessException e) {
          e.printStackTrace();
      } catch (InvocationTargetException e) {
          e.printStackTrace();
      } catch (NoSuchMethodException e) {
          e.printStackTrace();
      }

      // If we got here it means there was an exception
      throw new IllegalStateException("ServerUtils.transactionActive was unable to complete properly");
  }

  public static void transactionRequired(String message) {
      // Are we debugging transactions?
      if (!transactionDebugging) {
          // No, just return
          return;
      }

      // Are we doing verbose transaction debugging?
      if (verboseTransactionDebugging) {
          // Yes, show the status before we get to the possibility of throwing an exception
          showTransactionStatus(message);
      }

      // Is there a transaction active?
      if (!transactionActive()) {
          // No, throw an exception
          throw new IllegalStateException("Transaction required but not active [" + message + "]");
      }
  }
}

参考链接

http://blog.timmattison.com/archives/2012/04/19/tips-for-debugging-springs-transactional-annotation/

以上是关于Spring @Transactional踩坑记的主要内容,如果未能解决你的问题,请参考以下文章

踩坑记:根据类型获取Spring容器中的Bean

踩坑记:根据类型获取Spring容器中的Bean

Spring 的 BeanUtils 踩坑记,你是不是遇到过这些问题?

Spring 的 BeanUtils 踩坑记,你是不是遇到过这些问题?

踩坑记——使用slf4j+logback记录日志

踩坑记SpringBoot使用Logback