Spring框架是个脚手架,也就是帮助我们编程的工具框架,自然也提供方便的实现事务的方法,Spring的事务约等于是帮助我们实现数据库事务,它主要有两种使用方式。编程式事务管理和声明式事务。
前面讲过了如何使用使用xml中实现声明式事务,详见博客:Spring5_狂神(IOC,AOP,SpringMybatis,事务)Spring5_狂神(IOC,AOP,SpringMybatis,事务)_逮虾户<的博客-CSDN博客
我们通过xml配置文件很不方便,那么今天我们来学习通过 @Transactional注解实现Spring声明式事务,Spring声明式事务管理也有两种常用的方式:
一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
一. 使用位置
@Transactional
:使用在方法或者类的上面。
解释:使用在方法上,该方法支持事务,使用在类上,该类的所有方法都支持事务
二. 参数说明
参数 | 含义 | 备注 |
---|---|---|
value | 定义事务管理器 | Spring IoC容器里的一个Bean id,这Bean需要实现接口PlatformTransactionManager |
TransactionManager | 同上 | 同上 |
propagation | 事务传播行为 | 事务传播行为就是被调用者的事务与调用者的事务之间的关系。一般工作中设置为:@Transactional(propagation=propagation.REQUIRED),这也是默认值,所以不用设置 |
isolation | 事务隔离级别 | 隔离级别是一个数据库在多个事务同时存在时的概念,该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,默认值为:ISOLATION_DEFAULT (即数据库默认隔离级别),Mysql需要设置成ISOLATION_READ_COMMITTED 。 |
timeout | 事务超时时间1 | 单位为秒,发生超时时引发异常,默认会导致事务回滚,默认值为-1表示永不超时 |
readOnly | 是否开启只读事务2 | 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) |
rollbackFor | 回滚事务的异常类定义3 | 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如︰ 指定单一异常类:@Transactional(rollbackFor=RunTimeException.class); 指定多个异常类:@Transactional(rollbackForClassName={“RuntimeException”,“Exception”}) spring默认抛出RunTimeException 或 Error时回滚事务,其他异常不会回滚事务,如果在事务中抛出了其他异常,却希望spring能回滚事务,就需要指定rollbackFor属性,实际一般这样使用:@Transactional(rollbackFor=Exception.class);这个必须这么写 |
rollbackForClassName | 回滚事务的异常类名定义3 | 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如∶ 指定单―异常类名称@Transactional(rollbackForClassName=“RuntimeException”); 指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,“Exception”}) |
noRollbackFor | 当产生哪些异常不回滚事务3 | 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回。例如: 指定单一异常类:@Transactional(rollbackFor=RunTimeException.class); 指定多个异常类:@Transactional(rollbackForClassName={“RuntimeException”,“Exception”}) |
noRollbackForClassName | 当产生哪些异常类名不回滚事务3 | 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如: 指定―异常类名称:@Transactional(noRollbackForClassName=“RuntimeException”) 指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException” ,“Exception”}) |
三. Spring声明式事务传播行为
事务传播行为:如果在开始当前事务之前,一个事务上下文已经存在,此时有若干个选项可以指定一个事物性方法的执行行为。在TransactionalDefinition
定义中包括了如下几个表示传播行为常量:
TransactionalDefinition.PROPAGATION_REQUIRED
:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。该设置是默认值。TransactionalDefinition.PROPAGATION_REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。TransactionalDefinition.PROPAGATION_SUPPORTS
:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。TransactionalDefinition.PROPAGATION_NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。TransactionalDefinition.PROPAGATION_NEVER
:以非事务方式执行,如果当前存在事务,则抛出异常。TransactionalDefinition.PROPAGATION_MANDATORY
(强制的):如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。TransactionalDefinition.PROPAGATION_NESTED
(嵌套):如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则按REQUIRED属性执行。
PROPAGATION_REQUIRED 加入事务 propagation_required
PROPAGATION_REQUIRES_NEW 重新新建事务,挂起老的
PROPAGATION_SUPPORTS 存在就加入,不存在,非事务执行
PROPAGATION_NOT_SUPPORTED 非事务执行,存在就挂起
PROPAGATION_NEVER 不存在,非事务执行,存在抛异常
PROPAGATION_MANDATORY 存在就加入,不存在抛异常。 mandatory(强制)
PROPAGATION_NESTED 存在创建嵌套的事务,不存在,新建事务。 nested
四. 数据库的隔离级别
1. SQL标准规范
SQL标准规范把隔离级别定义为4层,分别是:
- 脏读(dirty read);
- 读/写提交(read commit);
- 可重复度(repeatable read);
- 序列化(serializable)
各类隔离级别对应产生的现象:
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
脏读 | Y | Y | Y |
读/写提交 | N | Y | Y |
可重复度 | N | N | Y |
序列化 | N | N | N |
2. 数据库事务隔离级别
事务隔离级别:指若干个并发的事务之间的隔离程度。TransactionDefinition
接口中定义了五个表示隔离级别的常量:
TransactionDefnion.ISOLATION_DEFAULT
:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常是TransactionDefinition.ISOLATION_READ_COMMITTED
(读已提交RC)。mysql默认是ISOLATION_REPEATABLE_READ(
可重复读RR),所以需要设置为ISOLATION_READ_COMMITTED。
Transactionboehnlion. ISOLATION_READ_UNCOMMITTED
∶(读取未提交RU)该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。TransactionDefinition.ISOLATION_READ_COMMITTED
:(读已提交RC)该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。TransactionDefinition.ISOLATION_REPEATABLE_READ
:(可重复读RR)该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。TransactionDefinition.ISOLATION_SERIALIZABLE
:(可串行化S)所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
五. 失效问题
在使用@Transactional
注解配置事务时,需要注意一些细节上的问题,避免@Transactional
注解的失效问题;
1. 非静态方法和公共方法
@Transactional
的底层实现是Spring AOP,而Spring AOP技术底层使用的是动态代理技术,也就是说使用@Transactional
注解的方法必须是非静态 (static
修饰)方法和public
修饰方法。
2. 自调用问题
一个类中的一个方法去调用另外一个方法的过程。
@Service
public class StudentListServiceImpl implements StudentListService {
private static final Logger log = LoggerFactory.getLogger(StudentListServiceImpl.class);
@Autowired
private StudentMapper studentMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@Override
public int insertStudent(Student student){
return studentMapper.insertStudent(student);
}
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@Override
public int insertStudentList(List<Student> studentList) {
int count = 0;
for (Student student: studentList){
try {
// 调用自身类的方法,产生自调用问题
count += insertStudent(student);
} catch (Exception e) {
log.info("系统异常:{}", e);
}
}
return count;
}
}
执行查看主要日志输出:
2021/04/05-13:55:38 [main] DEBUG org.springframework.jdbc.support.JdbcTransactionManager- Acquired Connection [HikariProxyConnection@315059566 wrapping com.mysql.cj.jdbc.ConnectionImpl@592a1882] for JDBC transaction
2021/04/05-13:55:38 [main] DEBUG org.springframework.jdbc.datasource.DataSourceUtils- Changing isolation level of JDBC Connection [HikariProxyConnection@315059566 wrapping com.mysql.cj.jdbc.ConnectionImpl@592a1882] to 2
2021/04/05-13:55:38 [main] DEBUG org.springframework.jdbc.support.JdbcTransactionManager- Switching JDBC Connection [HikariProxyConnection@315059566 wrapping com.mysql.cj.jdbc.ConnectionImpl@592a1882] to manual commit
2021/04/05-13:55:38 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Creating a new SqlSession
......
2021/04/05-13:55:38 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
......
2021/04/05-13:55:38 [main] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction- JDBC Connection [HikariProxyConnection@315059566 wrapping com.mysql.cj.jdbc.ConnectionImpl@592a1882] will be managed by Spring
......
2021/04/05-13:55:38 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- ==> Preparing: insert into student(name, number) values(?, ?)
2021/04/05-13:55:39 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- ==> Parameters: 学生1(String), 1(Integer)
2021/04/05-13:55:39 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- <== Updates: 1
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba] from current transaction
2021/04/05-13:55:39 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- ==> Preparing: insert into student(name, number) values(?, ?)
2021/04/05-13:55:39 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- ==> Parameters: 学生2(String), 2(Integer)
2021/04/05-13:55:39 [main] DEBUG cn.zhuzicc.spring.transactional.dao.StudentMapper.insertStudent- <== Updates: 1
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
2021/04/05-13:55:39 [main] DEBUG org.mybatis.spring.SqlSessionUtils- Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@247bbfba]
2021/04/05-13:55:39 [main] DEBUG org.springframework.jdbc.support.JdbcTransactionManager- Initiating transaction commit
来看看日志的具体内容:
1.获取连接592a1882给当前JDBC事务使用;
2.改变隔离级别为2,这里的2我理解的是TransactionDefinition中的ISOLATION_READ_COMMITTED;
3.改变当前事务的提交方式为手动提交;
4.创建了一个SqlSession会话;
5.为本次SqlSession会话注册事务同步;
7.将当前JDBC连接交给Spring管理;
17.事务同步释放SqlSession会话;
18.事务同步提交SqlSession会话;
19.事务同步注销SqlSession会话;
20.事务同步关闭SqlSession会话;
21.启动事务提交;
从日志中可以看到数据插入两次都使用了同一个事务,说明insertStudent()
方法上标注的@Transactional
失效,出现这个问题的根本原因就在于AOP的实现原理,AOP实现原理是动态代理,而上述示例代码是本类自己调用自己的过程,并不存在代理对象的调用,所以就不会产生AOP去设置insertStudent()
方法上标注的@Transactional
参数。
第3行和第21行是Spring 关闭数据库中自动提交,改为手动提交,在方法执行前关闭自动提交,方法执行完毕后再开启自动提交;org.springframework.jdbc.datasource.DataSourceTransactionManager.java
相关源码展示:
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
3. 错误捕获异常
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@Override
public int insertStudentList(List<Student> studentList) {
int count = 0;
for (Student student: studentList){
try {
count += studentService.insertStudent(student);
if (count == studentList.size()){
studentService.allotStudent(student);
}
} catch (Exception e) {
log.info("系统异常:{}", e);
}
}
return count;
}
场景:在上例方法中分别进行了添加学生和分配学生的操作,并且进行了try…catch异常捕获,当产生异常时就可能会出现添加学生成功,但是在分配学生时产生异常,此时的Spring依然会照常提交事务,因为Spring本该在数据库事务所约定的流程中获取到方法抛出的异常,但是异常信息已经被方法自定义的catch所捕获,所以现在却获取不到任何异常信息。这样就会导致数据错误提交,产生严重的生产事故。
代码改造:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@Override
public int insertStudentList(List<Student> studentList) {
int count = 0;
for (Student student: studentList){
try {
count += studentService.insertStudent(student);
if (count == studentList.size()){
studentService.allotStudent(student);
}
} catch (Exception e) {
log.info("系统异常:{}", e);
// 自行抛出异常,让spring事务管理流程获取到方法抛出的异常,从而进行事务管理
throw new RuntimeException(e);
}
}
return count;
}
-
事务超时:指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回浪事务,在
Transactonbeiatlon
中i以int
的值来表示超时的时间,其单位是秒,默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。 ↩︎ -
只读事务用户代码只读取数据,不修改数据的情形,只读事务用于特定情境下的优化。 ↩︎
注意点:
1、SpringBoot项目会自动配置一个 DataSourceTransactionManager,所以我们只需在方法(或者类)加上 @Transactional 注解,就自动纳入 Spring 的事务管理了
2、虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略(无效),也不会抛出任何异常。
3、Spring的事务在抛异常的时候会回滚,如果是catch捕获了,事务无效。可以在catch里面加上throw new RuntimeException()把异常抛出
4、和锁同时使用需要注意:由于Spring事务是通过AOP实现的,所以在方法执行之前会有开启事务,之后会有提交事务逻辑。而synchronized代码块执行是在事务之内执行的,可以推断在synchronized代码块执行完时,事务还未提交,其他线程进入synchronized代码块后,读取的数据不是最新的。
所以必须使synchronized锁的范围大于事务控制的范围,把synchronized加到Controller层或者大于事务边界的调用层!
5、该@transactional注解一般使用在service层,偶尔使用在controller层