什么是事务?
事务实际上是指一系列操作,程序中的事务大多时候是指数据库事务,那么这一系列操作就是数据库操作,通常包含多个 SQL 语句。
为了保证完整性,事务需要满足 ACID 原则:
(1) 原子性(Atomic)
一个事务无论包含多少操作,要么全部执行,要么全部不执行,若执行期间某个操作失败,则在其之前执行的操作都要回滚到事务执行前状态。
(2) 一致性(Consistency)
一个事务使系统从一个一致状态转换到另一个一致状态。
(3) 隔离性(Isolation)
事务执行过程中的数据变化只存在于该事务中,对外界不产生影响,只有该事务正常执行完毕后,其它事务才能获取到这些变化的数据。
(4) 持久性(Durability)
事务正常执行完毕后对数据的改变是永久性的。
本文重点在于 Spring Boot 中事务的使用,不再赘述事务相关的技术细节。
本文示例基于之前已介绍过的代码,如有不清楚还请参看:
Spring Boot 集成 MyBatis
Spring Boot 集成阿里巴巴 Druid 数据库连接池
Spring 在之前版本中早已提供事务管理的能力,Spring Boot 诞生后进一步简化了事务配置工作。如果添加了 spring-boot-starter-jdbc
依赖,框架会默认自动注入 DataSourceTransactionManager
;如果添加了 spring-boot-starter-data-jpa
依赖,框架会默认自动注入 JpaTransactionManager
。无需其它额外配置,直接在需要添加事务处理的方法上使用 @Transactional
注解。
1 定义 Service 接口
package demo.spring.boot.transaction.service;
import demo.spring.boot.transaction.domain.User;
public interface UserService {
void addUser(User user);
}
2 定义 Service 接口实现类
package demo.spring.boot.transaction.service.impl;
import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser(User user) {
userDao.insert(user);
}
}
3 编写单元测试
package demo.spring.boot.transaction;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDate;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
@Autowired
private UserService userService;
@Test
public void testAddUser() {
User user = new User();
user.setAccount("test_account");
user.setName("Test Name");
user.setBirth(LocalDate.now());
userService.addUser(user);
}
}
执行单元测试,测试通过,查询数据库可以看到刚插入的数据
mysql> select * from user \G;
*************************** 1. row ***************************
id: 24
account: test_account
name: Test Name
birth: 2018-08-03
1 row in set (0.00 sec)
4 对 Service 接口实现类的 addUser
方法稍作调整,在插入数据后执行一条肯定会抛出异常的语句,如下
@Override
public void addUser(User user) {
userDao.insert(user);
int x = 1 / 0;
}
删除数据库 user
表中记录(因为 account
字段添加了唯一性索引),再次执行单元测试,测试失败,失败原因是被测试方法抛出了异常 java.lang.ArithmeticException: / by zero
。
但是因为抛出异常在 DAO 执行插入操作之后,所以数据库中还是成功插入了数据,数据库查询结果如下:
mysql> select * from user \G;
*************************** 1. row ***************************
id: 25
account: test_account
name: Test Name
birth: 2018-08-03
1 row in set (0.00 sec)
5 在 Service 接口实现类的 addUser
方法中添加 @Transactional
注解实现事务管理
package demo.spring.boot.transaction.service.impl;
import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
@Transactional
public void addUser(User user) {
userDao.insert(user);
int x = 1 / 0;
}
}
执行和第 4 步相同步骤的单元测试,注意还是需要删除之前插入 user
表中记录,依旧因为异常原因导致单元测试失败,但是查询数据库 user
表无数据记录,说明异常抛出前 DAO 插入的数据已被成功回滚(省略数据库查询结果)。
注意:@Transactional
默认只回滚 Unchecked Exception,即 RuntimeException
,所有 Checked Exception 默认是不会滚的
示例:
首先,修改 addUser
方法,将 int x = 1 / 0;
替换成抛出 Checked Exception
package demo.spring.boot.transaction.service.impl;
import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
@Transactional
public void addUser(User user)
throws IOException {
userDao.insert(user);
throw new IOException();
}
}
其次,修改单元测试方法
@Test
public void testAddUser()
throws IOException {
User user = new User();
user.setAccount("test_account");
user.setName("Test Name");
user.setBirth(LocalDate.now());
userService.addUser(user);
}
最后,运行单元测试,测试失败,但是查询数据库仍看到抛出异常前插入的数据
mysql> select * from user \G;
*************************** 1. row ***************************
id: 28
account: test_account
name: Test Name
birth: 2018-08-03
1 row in set (0.00 sec)
@Transactional
指定异常回滚
查看 @Transactional
源码,rollbackFor
属性可以指定针对某些异常回滚(其它类似功能属性请参考 API 文档)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package 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 {
@AliasFor("transactionManager")
String value() default "";
@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 {};
}
修改 addUser
方法,添加 @Transactional
注解属性 rollbackFor
,指定当抛出 java.io.IOException
异常时回滚
package demo.spring.boot.transaction.service.impl;
import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
@Transactional(rollbackFor = {IOException.class})
public void addUser(User user)
throws IOException {
userDao.insert(user);
throw new IOException();
}
}
运行单元测试,测试失败,但是查询数据库 user
表无数据记录,说明异常抛出前 DAO 插入的数据已被成功回滚(省略数据库查询结果)。
如果赋予 rollbackFor
属性其它异常类型,既不是 java.io.IOException
又不是其父类,则运行单元测试后尽管测试失败,但是异常抛出前的数据也会被插入数据库 user
表中,请自行测试。
附
项目工程目录
POM文件
4.0.0
demo.spring.boot
demo-spring-boot-transaction
0.0.1-SNAPSHOT
jar
demo-spring-boot-transaction
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin