专栏首页Java开发者杂谈Spring @Transactional踩坑记

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/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • java如何获取一个对象的大小

    When---什么时候需要知道对象的内存大小 在内存足够用的情况下我们是不需要考虑java中一个对象所占内存大小的。但当一个系统的内存有限,或者某块程序代码允许...

    SecondWorld
  • JDK1.7新特性(1):Switch和数字

    Switch jdk1.7的switch语句增加了对字符串类型的支持。其实现的原理是通过字符串的hash值来比较的,代码示例如下: 1 String name...

    SecondWorld
  • JDK1.7新特性(4):java语言动态性之反射API

    直接通过一个代码示例来熟悉java中通过反射来对构造函数/域以及方法处理的相关API: 1 package com.rampage.jdk7.chapter...

    SecondWorld
  • java编程思想第四版第九章习题

    用户7798898
  • 10(01)总结形式参数,包,修饰符,内部类

    类,抽象类,接口的综合小练习 /* 教练和运动员案例(学生分析然后讲解) 乒乓球运动员和篮球运动员。 乒乓球教练和篮球教练。 为了出国交流,跟乒乓球相关...

    Java帮帮
  • 设计模式~简单工厂模式

    从上图可以看出,简单工厂模式涉及到工厂角色、抽象产品角色以及具体产品角色等三个角色:

    Vincent-yuan
  • 去吧!设计模式之桥接模式

    张风捷特烈
  • 用命令模式实现撤销与恢复 命令模式定义撤销与重做功能就此实现。整个过程中,最关键部分是命令对象的封装以及控制类与具体工厂类耦合的解除。

    通过 ICommand 接口,实现了控制类与调用者的解耦。 * 下面通过一个简单的实例来详细说明这种解耦以恢复撤销是如何实现。 假定有一个风扇,当前有...

    用户2434869
  • 【java设计模式】之 工厂模式

    使用场景:在任何需要生成复杂对象的地方,都可以使用工厂方法模式。 直接用new可以完成的不需要用工厂模式

    用户5640963
  • 23种设计模式详解(四)

    南风

扫码关注云+社区

领取腾讯云代金券