Spring Boot中的事务管理

负载均衡和容错,限流,降级。这些。

什么是事务?

我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,需要在这种情况下进行回退。

事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。

事务管理是 Spring 框架中最为常用的功能之一,我们在使用 Spring Boot 开发应用时,大部分情况下也都需要使用事务。

快速入门

在 Spring Boot 中,当我们使用了 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 依赖的时候,框 架会自动默认分别注入 DataSourceTransactionManager 或 JpaTransactionManager。所以我们不需要任何额外 配置就可以用 @Transactional 注解进行事务的使用。

在该样例工程中(若对该数据访问方式不了解,可先阅读该文章),我们引入了 spring-data-jpa,并创建了 User 实体以及对 User 的数据访 问对象 UserRepository,在 ApplicationTest 类中实现了使用 UserRepository 进行数据读写的单元测试用例,如下:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> UserRepository userRepository;

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">test</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {

    <span class="hljs-comment">// 创建10条记录</span>
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"AAA"</span>, <span class="hljs-number">10</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"BBB"</span>, <span class="hljs-number">20</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"CCC"</span>, <span class="hljs-number">30</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"DDD"</span>, <span class="hljs-number">40</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"EEE"</span>, <span class="hljs-number">50</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"FFF"</span>, <span class="hljs-number">60</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"GGG"</span>, <span class="hljs-number">70</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"HHH"</span>, <span class="hljs-number">80</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"III"</span>, <span class="hljs-number">90</span>));
    userRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"JJJ"</span>, <span class="hljs-number">100</span>));

    <span class="hljs-comment">// 省略后续的一些验证操作</span>
}

}

可以看到,在这个单元测试用例中,使用 UserRepository 对象连续创建了 10 个 User 实体到数据库中,下面我们人为的来制造一些异常,看看会发生什么情况。

通过定义 User 的 name 属性长度为 5,这样通过创建时 User 实体的 name 属性超长就可以触发异常产生。

@Entity
public class User {
<span class="hljs-meta">@Id</span>
<span class="hljs-meta">@GeneratedValue</span>
<span class="hljs-keyword">private</span> Long id;

<span class="hljs-meta">@Column(nullable = false, length = 5)</span>
<span class="hljs-keyword">private</span> String name;

<span class="hljs-meta">@Column(nullable = false)</span>
<span class="hljs-keyword">private</span> Integer age;

<span class="hljs-comment">// 省略构造函数、getter和setter</span>

}

修改测试用例中创建记录的语句,将一条记录的 name 长度超过 5,如下:name 为 HHHHHHHHH 的 User 对象将会抛出异常。

// 创建 10 条记录
userRepository.save(new User("AAA", 10));  
userRepository.save(new User("BBB", 20));  
userRepository.save(new User("CCC", 30));  
userRepository.save(new User("DDD", 40));  
userRepository.save(new User("EEE", 50));  
userRepository.save(new User("FFF", 60));  
userRepository.save(new User("GGG", 70));  
userRepository.save(new User("HHHHHHHHHH", 80));  
userRepository.save(new User("III", 90));  
userRepository.save(new User("JJJ", 100));

执行测试用例,可以看到控制台中抛出了如下异常,name 字段超长:

2016-05-27 10:30:35.948  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1406, SQLState: 22001  
2016-05-27 10:30:35.948 ERROR 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Data truncation: Data too long for column 'name' at row 1  
2016-05-27 10:30:35.951  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Warning Code: 1406, SQLState: HY000  
2016-05-27 10:30:35.951  WARN 2660 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Data too long for column 'name' at row 1

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement

此时查数据库中,创建了 name 从 AAA 到 GGG 的记录,没有 HHHHHHHHHH、III、JJJ 的记录。而若这是一个希望保证完整性操作的情况 下,AAA 到 GGG 的记录希望能在发生异常的时候被回退,这时候就可以使用事务让它实现回退,做法非常简单,我们只需要在 test 函数上添加 @Transactional 注解即可。

@Test
@Transactional
public void test() throws Exception {
<span class="hljs-comment">// 省略测试内容</span>

}

这里主要通过单元测试演示了如何使用 @Transactional 注解来声明一个函数需要被事务管理,通常我们单元测试为了保证每个测试之间的数据独立,会使用 @Rollback 注解让每个单元测试都能在结束时回滚。而真正在开发业务逻辑时,我们通常在 service 层接口中使用 @Transactional 来对各个业务逻辑进行事务管理的配置,例如:

public interface UserService {
<span class="hljs-meta">@Transactional</span>
User <span class="hljs-title function_">login</span><span class="hljs-params">(String name, String password)</span>;

}
=====================================================
SpringBoot 事物的使用

spring Boot 使用事务非常简单,首先使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的 Service 方法上添加注解 @Transactional 便可。

关于事务管理器,不管是 JPA 还是 JDBC 等都实现自接口 PlatformTransactionManager 如果你添加的是 spring-boot-starter-jdbc 依赖,框架会默认注入 DataSourceTransactionManager 实例。如果你添加的是 spring-boot-starter-data-jpa 依赖,框架会默认注入 JpaTransactionManager 实例。

你可以在启动类中添加如下方法,Debug 测试,就能知道自动注入的是 PlatformTransactionManager 接口的哪个实现类。

@EnableTransactionManagement // 启注解事务管理,等同于 xml 配置方式的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication {
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> Object <span class="hljs-title function_">testBean</span><span class="hljs-params">(PlatformTransactionManager platformTransactionManager)</span>{
    System.out.println(<span class="hljs-string">"&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;"</span> + platformTransactionManager.getClass().getName());
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>();
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    SpringApplication.run(ProfiledemoApplication.class, args);
}

}

 

 

这些 SpringBoot 为我们自动做了,这些对我们并不透明,如果你项目做的比较大,添加的持久化依赖比较多,我们还是会选择人为的指定使用哪个事务管理器。 
代码如下:

@EnableTransactionManagement
@SpringBootApplication
public class ProfiledemoApplication {
<span class="hljs-comment">// 其中 dataSource 框架会自动为我们注入</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> PlatformTransactionManager <span class="hljs-title function_">txManager</span><span class="hljs-params">(DataSource dataSource)</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DataSourceTransactionManager</span>(dataSource);
}

<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> Object <span class="hljs-title function_">testBean</span><span class="hljs-params">(PlatformTransactionManager platformTransactionManager)</span> {
    System.out.println(<span class="hljs-string">"&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;"</span> + platformTransactionManager.getClass().getName());
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>();
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    SpringApplication.run(ProfiledemoApplication.class, args);
}

}

 

 

在 Spring 容器中,我们手工注解 @Bean 将被优先加载,框架不会重新实例化其他的 PlatformTransactionManager 实现类。

然后在 Service 中,被 @Transactional 注解的方法,将支持事务。如果注解在类上,则整个类的所有方法都默认支持事务。

对于同一个工程中存在多个事务管理器要怎么处理,请看下面的实例,具体说明请看代码中的注释。

@EnableTransactionManagement // 开启注解事务管理,等同于 xml 配置文件中的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication implements TransactionManagementConfigurer {
<span class="hljs-meta">@Resource(name="txManager2")</span>
<span class="hljs-keyword">private</span> PlatformTransactionManager txManager2;

<span class="hljs-comment">// 创建事务管理器1</span>
<span class="hljs-meta">@Bean(name = "txManager1")</span>
<span class="hljs-keyword">public</span> PlatformTransactionManager <span class="hljs-title function_">txManager</span><span class="hljs-params">(DataSource dataSource)</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DataSourceTransactionManager</span>(dataSource);
}

<span class="hljs-comment">// 创建事务管理器2</span>
<span class="hljs-meta">@Bean(name = "txManager2")</span>
<span class="hljs-keyword">public</span> PlatformTransactionManager <span class="hljs-title function_">txManager2</span><span class="hljs-params">(EntityManagerFactory factory)</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JpaTransactionManager</span>(factory);
}

<span class="hljs-comment">// 实现接口 TransactionManagementConfigurer 方法,其返回值代表在拥有多个事务管理器的情况下默认使用的事务管理器</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> PlatformTransactionManager <span class="hljs-title function_">annotationDrivenTransactionManager</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> txManager2;
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    SpringApplication.run(ProfiledemoApplication.class, args);
}

}


@Component
public class DevSendMessage implements SendMessage {

    // 使用 value 具体指定使用哪个事务管理器
    @Transactional(value="txManager1")
    @Override
    public void send() {
        System.out.println(">>>>>>>>Dev Send()<<<<<<<<");
        send2();
    }

    // 在存在多个事务管理器的情况下,如果使用 value 具体指定
    // 则默认使用方法 annotationDrivenTransactionManager() 返回的事务管理器
    @Transactional
    public void send2() {
        System.out.println(">>>>>>>>Dev Send2()<<<<<<<<");
    }

}

 

注: 
如果 Spring 容器中存在多个 PlatformTransactionManager 实例,并且没有实现接口 TransactionManagementConfigurer 指定默认值,在我们在方法上使用注解 @Transactional 的时候,就必须要用 value 指定,如果不指定,则会抛出异常。

对于系统需要提供默认事务管理的情况下,实现接口 TransactionManagementConfigurer 指定。

对有的系统,为了避免不必要的问题,在业务中必须要明确指定 @Transactional 的 value 值的情况下。不建议实现接口 TransactionManagementConfigurer,这样控制台会明确抛出异常,开发人员就不会忘记主动指定。

事务详解

上面的例子中我们使用了默认的事务配置,可以满足一些基本的事务需求,但是当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。对于不同数据源的事务管理配置可以见 《Spring Boot 多数据源配置与使用》 中的设置。在声明事务时,只需要通过 value 属性指定配置的事务管理器名即可,例如:@Transactional(value="transactionManagerPrimary") 。

除了指定不同的事务管理器之后,还能对事务进行隔离级别和传播行为的控制,下面分别详细解释:

隔离级别

隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。

我们可以看 org.springframework.transaction.annotation.Isolation 枚举类中定义了五个表示隔离级别的值:

public enum Isolation {  
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT :这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是: READ_COMMITTED 。
  • READ_UNCOMMITTED :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
  • READ_COMMITTED :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  • REPEATABLE_READ :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
  • SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

指定方法:通过使用 isolation 属性设置,例如:

@Transactional(isolation = Isolation.DEFAULT)

传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

我们可以看 org.springframework.transaction.annotation.Propagation 枚举类中定义了 6 个表示传播行为的枚举值:

public enum Propagation {  
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。
  • NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED 。

指定方法:通过使用 propagation 属性设置,例如:

@Transactional(propagation = Propagation.REQUIRED)