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 条评论
登录 后参与评论

相关文章

来自专栏阿杜的世界

在Spring Boot项目中使用Spock框架

Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目中使用该框架写优雅、高效以及DSL...

1051
来自专栏纯洁的微笑

springboot(六):如何优雅的使用mybatis

这两天启动了一个新项目因为项目组成员一直都使用的是mybatis,虽然个人比较喜欢jpa这种极简的模式,但是为了项目保持统一性技术选型还是定了 mybatis。...

34812
来自专栏一个会写诗的程序员的博客

6.3 Spring Boot集成mongodb开发小结

本章我们通过SpringBoot集成mongodb,Java,Kotlin开发一个极简社区文章博客系统。

923
来自专栏lgp20151222

Java规则引擎drools:drt动态生成规则并附上具体项目逻辑

由于本人的码云太多太乱了,于是决定一个一个的整合到一个springboot项目里面。

1446
来自专栏yukong的小专栏

【ssm个人博客项目实战05】easy ui datagrid实现数据的分页显示1、数据格式准备工作2、业务层实现3、控制层实现4、前端视图处理

前面一节 我们已经实现博客类别的dao层的实现,其中特别讲解了博客类别的分页的实现,那么现在我们实现了后台的分页,那么前台分页怎么显示呢,这时候我们用到了eas...

982
来自专栏青枫的专栏

day52_BOS项目_04

第一步:导入pinyin4j-2.5.0.jar包,拷贝PinYin4jUtils.java工具类至utils包中 第二步:测试类代码如下:

512
来自专栏Java3y

用户登陆注册【JDBC版】

前言 在讲解Web开发模式的时候,曾经写过XML版的用户登陆注册案例!现在在原有的项目上,使用数据库版来完成用户的登陆注册!如果不了解的朋友,可以看看我Web开...

3119
来自专栏张善友的专栏

IronPython 承载和消费WCF服务

是开始学习IronPython 的时候了文章里谈到了“IronPython 2.6提供了新特性clrtype,允许程序员用纯IronPython代码提供prop...

2036
来自专栏草根专栏

asp.net web api 2.2 基础框架(带例子)

简介 这个是我自己编写的asp.net web api 2.2的基础框架,使用了Entity Framework 6.2(beta)作为ORM。 该模板主要采用...

4239
来自专栏Android机动车

我的图片四级缓存框架

开发App一定涉及到图片加载、图片处理,那就必须会用到三方的图片框架,要么选择自己封装。至于主流的三方图片框架,就不得不说老牌的ImageLoader、如今更流...

863

扫码关注云+社区