Spring事务

253 阅读8分钟

前言

终于学习完了AOP,当方法存在事务时执行真相是怎样的?在某一个Service类中,f1调用f2,f1、f2使用不同的事务传播会有什么现象?f1、f2存在或不存在事务注解时会有什么现象?带着这些问题我们一起学习Spring事务

代码示例:

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/nba?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
@Service
public class TxService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Transactional
    public void f1(PlayerData user){
        jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
        //当前测试先注释调用f2,后面放开用于测试事务转播机制
        //f2(user);
    }
    @Transactional
    public void f2(PlayerData user){
        jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
    }
    public static class PlayerData{
        public double playerHeigth;
        public String playerName;
        public int teamId;
        public String teamName;
    }
}
@SpringBootApplication
public class TxDemo {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(TxDemo.class);
        TxService tx1Service = context.getBean("txService", TxService.class);
        TxService.PlayerData playerData = new TxService.PlayerData();
        playerData.playerHeigth = 1.98;
        playerData.playerName = "科比";
        playerData.teamName = "湖人";
        playerData.teamId = 1001;
        tx1Service.f1(playerData);
    }
}

跟踪调用f1,在TransactionAspectSupport类中看到如下核心逻辑

image-20220114104559878.png 事务处理简化版本如下:

//开启事务
TransactionInfo txInfo = createTransactionIfNecessary()
try{
    //调用目标方法
    invocation.proceedWithInvocation
}catch(Throwable e){
    //回滚事务
    completeTransactionAfterThrowing(txInfo)
    throw ex;
}finally{
    //清理事务
    cleanupTransactionInfo(txInfo)
}
//提交事务
commitTransactionAfterReturning(txInfo)

开启事务

image-20220121115657655.png 开启事务的两个核心操作:创建TransactionStatus、创建TransactionInfo

序列图:

image-20220126163041531.png 核心流程描述:

  1. 从resources中获取当前线程的数据库连接,若存在,将事务状态封装为TransactionStatus返回,否则进入2
  2. 获取一个数据库连接,并关闭自动提交,将连接封装为ConnectionHolder并绑定到resources中
  3. 将事务信息封装为TransactionInfo绑定到transactionHolder中,并返回

核心类

TransactionInterceptor事务拦截器

开启处理事务的地方,最顶层实现自Advice,这就是前面Aop讲解5种通知一样的原理,效果如同Around

DataSourceTransactionManager资源管理器

拥有一个DataSource成员,提供事务接口,如:doBegin、doSuspend、doResume、doCommit、doRollback

在DataSourceTransactionManagerAutoConfiguration中被扫描到容器中

image-20220125160002936.png

DataSource数据源

A factory for connections to the physical data source that 
this DataSource object represents. A DataSource object
is the preferred means of getting a connection

连接到实体数据源的抽象,类似工厂模式,能够从数据源获取连接。常见的有:HikariDataSource、DruidDataSource。在yml文件中,spring.datasource下的信息将会封装为一个DataSource实例

TransactionSynchronizationManager事务同步管理器

为一个全局静态类,所有成员变量和函数都是静态的

/**
 * Central delegate that manages resources and transaction synchronizations per thread
 * 这里不好翻译,结合后面的代码会知道,是为每个事务管理回调接口
 */
  public abstract class TransactionSynchronizationManager {
  private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
  //线程变量,值为一个map,key为dataSource,value为ConnectionHolder。每个线程在同一个数据源下拥有各自的数据库连接
  private static final ThreadLocal<Map<Object, Object>> resources =
      new NamedThreadLocal<>("Transactional resources");
  //线程变量,值为一个set,在事务执行的各个阶段(提交前、提交后 、完成前、完成后)能够调用遍历set中的回调
  private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
      new NamedThreadLocal<>("Transaction synchronizations");
  }

TransactionInfo事务信息

/**
* used to hold transaction information 用于保存事务信息
*/
protected static final class TransactionInfo {
   //事务管理,提供事务操作接口
   @Nullable
   private final PlatformTransactionManager transactionManager;
   //传播机制与隔离信息的抽象
   @Nullable
   private final TransactionAttribute transactionAttribute;
   private final String joinpointIdentification;
   //事务状态
   @Nullable
   private TransactionStatus transactionStatus;
   //旧事务,如果有挂起操作,以前的事务会被存在到这个字段
   @Nullable
   private TransactionInfo oldTransactionInfo;
}

TransactionStatus事务状态

/**
 * Representation of the status of a transaction 事务状态的呈现
 */
public class DefaultTransactionStatus extends AbstractTransactionStatus {
   //实际保存DataSourceTransactionObject值,DataSourceTransactionObject中拥有ConnectionHolder
   @Nullable
   private final Object transaction;
   //是否是新事务 REQUIRES_NEW开启新事务,为真。REQUIRED不存在时创建,为真
   private final boolean newTransaction;
   //是否是新同步,翻译成是否是新回调可能更好 这与前面讲的TransactionSynchronization有关,表示回调是在以前的事务中,还是新的事务中 REQUIRES_NEW时为真
   private final boolean newSynchronization;
   private final boolean readOnly;
   private final boolean debug;
   //挂起的资源,与传播机制有关。REQUIRES_NEW、NOT_SUPPORTED挂起前面的资源
   @Nullable
   private final Object suspendedResources;
}
/**
* Resource holder wrapping a JDBC Connection.jdbc connection的封装
*/
public class ConnectionHolder extends ResourceHolderSupport {
   @Nullable
   private ConnectionHandle connectionHandle;
   //数据库连接
   @Nullable
   private Connection currentConnection;
   private boolean transactionActive = false;
   @Nullable
   private Boolean savepointsSupported;
   private int savepointCounter = 0;
   }

transactionInfoHolder为ThreadLocal变量,每个线程都拥有自己的TransactionInfo信息,并发时互不影响,再加上TransactionInterceptor没有其他共享变量,所以即使全局只有一个TransactionInterceptor实例,也是线程安全的,再回顾前面事务回滚、清理事务、提交事务流程只需要TransactionInfo参数,所以整个流程也就并发安全了

执行sql

执行sql语句时只需要与开始事务时使用的相同连接就可以了,是如何做到的呢?这里以JdbcTemplate为例

image-20220126153131879.png 引入spring-jdbc后,JdbcTemplate会扫描到容器中,且会注入yml文件配置的dataSource。执行sql时会进入如下流程

image-20220126162914710.png

回滚事务

使用当前线程数据源对应的连接发送rollback语句,移除resources中当前线程数据源对应的ConnectionHolder,开启自动提交,将连接置为NOT_IN_USE

image-20220127172500145.png

清理事务

image-20220126170403762.png

提交事务

使用当前线程数据源对应的连接发送commit语句,移除resources中当前线程数据源对应的ConnectionHolder,开启自动提交,将连接置为NOT_IN_USE

image-20220127172951802.png 以上流程为执行f1(不调用f2)时的主流程。其中忽略了判断事物是否存在、开启新事务、事务恢复、提交回滚时标记、上级提交回滚等存在事务传播时逻辑,下面通过事务传播来具体讲解这些细节

传播机制与坑

7中传播机制,网上已经有很多资料,这里结合源码只讲两种

REQUIRED

Support a current transaction, create a new one if none exists

支持当前事务,如果不存在就创建一个事务

public void f1(PlayerData user){
    jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
    f2(user);
}
@Transactional
public void f2(PlayerData user){
    jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
    int i = 1/0;
}

上述代码似乎符合REQUIRED事务传播的描述,f2发现f1没有事务,就创建一个。但实际运行结果两条sql语句都会成功,原因是整个过程中都没有开启事务,这里涉及的是代理的知识点(可以回顾前面的文章spring之Bean(2))。当类的接口被切面注解或事务标注时,普通bean会被封装为proxyBean,原始的bean会变为targetBean被proxyBean包裹着。调用接口时若方法具有Transactional注解就会添加TransactionIntercepter拦截器。这里f1没有注解所以无法添加拦截器,进入f1后调用f2的this指针是targetBean,不是proxyBean,更无法进入添加拦截器流程,所以f2也没有事务

要让调用f2具有事务只需要将targeBean变为proxyBean即可。注入自己。此时运行结果,f1能正常更新,f2会回滚

@Autowired
private TxService txService;
public void f1(PlayerData user){
    jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
    txService.f2(user);
}

注入自己与调用其他service是一样的道理

支持当前事务,如果不存在就创建一个事务。前提是当前被调用的方法是由proxyBean调用的

目前测试了后面句,不存在就创建事务,下面测试前半句支持当前事务。f1中调用f2时打断点

@Transactional
public void f1(PlayerData user){
    jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
    txService.f2(user);
}
@Transactional
public void f2(PlayerData user){
    jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
}

调用图:数字表示执行顺序,数字后的txInfo表示进入该流程时需要的参数

image-20220127145859173.png 在执行f1流程开启事务时此线程数据源下已经获取了connection,进入f2流程再开启事务时

image-20220126175004593.png 此时connectionHolder不为空且被激活,进入handleExistingTransaction流程。在所有条件都排出后进入Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED流程。生成一个新的transactionStatus状态信息,信息中的newTransaction为false。从开启事务序列图可知,transactionStatus会被封装进transactionInfo,而transactionInfo会绑定到当前线程 image-20220126180443940.png 从transactionInfoHolder获取事务信息即为f1开启事务时创建的信息赋值到oldTransactionInfo。将f2创建的事务信息赋值到当前线程中 image-20220127153314704.png 可以看到f2在REQUIRED模式下,是否是新事务,是否是新同步都为假。这就是源码注释中支持当前事务的本质

那么我们看看这两个字段在f2提交或回滚时是如何起作用的,在f2函数中执行sql处设置断点。

提交

在前面f1不调用f2时会在这儿进入提交,但此时f2的txInfo2中的transactionStatus的newTransaction为假,由上图可知,所以不会进入提交。此时f2执行完毕从调用图的第7步返回,进入第9步,f1中的txInfo1中的transactionStatus的newTransaction为真,一起提交 image-20220127150514168.png

回滚

在f2函数最后一样添加int i=1/0,测试回滚。在int i=1/0处设置断点,由图可知,此时newTransaction为假,不会回滚。进入else流程将连接信息设置rollbackOnly。f2流程执行完后,进入f1的回滚,f1中的txInfo1中的transactionStatus的newTransaction为真,一起回滚 image-20220127152401568.png

REQUIRES_NEW

Create a new transaction, and suspend the current transaction if one exists

创建一个新的事务,如果当前已存在事务,则将其挂起。 在学习了REQUIRED模式后应该可以猜到大致的执行流程,f2开始事务时会获取一个新的数据库连接,f2的txStatus中的newTransaction和newSynchronization会为真。f2的提交和回滚都会在新的事务中执行,而不是回到f1中。下面通过debug源码来证实一下

@Transactional
public void f1(PlayerData user){
    jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
    txService.f2(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void f2(PlayerData user){
    jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
}

新连接

在调用f2处设置断点,在handleExistingTransaction进入开启新事务流程 image-20220127155333444.png 再次进入绑定txInfo的地方 image-20220127161321860.png image-20220127161204420.png

提交

在新的连接中独立提交,与前面只调用f1(内部不调用f2)相同

回滚

在新的连接中独立回滚,与前面只调用f1(内部不调用f2)相同

事务执行阶段回调

上面的截图中会看到一个newSynchronization的字段,指明回调是否是新的,即是否在新事务中回调。

示例代码

@Transactional
public void f1(PlayerData user){
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void beforeCommit(boolean readOnly) {
            System.out.println("f1 beforeCommit:"+readOnly);
        }
        @Override
        public void beforeCompletion() {
            System.out.println("f1 beforeCompletion");
        }
        @Override
        public void afterCommit() {
            System.out.println("f1 afterCommit");
        }
        @Override
        public void afterCompletion(int status) {
            System.out.println("f1 afterCompletion");
        }
    });
    jdbcTemplate.update("update player set height=? where player_name=?",user.playerHeigth,user.playerName);
    System.out.println("update f1...");
    txService.f2(user);
}
@Transactional
public void f2(PlayerData user){
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void beforeCommit(boolean readOnly) {
            System.out.println("f2 beforeCommit:"+readOnly);
        }
        @Override
        public void beforeCompletion() {
            System.out.println("f2 beforeCompletion");
        }
        @Override
        public void afterCommit() {
            System.out.println("f2 afterCommit");
        }
        @Override
        public void afterCompletion(int status) {
            System.out.println("f2 status");
        }
    });
    jdbcTemplate.update("update team set team_name=? where team_id=?",user.teamName,user.teamId);
    System.out.println("update f2...");
}

效果如图:由于f2使用的f1的事务,所以f1调用完f2后才会进入提交阶段调用同步接口 image-20220127163315287.png

将f2的传播类型改为REQUIRES_NEW

效果如图:f2在新事务中运行,f2进入提交阶段后就会调用同步接口,f2处理完后f1进入提交阶段调用同步接口

image-20220127163504986.png

同步调用接口的顺序如下 image-20220127164025847.png image-20220127164102511.png

总结

当方法或类具有事务注解,方法被调用时实际是由代理对象调用的,目标方法执行前会添加TransactionInterceptor拦截器,拦截器全局只有一个,顶层实现自Advice,效果如同Around。代理对象调用方法时实际的执行流程时开启事务、执行目标方法、回滚事务、清理事务、提交事务

开启事务时总会先从TransactionSynchronizationManager的ThreadLocal变量resources中获取当前数据源对应的Connection,如果connection已经存在,则会进入事务存在流程,根据当前事务传播机制决定是加入当前事务还是开启新事务,获取到的连接信息会被绑定到resource中并被包装为TransactionStatus,在txStatus中有两个重要的字段newTransaction、newSynchronization来决定后面的提交或回滚是在当前事务执行还是新事务执行,txStatus会被包装为TransactionInfo,在txInfo中有一个oldTxInfo用来保存上一次的事务,txInfo会被绑定到TransactionInterceptor的ThreadLocal变量transactionInfoHolder中

执行sql时常用的jdbcTemplate或其他方式会得到当前数据源的抽象,最终从resources中获取到当前线程的数据库连接

回滚或提交事务时执行完rollback或commit语句后,会释放resources中连接,开启自动提交,并将连接置为NOT_IN_USE状态

清理事务时会解绑transactionInfoHolder中的txInfo

完结🎉