Spring @Transactional踩坑记

简介: @Transactional踩坑记总述​ Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务控制。

@Transactional踩坑记

总述

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


踩坑记

1. 多数据源

事务不生效

背景介绍

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



    ....

 
   



  
  
  
  
 


  

 
   




    ....

 
   

​ 但是在实际部署的时候,因为是单机部署的,多个数据源实际上对应的是同一个库,不存在分布式事务的问题。所以在代码编写的时候,直接通过在@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 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 serviceLocal = new ThreadLocal();
  
  @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 + "]");
      }
  }
}

参考链接

黎明前最黑暗,成功前最绝望!
相关文章
|
安全 Java 数据库连接
【Spring Boot 源码学习】@EnableAutoConfiguration 注解
本篇我们一起从源码学习 @EnableAutoConfiguration 注解
111 2
【Spring Boot 源码学习】@EnableAutoConfiguration 注解
|
Java 数据库连接 数据库
面试官:Spring@Transactional注解在什么情况下事务不生效?
这篇笔记来学习一下使用Spring框架的时候,@Transactional注解标注的方法在什么情况下事务不会生效。 我们可以写一个demo项目, 引入以下依赖
|
XML Java 数据库连接
透彻的掌握 Spring 中@transactional 的使用
事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。
透彻的掌握 Spring 中@transactional 的使用
|
SQL 存储 Java
【Spring系列】- Spring事务底层原理
昨天学习了bean生命周期底层原理,今天就来接着简单学习spring事务的底层理解。
455 0
【Spring系列】- Spring事务底层原理
|
Java 测试技术 数据库
SpringBoot - 不要在 Spring Boot 集成测试中使用 @Transactional
SpringBoot - 不要在 Spring Boot 集成测试中使用 @Transactional
343 0
SpringBoot - 不要在 Spring Boot 集成测试中使用 @Transactional
|
XML Java 关系型数据库
【Spring基础系列4】注解@Transactional(一)
前面已经讲解了IOC的基础知识,以及Spring常用的注解,这篇文章是对上一篇文章《【Spring基础系列3】Spring常用的注解》的补充,由于这个注解需要讲述的内容比较多,一方面该注解非常重要,另一方面非常容易入坑,所以这个注解的内容,就单独放到这篇文章来讲。
219 0
【Spring基础系列4】注解@Transactional(一)
|
SQL 存储 Java
SpringBoot 系列教程之声明式事务 Transactional
当我们希望一组操作,要么都成功,要么都失败时,往往会考虑利用事务来实现这一点;之前介绍的 db 操作,主要在于单表的 CURD,本文将主要介绍声明式事务@Transactional的使用姿势
202 0
SpringBoot 系列教程之声明式事务 Transactional
|
Java Spring 容器
【小家Spring】从基于@Transactional全注解方式的声明式事务入手,彻底掌握Spring事务管理的原理(下)
【小家Spring】从基于@Transactional全注解方式的声明式事务入手,彻底掌握Spring事务管理的原理(下)
|
SQL Java 数据库连接
Spring 源码解析 | Spring 事务(二)
上一篇文章中说到了申明事务,本文再说一下编程事务,以及实现实现原理。 注:Spring 6.x
183 0
Spring 源码解析 | Spring 事务(二)