Spring数据库事务处理

目录

1.Spring声明式事务

1.2 声明式事务约定

1.3 @Transactional源码分析

1.4 Spring事务管理器

2.隔离级别

2.1 数据库事务特性

2.1.1 第一类丢失更新

2.1.2 第二类丢失更新

2.2 详解隔离级别

2.2.1 未提交读

2.2.2 读写提交

2.2.3 可重复读

2.2.4 串行化

2.2.5 使用合理的隔离级别

3. 传播行为

3.1 传播行为的定义

4.@Transactional自调用失效问题


对于一些业务网站而言,产品库存的扣减、交易记录以及账户都必须是要么同时成功,要么同时失败,这就是一种事务机制,Spring对这样的机制给予了支持。而在一些特殊的场景下,如一个批处理,它将处理多个交易,但是在一些交易中发生了异常,这个时候则不能将所有的交易都回滚。如果所有的交易都回滚了,那么那些本能够正常处理的业务也无端地被回滚了,这显然不是我们所期待的结果。通过Spring的数据库事务传播行为,可以很方便地处理这样的场景。

Spring中的数据库事务可以使用编程式事务,也可以使用声明式事务。大部分情况下,会使用声明式事务,编程式事务基本不再使用了。

1.Spring声明式事务

执行SQL事务的流程如下图所示:

在上图中,有业务逻辑的部分只是执行SQL那一步骤,其他步骤都是比较固定的,按照AOP的设计思想,就可以把除执行SQL这步以外的步骤抽取出来单独实现,这就是Spring数据库事务编程的思想。

1.2 声明式事务约定

对于声明式事务,使用@Transactional注解进行标注,该注解可以标注在类或者方法上,当它标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务。在@Transaction中,还允许配置许多的属性,如事务的隔离级别和传播行为;又如异常类型,从而确定方法发生什么异常下回滚事务或者发生什么异常下不回滚事务等。

有了@Transaction的配置,Spring就会知道在哪里启动事务机制,其约定流程如下图所示:

上图中,除了执行方法逻辑由开发者提供以外,其他的逻辑都由Spring数据库事务拦截器根据@Transactional配置的内容来实现。下面让我们来讨论一下@Transactional的配置项。

1.3 @Transactional源码分析

源码如下:

ackage org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    //指定事务管理器bean的名称
    @AliasFor("transactionManager")
    String value() default "";
    //同value属性
    @AliasFor("value")
    String transactionManager() default "";
    //指定事务的传播行为
    Propagation propagation() default Propagation.REQUIRED;
    //指定隔离级别
    Isolation isolation() default Isolation.DEFAULT;
    //指定超时时间
    int timeout() default -1;
    //是否是只读事务
    boolean readOnly() default false;
    //方法在发生指定异常时回滚,默认是所有的异常都回滚
    Class<? extends Throwable>[] rollbackFor() default {};
    //方法在发生指定异常名称时回滚,默认所有异常都回滚
    String[] rollbackForClassName() default {};
    //方法在发生指定异常时不回滚,默认是所有的异常都回滚
    Class<? extends Throwable>[] noRollbackFor() default {};
    //方法在发生指定异常名称时不回滚,默认所有异常都回滚
    String[] noRollbackForClassName() default {};
}

通过上面的注释,我们能够非常清晰的了解到每个配置项的含义。关于注解@Transaction值得注意的是它可以放在接口上,也可以放在实现类上,但是Spring团队建议放在实现类上,因为放在接口上将将使得你的类基于接口的代理时它才能生效。如果使用接口,那么你将不能使用CGLIB动态代理,而只能使用JDK动态代理,这将大大地限制你的应用,因此在实现类上使用@Transactional注解才是最佳的方式。由于注解@Transactional使用到了事务管理器,那么下面我们首先讨论事务管理器。

1.4 Spring事务管理器

在Spring中,事务管理器的顶层接口为PlatformTransactionManager,Spring基于它定义了一系列的接口和类,类图如下:

这里面最常用的事务管理器是DataSourceTransactionManager,从图中可以看到它是一个实现了接口PlatformTransactionManager的类,该接口类的实现代码如下:

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
    //获取事务,它还会设置数据属性
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事务
    void commit(TransactionStatus var1) throws TransactionException;
    //回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}

Spring在事务管理时,就是将这些方法按照约定织入对应的流程中的,其中getTransaction方法的参数是一个事务定义器,它是依赖于我们配置的@Transactional的配置项生成的,通过它就能够设置事务的属性了。而提交和回滚可以通过commit和rollback方法来执行。

在Spring Boot中,当你依赖于mybatis-spring-boot-starter之后,它会自动创建一个DataSourceTransactionManager对象,作为事务管理器;如果依赖于spring-boot-starter-data-jpa,则它会自动创建JtaTransactionManager对象作为事务管理器,所以我们一般不需要自己创建事务管理器而直接使用它们即可。

2.隔离级别

上面我们只是简单的使用了事务,下面我们将讨论Spring事务机制中最重要的两个配置项:隔离级别和传播行为。

2.1 数据库事务特性

数据库的事务具有以下4个基本特征,也就是著名的ACID特性:

  • Atomic(原子性):事务中包含的操作被看作一个整体的业务单元,这个业务单元中的操作,要么全部成功,要么全部失败,不会出现部分成功和部分失败的场景。
  • Consistency(一致性):事务在完成时,必须使所有的数据都保持一致的状态,在数据库中所有的修改都是基于事务,保证了数据的完整性。
  • Isolation(隔离性):这是我们讨论的核心内容,我们的同一数据可能同时被多个线程访问,这样数据库同样的数据就会在各个不同的事务中被访问,这样就会产生丢失更新。为了避免丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上避免丢失更新的发生。
  • Durability(持久性):事务结束后,所有的数据会固话到一个地方,如保存在磁盘上,即使断电重启后也可以提供给应用程序访问。

这四个特性,除了隔离性都比较好理解,为了便于理解,这里我们深入地讨论在多个事务同时操作数据的情况下,会引发丢失更新的场景。例如,有一种秒杀商品,库存100件,当秒杀时存在多个事务同时访问库存的场景。

2.1.1 第一类丢失更新

可以看到,在T5时刻事务1回滚了,导致原本库存为99的变为了100,显然事务2的结果就丢失了。对于这样,一个事务回滚另外一个事务已提交的值而引发的数据不一致的情况,我们称为是第一类丢失更新。然而,它并没有多大的讨论价值,因为目前大部分数据库已经克服了第一类丢失更新的问题,也就是当今的数据库系统已经不会再出现表中的情况了。

2.1.2 第二类丢失更新

因为在事务1中,无法感知事务2的操作,这样它就不知道事务2已经修改过了数据,因此它依旧认为只发生了一笔交易,所以库存变为了99,而这个结果又是一个错误的结果。这样,T5时刻事务1提交的事务,就会引发事务2提交结果的丢失,我们把这样的多个事务都提交引发的丢失更新称为第二类丢失更新。

为了克服这个问题,数据库提出了事务之间的隔离级别。

2.2 详解隔离级别

为了在不同程度上克服丢失更新,数据库标准提出了4类隔离级别,这四类隔离级别分别是未提交读、读写提交、可重复读和串行化。

也许这里有个疑问,为什么只是在不同程度上去压制丢失更新,而不能完全避免呢?其实,数据库现有的技术完全可以避免丢失更新,但是这样的代价就是付出锁的代价,降低数据库的性能。因此我们必须在性能和数据一致性中找到平衡。

2.2.1 未提交读

未提交读是最低的隔离级别,其含义就是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所有一般不会使用,但它的优点是并发能力强,适合那些对数据一致性没有要求而追求高并发的场景,它最大的缺点就是出现脏读。出现脏读的实例如下:

出现脏读
出现脏读

在T3时刻,因为采用了未提交读,所以事务2可以读取事务1未提交的库存数据为1,这里当它扣减库存后数据为0,然后它提交了事务,库存就变为了0,然后它提交了事务,库存就变为了0,而事务1在T5时刻回滚事务,因为第一类丢失更新已经克服,所以它不会回滚到2,那么结果就变为了0,这样就出现了错误。

为了克服脏读的问题,数据库隔离级别定义了读写提交的级别。

2.2.2 读写提交

读写提交隔离级别,是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。上面的场景在限制为读写提交以后,就变为以下的场景了。

克服脏读

采取了读写提交的隔离级别以后,就克服了脏读现象,但是读写提交也会产生下面的不可重复读的问题:

不可重读场景

这里的问题在于事务2之前认为可以扣减,而到了扣减的时候发现已经不可以扣减了,于是库存对于事务2而言是一个可变化的值,这样的现象我们称为不可重复读,这就是读写提交的不足。为了克服这个不足,数据库的隔离级别提出了可重复读的隔离级别,它能够消除不可重复读的问题。

2.2.3 可重复读

可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候可能会出现一些值得变化,影响当前事务的执行。克服不可重复读的现象如下所示:

克服不可重读

事务2在T3时刻尝试读取库存,但是此时这个库存已经被事务1事先读取,所以这个时候数据库就阻塞它的读取,直至事务1提交,事务2才能读取库存的值。此时已经是T5时刻,而读取到的值是0,这时就无法扣减库存了,显然在读写提交中出现的不可重复读的场景被消除了。但是这样也会引发新的问题出现,这就是幻读。假设现在正在进行商品交易,而后台有人也在进行查询分析和打印的业务。如下场景:

这里有一点需要注意的是,这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值。也就是说幻读不是针对一条数据库记录而言的,而是多条记录,例如这51笔交易就是多条数据库记录统计出来的。而可重复读是针对数据库的单一条记录,例如商品的库存是以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读。

2.2.4 串行化

串行化是数据库最高的隔离级别,它会要求所有的SQL都会按照顺序执行,这样就可以克服上述隔离级别出现的全部问题,所以它能够保证数据的一致性。

2.2.5 使用合理的隔离级别

下面我们总结一些数据库的隔离级别和可能发生的现象:

隔离级别和可能发生的现象

在实际开发中,追求更高的隔离级别,它能很好地保证数据的一致性,但是也要付出锁的代价。有了锁,就意味着性能的丢失,而且隔离级别越高,性能就越是直线的下降。

对于隔离级别,不同的数据库的支持也是不一样的,例如,Oracle只能支持读写提交和串行化,而MySQL则能够支持4种,对于Oracle默认的隔离级别为读写提交,MySQL则是可重复读。

在Spring中使用隔离级别非常简单,只需要在@Transactional配置即可:

@Transactional(isolation = Isolation.SERIALIZABLE)

上面的代码指定使用序列化的隔离级别来保证数据一致性,这使它将阻塞其他事务进行并发,所以它只能运用在那些低并发而又不需要保证数据一致性的场景下。

Spring Boot也可以通过配置文件指定默认的隔离级别,配置如下:

#指定Tomcat数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2

隔离级别数字配置的含义:

  • -1:使用数据库默认隔离级别
  • 1:未提交读
  • 2:读写提交
  • 4:可重复读
  • 8:串行化

3. 传播行为

传播行为是方法之间调用事务采取的策略问题,在绝大部分情况下,我们会认为数据库事务要么全部成功,要么全部失败。但现实中也许存在特殊的情况,比如执行一个批量任务,它会处理很多笔交易,绝大部分交易是可以顺利完成的,只有少部分可能发生异常,这时我们不应该因为极少数的交易不成功而回滚批量任务调用的整个事务,使得那些本能完成的交易也变为不能完成了。此时,我们真实的需求是,在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,而不是整个批量任务。示意图如下所示:

事务的传播行为

在Spring中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这就是事务的传播行为。

3.1 传播行为的定义

在Spring事务机制中对数据库存在7种传播行为,它是通过枚举类Propagation定义的。源代码如下:

package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;

public enum Propagation {

	/**
	 * 如果当前存在事务,就沿用当前事务,否则新建一个事务运行子方法
     * 它是默认的传播行为
	 */
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	/**
	 * 如果当前存在事务,就沿用当前事务,
     * 如果不存在就继续采用无事务的方式运行子方法
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	/**
	 * 必须使用事务,如果当前没有事务就会抛出异常
     * 如果存在事务,就沿用当前事务
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	/**
	 * 无论当前事务是否存在,都会创建新事务运行方法
     * 这样新事务就会拥有新的锁和隔离级别等特性,与当前事务相互独立
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	/**
	 * 不支持事务,如果当前存在事务,则挂起事务,运行方法
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	/**
	 * 不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	/**
	 * 在当前方法调用子方法时,如果子方法发生异常
     * 只回滚子方法执行过的SQL,而不回滚当前方法的事务
	 */
	NESTED(TransactionDefinition.PROPAGATION_NESTED);


	private final int value;


	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}

}

传播行为一共分为7种,但是常用的只有REQUIRED、REQUIRES_NEW和NESTED,其他的使用率非常低,不需要做过多的理解。REQUIRES_NEW会完全脱离原有事务的管控,每一个事务都拥有自己独立的隔离级别和锁。NESTED是一个如果子方法回滚而当前事务不回滚的方法。

在大部分的数据库中,一段SQL语句中可以设置一个标志位,然后后面的代码执行时如果有异常,只是回滚到这个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中被称为保存点。Spring就是使用保存点技术来完成让子事务回滚而不致使当前事务回滚的工作。需要注意的是,并不是所有的数据库都支持保存点技术,因此当数据库支持保存点技术时,就启用保存点;如果不支持,就新建一个事务去运行你的代码,即等价于REQUIRES_NEW传播行为。

NESTED传播行为和REQUIRES_NEW还有一个重要区别是,NESTED传播行为会沿用当前事务的隔离级别和锁等特性,而REQUIRES_NEW则可以拥有自己独立的隔离级别和锁等特性。

4.@Transactional自调用失效问题

@Transactional在自调用的场景下会失效,这是一个需要注意的问题。看如下的实例代码:

package com.martin.config.service.impl;

import com.martin.config.chapter5.dao.UserMapper;
import com.martin.config.chapter5.pojo.User;
import com.martin.config.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author: martin
 * @date: 2019/11/9 22:14
 * @description:
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUser(Long id) {
        return userMapper.getUser(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public boolean batchAddUser(List<User> users) {
        users.forEach(value -> {
            try {
                //调用自身类自身的方法,产生自调用问题
                insertUser(value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return true;
    }

    //传播行为设置为REQUIRES_NEW,每次调用产生新的事务
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public boolean insertUser(User user) {
        return userMapper.insertUser(user) > 0 ? true : false;
    }
}

UserServiceImpl 类中批量插入用户信息的方法调用了自己的方法insertUser一条一条的去插入,这是一个类自身方法之间的调用,我们称之为自调用。执行日志如下:

2019-11-15 22:18:57.799 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] for JDBC transaction
2019-11-15 22:18:57.800 DEBUG 20924 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] to 2
2019-11-15 22:18:57.817 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] to manual commit
2019-11-15 22:19:13.525  WARN 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=45s729ms237µs900ns).
2019-11-15 22:19:13.526 DEBUG 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:19:20.980 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:19:20.996 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
Fri Nov 15 22:19:20 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:19:21.000 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:21.007 DEBUG 20924 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@585339508 wrapping com.mysql.jdbc.JDBC4Connection@94b03b5] will be managed by Spring
2019-11-15 22:19:21.010 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:21.033 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user0(String), 0(Integer), note0(String)
2019-11-15 22:19:21.052 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:21.057 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:21.057 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:19:50.621 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:19:50.621 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:50.622 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user2(String), 0(Integer), note2(String)
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:50.640 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:19:59.668 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:19:59.677 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user3(String), 1(Integer), note3(String)
Fri Nov 15 22:19:59 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:19:59.703 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:19:59.704 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:19:59.704 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:20:41.188 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:20:41.188  WARN 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=50s576ms467µs300ns).
2019-11-15 22:20:41.189 DEBUG 20924 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:20:41.208 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec] from current transaction
2019-11-15 22:20:41.208 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:20:41.209 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user4(String), 0(Integer), note4(String)
2019-11-15 22:20:41.230 DEBUG 20924 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:20:41.230 DEBUG 20924 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@514d94ec]
2019-11-15 22:20:41.231 DEBUG 20924 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint

从上述日志的执行中,我们发现Spring在运行中并没有创建任何新的事物独立的运行insertUser,换句话说,我们的注解@Transactional失效了。

Spring数据库事务的实现原理是AOP,而AOP的原理是动态代理,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样就不能把你的代码织入到约定的流程中,于是就产生了现在看到的失败的场景。为了克服这个问题,我们可以使用一个Service去调用另外一个Service,这样很好理解。也可以从Spring AOP容器中获取Bean对象去启用AOP,实例代码如下:

package com.martin.config.service.impl;

import com.martin.config.chapter5.dao.UserMapper;
import com.martin.config.chapter5.pojo.User;
import com.martin.config.service.UserService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author: martin
 * @date: 2019/11/9 22:14
 * @description:
 */
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {
    @Autowired
    private UserMapper userMapper;

    private ApplicationContext applicationContext;

    @Override
    public User getUser(Long id) {
        return userMapper.getUser(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public boolean batchAddUser(List<User> users) {
        UserService userService = applicationContext.getBean(UserService.class);
        users.forEach(value -> {
            try {
                //调用自身类自身的方法,产生自调用问题
                userService.insertUser(value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return true;
    }

    //传播行为设置为REQUIRES_NEW,每次调用产生新的事务
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public boolean insertUser(User user) {
        return userMapper.insertUser(user) > 0 ? true : false;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

执行的日志如下:

2019-11-15 22:35:10.193 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] for JDBC transaction
2019-11-15 22:35:10.204 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] to 2
2019-11-15 22:35:10.228 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@504092881 wrapping com.mysql.jdbc.JDBC4Connection@24d8a954] to manual commit
2019-11-15 22:35:10.257 DEBUG 21444 --- [nio-8080-exec-1] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'userServiceImpl'
2019-11-15 22:35:16.754 DEBUG 21444 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
2019-11-15 22:35:16.763 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]
Fri Nov 15 22:35:16 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:16.949 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@905d07d
2019-11-15 22:35:16.950 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] for JDBC transaction
2019-11-15 22:35:16.950 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 2
Fri Nov 15 22:35:16 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:16.975 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to manual commit
2019-11-15 22:35:16.992 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:35:17.025 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2019-11-15 22:35:17.039 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.065 DEBUG 21444 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] will be managed by Spring
2019-11-15 22:35:17.109 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:35:17.113 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@6d6432b9
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.273 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@14190c5a
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.349 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user0(String), 0(Integer), note0(String)
2019-11-15 22:35:17.374 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:35:17.387 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.388 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:35:17.390 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.391 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.392 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@492bdaf3]
2019-11-15 22:35:17.393 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-15 22:35:17.393 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d]
2019-11-15 22:35:17.422 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 4
2019-11-15 22:35:17.444 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1135762437 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] after transaction
2019-11-15 22:35:17.445 DEBUG 21444 --- [onnection adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@1f680ddb
2019-11-15 22:35:17.445 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-15 22:35:17.450 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-15 22:35:17.451 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]
2019-11-15 22:35:17.452 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] for JDBC transaction
2019-11-15 22:35:17.452 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 2
Fri Nov 15 22:35:17 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2019-11-15 22:35:17.473 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to manual commit
2019-11-15 22:35:17.490 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [com.sun.proxy.$Proxy64.insertUser]
2019-11-15 22:35:17.502 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2019-11-15 22:35:17.502 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.503 DEBUG 21444 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] will be managed by Spring
2019-11-15 22:35:17.504 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==>  Preparing: insert into user(user_name,sex,note) value (?,?,?) 
2019-11-15 22:35:17.507 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : ==> Parameters: user1(String), 1(Integer), note1(String)
2019-11-15 22:35:17.528 DEBUG 21444 --- [nio-8080-exec-1] c.m.c.c.dao.UserMapper.insertUser        : <==    Updates: 1
2019-11-15 22:35:17.529 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.530 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2019-11-15 22:35:17.531 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.531 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.533 DEBUG 21444 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1056dc80]
2019-11-15 22:35:17.534 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-11-15 22:35:17.535 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d]
2019-11-15 22:35:17.585 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] to 4
2019-11-15 22:35:17.608 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@869941384 wrapping com.mysql.jdbc.JDBC4Connection@905d07d] after transaction
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Returning JDBC Connection to DataSource
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2019-11-15 22:35:17.609 DEBUG 21444 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.martin.config.service.impl.UserServiceImpl.insertUser]

通过Acquired Connection 日志,Spring为我们的方法创建了新的事务,这样自调用的问题就克服了。只是这样的代码需要依赖Spring的API,对代码造成侵入。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章