Spring Boot中声明式数据库事务使用与理解

JDBC的数据库事务

传统JDBC的数据库事务的一个示例如下代码所示,该示例仅为一个insertUser方法的数据库事务过程。可以看到,如果还存在很多其他的数据库事务需要,则需要编写很多类似于如下的代码过程,而其中大部分过程是重复的,仅为SQL的执行过程不相同。

@Service
public class JdbcServiceImpl implements JdbcService {

    @Autowired
    private DataSource dataSource;

    @Override
    public int insertUser(User user) {
        Connection connection = null;
        int result = 0;

        try {
            // 创建数据库连接
            connection = dataSource.getConnection();
            // 设置是否自动提交
            connection.setAutoCommit(false);
            // 开启事务并设置隔离级别
            connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            PreparedStatement statement = connection.prepareStatement(
                    "insert into t_user (user_name, sex, note) values (?, ?, ?)");
            statement.setString(1, user.getUserName());
            statement.setInt(2, user.getSex().getId());
            statement.setString(3, user.getNote());

            // 执行SQL
            result = statement.executeUpdate();
            // 提交事务
            connection.commit();

        } catch (SQLException e) {
            // 发生异常时回滚事务
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            // 关闭数据库连接
            try {
                if (connection != null || !connection.isClosed()) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}

而上述过程可以抽象为如下的数据库事务执行过程:

针对这种数据库事务的使用问题,Spring中使用AOP的方式将上述约定流程中除SQL执行以外的过程抽取出来单独实现,并将用户编写的SQL执行代码织入流程,从而简化大量公共代码。

Spring声明式事务

在Spring中,数据库事务主要使用声明式配置的方式,对于需要开启事务的类或者方法使用@Transactional注解进行标注。当其标注在类上时,表明这个类的所有publid、非静态方法都将启动事务,当标注在方法上时,则是对当前方法使用数据库事务。

此外@Transactional注解中还可以配置多个属性,例如事务的隔离级别以及转播行为等。这些配置内容,在Spring IOC容器加载时进行解析,然后将这些信息存到事务定义器(TransactionDefinition)中,并且记录那些类或者方法需要启动数据库事务,采取什么策略执行事务。此时在Spring中的事务处理过程即为如下:

所以,在开始数据库事务时,现在仅需要使用@Transactional注解进行标注,Spring会自动完成数据库连接、提交、回滚以及资源的释放等过程。

@Transactional注解的配置项

Spring中的数据库的事务属性通过@Transactional注解的属性进行配置,下面可以从该注解的源码中看到相关的配置项:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 通过Bean name设置事务管理器
    @AliasFor("transactionManager")
    String value() default "";

    // 同value属性
    @AliasFor("value")
    String transactionManager() default "";

    // 指定事务传播行为
    Propagation propagation() default Propagation.REQUIRED;

    // 指定事务隔离级别
    Isolation isolation() default Isolation.DEFAULT;

    // 指定事务超时时间(单位s)
    int timeout() default -1;

    // 是否为只读事务
    boolean readOnly() default false;

    // 方法在发生指定异常时回滚,默认是所有异常都回滚
    Class<? extends Throwable>[] rollbackFor() default {};

    // 方法在发生指定异常名称时回滚,默认是所有异常都回滚
    String[] rollbackForClassName() default {};

    // 方法在发生指定异常时不回滚,默认是所有异常都回滚
    Class<? extends Throwable>[] noRollbackFor() default {};

    // 方法在发生指定异常名称时不回滚,默认是所有异常都回滚
    String[] noRollbackForClassName() default {};
}

其中,isolation和propagation将在后续进行说明,而rollbackFor, rollbackForClassName, noRollbackFor, noRollbackForClassName都是指定异常,也就是指定在什么异常发生的情况下依旧提交事务,在什么异常的情况下回滚事务。

数据库事务管理器

在上述数据库事务的流程中,事务的打开,回滚和提交都是由事务管理器来完成的,在Spring中事务管理器的顶层接口为PlatformTransactionManager,并存在多个相关的实现类和接口。例如引入Hibernate是Spring ORM包会提供HibernateTransactionManager,引入MyBatis包时,使用的管理器为DataSourceTransactionManager(这里使用的为MyBatis)。

在Spring Boot中,当依赖于mybatis-spring-boot-starter后,会自动创建一个DataSourceTransactionManager对象,作为事务管理器,因此不需要自己手动的创建。

事务隔离级别

数据库事务具有ACID四个特性,如下:

  • Atomic(原子性)
  • Consistency(一致性)
  • Isolation(隔离性)
  • Durability(持久性)

其中,隔离性这里需要在数据库事务中进行配置的项。在并发环境中,存在数据库中同一条数据被不同的事务进行访问,这样就可能产生丢失更新的问题。因此隔离性的设置就是为了在不同不同程度上解决多个数据库事务同时访问一个数据库数据而产生丢失更新问题。

数据库在高并发环境中会出现几种问题,分别是:

  • 脏读:一个事务读到另一个事务的未提交数据。
  • 不可重复读:事务在前后读取同一条数据时,数据不一致。
  • 幻读:共享的数据对一个事务来说,前后表现出的状态不一致。

为了遏制这几种数据库事务并发问题,设计了如下四种数据库事务隔离级别,并且每种级别可以遏制的并发问题(X号所示)如下表所示:

隔离级别 脏读 不可重复读 幻读
读未提交 ✔️ ✔️ ✔️
读提交(读写提交) X ✔️ ✔️
可重复读 X X ✔️
串行化 X X X

在上述的隔离级别中,虽然对并发环境下数据一致性的保证依次增强,但其对性能的影响也依次升高。因此在选择数据库事务隔离记别时,需要综合考虑使用场景以及数据一致性要求,和性能等问题进行选择。一般而言,隔离级别的选择为读提交为主。

而不同数据库对于数据库的隔离级别的支持也不尽相同,Oracle只能支持读提交和串行化,MySQL可以支持全部四种。Oracle默认为读写提交,MySQL默认为可重复读。 

在Spring Boot中使用@Transactional注解时,可以对事务隔离级别进行设置,例如:

@Transactional(isolation = Isolation.READ_COMMITTED)

除此之外可以通过Spring Boot的application.properties配置文件设置默认的隔离级别:

# 设置默认的隔离级别为读写提交
# -1 使用数据库默认隔离级别
# 1 读未提交
# 2 读提交
# 3 可重复读
# 4 串行化
spring.datasource.tomcat.default-transaction-isolation=2

事务传播行为

事务的传播行为定义了当一个事务调用另一个事务采取的策略。在Spring 中,当一个方法调用另一个方法时,可以让事务采取不同的策略进行工作,如新建事务执行,挂起当前事务执行等,这就是数据库事务的传播行为。

在对@Transactional注解进行设置事务传播行为时,可以选择的项目可以从如下枚举类型中查看:

public enum Propagation {
    
    // 需要事务,为默认传播行为,如果当前方法存在事务,就沿用当前的事务,否则新建一个事务运行子方法
    REQUIRED(0),

    // 支持事务,如果当前存在事务沿用当前事务,如果不存在则继续采用无事务的方式运行子方法
    SUPPORTS(1),

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

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

    // 不支持事务,当前存在事务时将当前事务挂起,运行方法
    NOT_SUPPORTED(4),

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

    // 在当前方法调用子方法是,如果子方法发生异常,只回滚子方法的事务,而不回滚当前方法的事务
    NESTED(6);

    private final int value;

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

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

数据库事务使用实例

下面使用@Transactional注解对数据库事务的简单使用进行说明,这里使用的为Spring Boot+MyBatis来操作MySQL数据库。该项目的前序工作见:Spring Boot整合MyBatis框架操作MySQL数据库实例。这里在此基础上进行数据库事务的使用说明。下面构建相应的方法。

事务的使用

创建用户表

create table t_user (
	id int(12) not null auto_increment,
	user_name varchar(60) not null,
	sex int(3) not null default 1 check (sex in(1,2)),
	note varchar(256) null,
	primary key(id)
);

User对象类

@Data
@Alias(value = "user")
public class User {
 
    private Long id;
 
    @NotNull(message = "用户名不能为空")
    private String userName;
 
    @NotNull(message = "备注不能为空")
    private String note;
 
    @NotNull(message = "性别不能为空")
    private SexEnum sex;
}

MyBatis映射文件及接口

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zyt.springbootlearning.dao.UserMapper">
    <select id="getUserById" parameterType="long" resultType="user">
        select id, user_name as userName, sex, note from t_user where id = #{id}
    </select>
    
    <insert id="insertUser" parameterType="cn.zyt.springbootlearning.domain.User" useGeneratedKeys="true" keyProperty="id">
        insert into t_user (user_name, sex, note) values (#{userName}, #{sex}, #{note})
    </insert>
</mapper>

映射接口为: 

@Repository
public interface UserMapper {
    User getUserById(Long id);
    int insertUser(User user);
}

Service接口和实现类

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
    public User getUserById(Long id) {
        return userMapper.getUserById(id);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }
}

在Service中,使用了Transactional注解来标识两个方法开启事务,并并且配置了相应的事务隔离级别为读提交,超时时间为1s。下面使用一个controller来测试一下数据库事务。 

Controller方法

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
 
    /**
     * 测试Transactional
     */
    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(Long id) {
        return userService.getUserById(id);
    }

    /**
     * 测试Transactional
     */
    @RequestMapping("/insertUser")
    @ResponseBody
    public Map<String, Object> insertUser(String userName, int sex, String note) {
        User user = new User(userName, sex, note);
        int update = userService.insertUser(user);
        Map<String, Object> result = new HashMap<>();
        result.put("success", update == 1);
        result.put("user", user);
        return result;
    }
}

此外为了查看一下在使用MyBatis时,使用的事务管理器,在configuration配置类中加入了如下代码,在这个类对象被初始化后调用后初始化方法来打印出当前的事务管理器的名称:

@Configuration
public class MyBatisConfiguration {
    @Autowired
    PlatformTransactionManager transactionManager;

    @PostConstruct
    public void viewTransactionManager() {
        System.out.println("-----------------------------------");
        System.out.println(transactionManager.getClass().getName());
        System.out.println("-----------------------------------");
    }
}

上述的内容完成后,启动Spring Boot项目,来进行测试。启动完成后,可以看懂console中有如下的输出,即为当前在引入MyBatis之后使用的事务管理器:

-----------------------------------
org.springframework.jdbc.datasource.DataSourceTransactionManager
-----------------------------------

使用如下URL对开启事务的getUser方法进行测试:

http://localhost:8080/user/getUser?id=1

在Console中查看DEBUG级别的日志输出中包括如下内容: 

Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.getUserById]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,timeout_1
2020-02-19 17:20:19.186 DEBUG 80529 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] for JDBC transaction
2020-02-19 17:20:19.187 DEBUG 80529 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 2
2020-02-19 17:20:19.229 DEBUG 80529 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to manual commit
2020-02-19 17:20:19.255 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 17:20:19.257 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fa1dcd7]
2020-02-19 17:20:19.262 DEBUG 80529 --- [nio-8080-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] will be managed by Spring
2020-02-19 17:20:19.263 DEBUG 80529 --- [nio-8080-exec-1] c.z.s.dao.UserMapper.getUserById         : ==>  Preparing: select id, user_name as userName, sex, note from t_user where id = ? 
2020-02-19 17:20:19.278 DEBUG 80529 --- [nio-8080-exec-1] c.z.s.dao.UserMapper.getUserById         : ==> Parameters: 1(Long)
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] c.z.s.dao.UserMapper.getUserById         : <==      Total: 1
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fa1dcd7]
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fa1dcd7]
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fa1dcd7]
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6fa1dcd7]
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 17:20:19.310 DEBUG 80529 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be]
2020-02-19 17:20:19.352 DEBUG 80529 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 4
2020-02-19 17:20:19.372 DEBUG 80529 --- [nio-8080-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@853423018 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] after transaction

 同样的在使用如下URL进行insertUser时一样开启了事务:

http://localhost:8080/user/insertUser?userName=tname&sex=1&note=none

日志输出如下: 

2020-02-19 17:22:57.875 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.insertUser]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,timeout_1
2020-02-19 17:22:57.896 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] for JDBC transaction
2020-02-19 17:22:57.896 DEBUG 80529 --- [nio-8080-exec-5] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 2
2020-02-19 17:22:57.938 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to manual commit
2020-02-19 17:22:57.960 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 17:22:57.960 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53d67f36]
2020-02-19 17:22:57.960 DEBUG 80529 --- [nio-8080-exec-5] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] will be managed by Spring
2020-02-19 17:22:57.960 DEBUG 80529 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 17:22:57.962 DEBUG 80529 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname(String), 1(Integer), none(String)
2020-02-19 17:22:57.983 DEBUG 80529 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 17:22:57.984 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53d67f36]
2020-02-19 17:22:57.984 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53d67f36]
2020-02-19 17:22:57.985 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53d67f36]
2020-02-19 17:22:57.985 DEBUG 80529 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53d67f36]
2020-02-19 17:22:57.985 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 17:22:57.985 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be]
2020-02-19 17:22:58.026 DEBUG 80529 --- [nio-8080-exec-5] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 4
2020-02-19 17:22:58.047 DEBUG 80529 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@678085356 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] after transaction

设置事务传播行为

下面通过一个批量创建用户的过程来测试事务的传播行为。首先创建用于批量创建用户的Service接口和实现类:

@Service
public class UserBatchSeviceImpl implements UserBatchService {
    @Autowired
    private UserService userService;

    /**
     * 数据库事务传播机制REQUESTED
     */
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int insertUsers(List<User> userList) {
        int count = 0;
        for (User user : userList) {
            // 调用子方法,将使用@Transactional中定义的传播行为
            count += userService.insertUser(user);
        }
        return count;
    }
}

注意上述代码,其中将该insertUsers方法的事务传播行为设置为REQUIRED,而insertUser方法没有设置传播行为,因此该子方法insertUser将沿用insertUses的事务(即同一个事务)。下面创建一个一个Controller来进行测试:

    /**
     * 测试Transactional数据库事务隔离机制和传播方式
     */
    @RequestMapping("/insertUsers")
    @ResponseBody
    public Map<String, Object> insertBatchUser(String userName1, int sex1, String note1,
                                               String userName2, int sex2, String note2) {
        User user1 = new User(userName1, sex1, note1);
        User user2 = new User(userName2, sex2, note2);
        List<User> userList = new ArrayList<>();
        userList.add(user1);
        userList.add(user2);

        int insertCount = userBatchService.insertUsers(userList);
        Map<String, Object> result = new HashMap<>();
        result.put("success", insertCount > 0);
        result.put("user", userList);
        return result;
    }

使用如下的URL进行测试:

http://localhost:8080/user/insertUsers?userName1=tname1&sex1=1&note1=none&userName2=tname2&sex2=1&note2=none

日志输出如下: 

# 为insertUsers方法创建new transaction
2020-02-19 17:36:11.803 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserBatchSeviceImpl.insertUsers]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED
2020-02-19 17:36:11.823 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] for JDBC transaction
2020-02-19 17:36:11.824 DEBUG 80529 --- [nio-8080-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 2
2020-02-19 17:36:11.865 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to manual commit
# 使用已经存在的事务
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] will be managed by Spring
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 17:36:11.888 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname1(String), 1(Integer), none(String)
2020-02-19 17:36:11.909 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 17:36:11.909 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
# 使用已经存在的事务
2020-02-19 17:36:11.909 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
2020-02-19 17:36:11.909 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d] from current transaction
2020-02-19 17:36:11.909 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 17:36:11.910 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname2(String), 1(Integer), none(String)
2020-02-19 17:36:11.930 DEBUG 80529 --- [nio-8080-exec-2] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33f6439d]
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 17:36:11.931 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be]
2020-02-19 17:36:11.972 DEBUG 80529 --- [nio-8080-exec-2] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] to 4
2020-02-19 17:36:11.994 DEBUG 80529 --- [nio-8080-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@207102938 wrapping com.mysql.jdbc.JDBC4Connection@568ed1be] after transaction

此时事务的执行示意图如下: 

下面为子方法insertUser的设置相应的数据库传播行为如下:

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW, timeout = 1)
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

重启项目再次测试insertUsers,查看日志如下:


# 创建当前方法的事务(insertUsers)
2020-02-19 21:30:43.061 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserBatchSeviceImpl.insertUsers]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED
2020-02-19 21:30:43.080 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74] for JDBC transaction
2020-02-19 21:30:43.082 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74] to 2
2020-02-19 21:30:43.122 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74] to manual commit
# 创建子方法insertUser的新事务
2020-02-19 21:30:43.147 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.insertUser]
2020-02-19 21:30:43.171 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] for JDBC transaction
2020-02-19 21:30:43.171 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to 2
2020-02-19 21:30:43.216 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to manual commit
2020-02-19 21:30:43.246 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 21:30:43.248 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42b127f0]
2020-02-19 21:30:43.253 DEBUG 81249 --- [nio-8080-exec-6] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] will be managed by Spring
2020-02-19 21:30:43.254 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 21:30:43.270 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname1(String), 1(Integer), none(String)
2020-02-19 21:30:43.295 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42b127f0]
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42b127f0]
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42b127f0]
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42b127f0]
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 21:30:43.296 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77]
2020-02-19 21:30:43.341 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to 4
2020-02-19 21:30:43.364 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@853398429 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] after transaction
2020-02-19 21:30:43.364 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
# 创建子方法insertUser的新事务
2020-02-19 21:30:43.365 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.insertUser]
2020-02-19 21:30:43.365 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] for JDBC transaction
2020-02-19 21:30:43.365 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to 2
2020-02-19 21:30:43.410 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to manual commit
2020-02-19 21:30:43.433 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 21:30:43.433 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6bab40fc]
2020-02-19 21:30:43.433 DEBUG 81249 --- [nio-8080-exec-6] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] will be managed by Spring
2020-02-19 21:30:43.433 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 21:30:43.433 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname2(String), 1(Integer), none(String)
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6bab40fc]
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6bab40fc]
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6bab40fc]
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6bab40fc]
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 21:30:43.456 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77]
2020-02-19 21:30:43.501 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] to 4
2020-02-19 21:30:43.523 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1482450123 wrapping com.mysql.jdbc.JDBC4Connection@4b3ad77] after transaction
2020-02-19 21:30:43.523 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction
2020-02-19 21:30:43.524 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 21:30:43.524 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74]
2020-02-19 21:30:43.562 DEBUG 81249 --- [nio-8080-exec-6] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74] to 4
2020-02-19 21:30:43.580 DEBUG 81249 --- [nio-8080-exec-6] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1925691138 wrapping com.mysql.jdbc.JDBC4Connection@51cdb74] after transaction

此时事务的传播行为示例如下:

下面将insertUser方法的事务传播级别设置为NESTED,如下:

    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.NESTED, timeout = 1)
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

此时如果子方法中出现异常,会回滚子方法的事务而不会回滚当前事务。日志输出如下:


# 创建当前事务
2020-02-19 21:41:07.711 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [cn.zyt.springbootlearning.service.impl.UserBatchSeviceImpl.insertUsers]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED
2020-02-19 21:41:07.734 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] for JDBC transaction
2020-02-19 21:41:07.734 DEBUG 81319 --- [nio-8080-exec-5] o.s.jdbc.datasource.DataSourceUtils      : Changing isolation level of JDBC Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] to 2
2020-02-19 21:41:07.779 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] to manual commit
# 创建子方法事务
2020-02-19 21:41:07.807 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.insertUser]
2020-02-19 21:41:07.831 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2020-02-19 21:41:07.831 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
2020-02-19 21:41:07.832 DEBUG 81319 --- [nio-8080-exec-5] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] will be managed by Spring
2020-02-19 21:41:07.832 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 21:41:07.832 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname1(String), 1(Integer), none(String)
2020-02-19 21:41:07.855 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 21:41:07.856 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
# 释放savapoint
2020-02-19 21:41:07.856 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2020-02-19 21:41:07.856 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Creating nested transaction with name [cn.zyt.springbootlearning.service.impl.UserServiceImpl.insertUser]
2020-02-19 21:41:07.879 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e] from current transaction
2020-02-19 21:41:07.879 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==>  Preparing: insert into t_user (user_name, sex, note) values (?, ?, ?) 
2020-02-19 21:41:07.879 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : ==> Parameters: tname2(String), 1(Integer), none(String)
2020-02-19 21:41:07.902 DEBUG 81319 --- [nio-8080-exec-5] c.z.s.dao.UserMapper.insertUser          : <==    Updates: 1
2020-02-19 21:41:07.902 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
# 释放savapoint
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing transaction savepoint
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@458d82e]
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2020-02-19 21:41:07.903 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1]
2020-02-19 21:41:07.947 DEBUG 81319 --- [nio-8080-exec-5] o.s.jdbc.datasource.DataSourceUtils      : Resetting isolation level of JDBC Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] to 4
2020-02-19 21:41:07.970 DEBUG 81319 --- [nio-8080-exec-5] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1337356240 wrapping com.mysql.jdbc.JDBC4Connection@480647a1] after transaction

可以看到NESTED传播行为,之所以可以实现子事务回滚而当前事务不回滚,使用了savepoint的机制。

此外,NESTED传播行为和REQUIRES_NEW还是有区别的,NESTED传播行为会沿用当前事务的隔离级别和锁等特性,而REQURIES_NEW则可以拥有子事务独立的隔离级别和锁等特性。

发布了322 篇原创文章 · 获赞 64 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yitian_z/article/details/104390812