SpringBoot事务Transaction 实例详解讲解
开发准备
环境要求
JDK:1.8
SpringBoot:1.5.17.RELEASE
首先还是Maven的相关依赖:
pom.xml文件如下:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.17.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<!-- Spring Boot Web 依赖 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!-- MySQL 连接驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<!-- Druid 数据连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
</dependencies>
application.properties
的文件的配置:
banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.application.name=springboot-transactional
server.port=8182
spring.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
logging.level.com.pancm.dao=debug
代码编写
SpringBoot在使用事务Transactional的时候,要在main方法上加上 @EnableTransactionManagement
注解开发事务声明,在使用的service层的公共方法加上 @Transactional
(spring)注解。
使用示例一
那么首先我们来看下 @Transactional
这个注解的使用方法吧,只需要你在需要添加公共方法上面添加该注解即可。但是这么使用的话需要你将异常抛出,由spring进行去控制。
代码示例:
@Transactional
public boolean test1(User user) throws Exception {
long id = user.getId();
System.out.println("查询的数据1:" + udao.findById(id));
// 新增两次,会出现主键ID冲突,看是否可以回滚该条数据
udao.insert(user);
System.out.println("查询的数据2:" + udao.findById(id));
udao.insert(user);
return false;
}
使用示例二
如果我们在使用事务 @Transactional
的时候,想自己对异常进行处理的话,那么我们可以进行手动回滚事务。在catch中加上 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
方法进行手动回滚。不过需要注意的是发生异常需要第一时间进行手动回滚事务,也就是要在异常抛出之前!
代码示例:
@Transactional
public boolean test2(User user) {
long id = user.getId();
try {
System.out.println("查询的数据1:" + udao.findById(id));
// 新增两次,会出现主键ID冲突,看是否可以回滚该条数据
udao.insert(user);
System.out.println("查询的数据2:" + udao.findById(id));
udao.insert(user);
} catch (Exception e) {
System.out.println("发生异常,进行手动回滚!");
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return false;
}
使用示例三
如果我们在使用事务 @Transactional
的时候,调用了其他的子方法进行了数据库的操作,但是我们想使其事务生效的话,我们可以使用rollbackFor
注解或者将该子方法的异常抛出由调用的方法进行处理,不过这里需要注意的是,子方法也必须是公共的方法!
代码示例:
@Transactional
public boolean test3(User user) {
/*
* 子方法出现异常进行回滚
*/
try {
System.out.println("查询的数据1:" + udao.findById(user.getId()));
deal1(user);
deal2(user);
deal3(user);
} catch (Exception e) {
System.out.println("发生异常,进行手动回滚!");
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return false;
}
public void deal1(User user) throws SQLException {
udao.insert(user);
System.out.println("查询的数据2:" + udao.findById(user.getId()));
}
public void deal2(User user) throws SQLException{
if(user.getAge()<20){
//SQL异常
udao.insert(user);
}else{
user.setAge(21);
udao.update(user);
System.out.println("查询的数据3:" + udao.findById(user.getId()));
}
}
@Transactional(rollbackFor = SQLException.class)
public void deal3(User user) {
if(user.getAge()>20){
//SQL异常
udao.insert(user);
}
}
使用示例四
如果我们不想使用事务 @Transactional
注解,想自己进行事务控制(编程事务管理),控制某一段的代码事务生效,但是又不想自己去编写那么多的代码,那么可以使用springboot中的DataSourceTransactionManager
和TransactionDefinition
这两个类来结合使用,能够达到手动控制事务的提交回滚。不过在进行使用的时候,需要注意在回滚的时候,要确保开启了事务但是未提交,如果未开启或已提交的时候进行回滚是会在catch里面发生异常的!
代码示例:
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
public boolean test4(User user) {
/*
* 手动进行事务控制
*/
TransactionStatus transactionStatus=null;
boolean isCommit = false;
try {
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
System.out.println("查询的数据1:" + udao.findById(user.getId()));
// 进行新增/修改
udao.insert(user);
System.out.println("查询的数据2:" + udao.findById(user.getId()));
if(user.getAge()<20) {
user.setAge(user.getAge()+2);
udao.update(user);
System.out.println("查询的数据3:" + udao.findById(user.getId()));
}else {
throw new Exception("模拟一个异常!");
}
//手动提交
dataSourceTransactionManager.commit(transactionStatus);
isCommit= true;
System.out.println("手动提交事务成功!");
throw new Exception("模拟第二个异常!");
} catch (Exception e) {
//如果未提交就进行回滚
if(!isCommit){
System.out.println("发生异常,进行手动回滚!");
//手动回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
}
e.printStackTrace();
}
return false;
}
使用实例五
上述的这几种示例是比较常见使用的,基本可以满足日常我们对事务的使用,spring里面还有一种事务的控制方法,就是设置断点进行回滚。但是这种方法个人还没实际验证过,可靠性待确认。
使用方法如下:
Object savePoint =null;
try{
//设置回滚点
savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
}catch(Exception e){
//出现异常回滚到savePoint。
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
}
上面的使用示例介绍完毕之后,我们再来介绍一下几个主要的类。
首先还是实体类:
实体类
又是万能的用户表
public class User {
private Long id;
private String name;
private Integer age;
//getter 和 setter 略
}
Controller 控制层
然后便是控制层,控制层这块的我做了下最后的查询,用于校验事务是否成功生效!
控制层代码如下:
@RestController
@RequestMapping(value = "/api/user")
public class UserRestController {
@Autowired
private UserService userService;
@Autowired
private UserDao userDao;
@PostMapping("/test1")
public boolean test1(@RequestBody User user) {
System.out.println("请求参数:" + user);
try {
userService.test1(user);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("最后查询的数据:" + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test2")
public boolean test2(@RequestBody User user) {
System.out.println("请求参数:" + user);
userService.test2(user);
System.out.println("最后查询的数据:" + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test3")
public boolean test3(@RequestBody User user) {
System.out.println("请求参数:" + user);
userService.test3(user);
System.out.println("最后查询的数据:" + userDao.findById(user.getId()));
return true;
}
@PostMapping("/test4")
public boolean test4(@RequestBody User user) {
System.out.println("请求参数:" + user);
userService.test4(user);
System.out.println("最后查询的数据:" + userDao.findById(user.getId()));
return true;
}
}
App 入口
和普通的SpringBoot项目基本一样,只不过需要加上 @EnableTransactionManagement
注解!
代码如下:
@EnableTransactionManagement
@SpringBootApplication
public class TransactionalApp
{
public static void main( String[] args )
{
SpringApplication.run(TransactionalApp.class, args);
System.out.println("Transactional 程序正在运行...");
}
}
功能测试
我们在启动程序之后,来进行上述的几个示例测试,这里的测试示例分别对应上述的使用示例,有的示例需要测试两边以上才能验证事务是否能够生效!这里我们使用Postman进行测试!
测试示例一
两次测试,第一次不使用@Transactional
注解,第二次使用!
第一次测试:
注释掉@Transactional
注解!
使用进行POST请求
http://localhost:8182/api/user/test1
Body参数为:
{"id":1,"name":"xuwujing","age":18}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=18]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
最后查询的数据:User [id=1, name=xuwujing, age=18]
第二次测试:
解除@Transactional
注解注释!
使用进行POST请求
http://localhost:8182/api/user/test1
Body参数为:
{"id":1,"name":"xuwujing","age":18}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=18]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=18]
Duplicate entry '1' for key 'PRIMARY'
最后查询的数据:null
注: 在第二次测试的之前是把第一次测试写入数据库的id为1的数据个删除了!
第一次测试中由于没有添加@Transactional
注解,因此发生了异常数据还是写入了,但是第二次测试中添加了@Transactional
注解,发现即使数据已经写入了,但是出现了异常之后,数据最终被回滚了,没有写入!
从上述的测试用例中可以看到测试用例一种的事务已经生效了!
测试示例二
由于使用示例二中的代码几乎和使用示例一种的一样,不同的是异常由我们自己进行控制!
使用进行POST请求
http://localhost:8182/api/user/test2
Body参数为:
{"id":1,"name":"xuwujing","age":18}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=18]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=18]
发生异常,进行手动回滚!
Duplicate entry '1' for key 'PRIMARY'
最后查询的数据:null
可以看到事务生效了!
测试示例三
由于使用示例三中进行了子方法调用,这里我们进行两次测试,根据不同的请求条件来进行测试!
第一次测试:
使用进行POST请求
http://localhost:8182/api/user/test3
Body参数为:
{"id":1,"name":"xuwujing","age":18}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=18]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=18]
发生异常,进行手动回滚!
Duplicate entry '1' for key 'PRIMARY'
最后查询的数据:null
第二次测试:
使用进行POST请求
http://localhost:8182/api/user/test3
Body参数为:
{"id":1,"name":"xuwujing","age":21}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=21]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=21]
查询的数据3:User [id=1, name=xuwujing2, age=21]
发生异常,进行手动回滚!
Duplicate entry '1' for key 'PRIMARY'
最后查询的数据:null
根据上述的两次测试,可以得出使用rollbackFor
注解或者将该子方法的异常抛出由调用的方法进行处理都可以使事务生效!
测试示例四
由于使用示例四中进行了手动控制事务,这里我们进行两次测试,根据不同的请求条件来进行测试!
第一次测试:
使用进行POST请求
http://localhost:8182/api/user/test4
Body参数为:
{"id":1,"name":"xuwujing","age":18}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=18]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=18]
查询的数据3:User [id=1, name=xuwujing2, age=20]
手动提交事务成功!
模拟第二个异常!
最后查询的数据:User [id=1, name=xuwujing, age=20]
第二次测试:
事先还是把数据库id为1的数据给删除!
使用进行POST请求
http://localhost:8182/api/user/test4
Body参数为:
{"id":1,"name":"xuwujing","age":21}
控制台打印的数据:
请求参数:User [id=1, name=xuwujing, age=21]
查询的数据1:null
查询的数据2:User [id=1, name=xuwujing, age=21]
发生异常,进行手动回滚!
模拟一个异常!
最后查询的数据:null
根据上述的两次测试,我们可以得出使用手动控制事务完全ok,只要提交了事务,即使后面发生了异常也不回影响之前的写入!如果在控制的范围之类发生了异常,也可以进行回滚!
测试示例图: