一、简介
1、介绍
事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。
2、事务特点(ACID)
-
原子性(Atomicity):整个事务是一个整体,不可分割的最小工作单位。一个事务中的所有操作要么全部执行成功,要么全部都不执行。其中任何一条语句执行失败,都会导致事务回滚;
-
一致性(Consistency):数据库的记录总是从一个一致性状态转变成另一个一致性状态;
-
隔离性(Isolation):一个事物的执行,不受其他事务的干扰,即并发执行的事物之间互不干扰;
-
持久性(Durability):数据一旦提交,结果就是永久性的。并不应为宕机等情况丢失。一般理解就是写入硬盘保存成功。
3、事务实现方式
3.1 MySql事务实现方式
-
原子性和持久性利用redo log(重做日志) 实现
-
一致性利用undo log(回滚日志)实现
-
隔离性利用锁来实现
3.2 SpringBoot实现机制
Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。
编程式事务管理: 编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate
声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用!!!
声明式事务有两种方式:
一种是在配置文件(xml)中做相关的事务规则声明
另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,推荐使用
在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor来使用拦截,在 TransactionInterceptor拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务
Spring AOP 代理有 CglibAopProxy和 JdkDynamicAopProxy两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor的 intercept方法。对于 JdkDynamicAopProxy,需要调用其 invoke方法。
二、@Transactional详解
1、@Transactional常用配置
2、事务传播行为
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。
例如:methodA方法调用methodB方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。Propagation枚举则引用了这些类型,开发过程中我们一般直接用Propagation枚举。例如@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),常用的三项已经加粗。
说明:加入该事务,指的是父、子方法共用一个事务(无论父、子方法报错,整体回滚)
数据准备
准备t_a数据表:
准备t_b数据表:
实体类TableA:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableA {
private Integer id;
private String word;
}
实体类TableB:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableB {
private Integer id;
private String word;
}
TableAMapper接口:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableB {
private Integer id;
private String word;
}
TableBMapper接口:
@Mapper
public interface TableBMapper {
/**
* 向t_b中插入一条数据
* @param tableB
*/
void add(TableB tableB);
}
TableAMapper.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dw.transactional.mapper.TableAMapper">
<!-- 向t_a中插入一条数据 -->
<insert id="add">
insert into t_a values(#{id}, #{word})
</insert>
</mapper>
TableBMapper.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dw.transactional.mapper.TableBMapper">
<!-- 向t_b中插入一条数据 -->
<insert id="add">
insert into t_b values(#{id}, #{word})
</insert>
</mapper>
2.1、REQUIRED案例
Propagation.REQUIRED:需要事务(默认)。若当前无事务,新建一个事务;若当前有事务,加入此事务中。
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper mapper;
@Transactional
public void insertTableBData(TableB tableB){
mapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper mapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
mapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则t_a数据插入成功,t_b数据回滚;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。
总结:
父方法无事务,则子方法开启新事务;
若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)
2.2、SUPPORTS案例
Propagation.SUPPORTS:支持事务。若当前没有事务以非事务方式执行;若当前有事务,加入此事务中。
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper mapper;
@Transactional
public void insertTableBData(TableB tableB){
mapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper mapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
mapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据插入成功,子方法抛出异常;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。
总结:
父方法无事务,则子方法则以非事物的方式执行(未开启事务);
若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)
2.3、MANDATORY案例
Propagation.MANDATORY:强制使用事务。若当前有事务,就使用当前事务;若当前没有事务,抛出IllegalTransactionStateException异常。
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper bMapper;
@Transactional(propagation = Propagation.MANDATORY)
public void insertTableBData(TableB tableB){
bMapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper aMapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
aMapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则父方法以非事务方式执行,当执行到子方法时就会立即抛出IllegalTransactionStateException异常;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。
总结:
父方法无事务,则父方法以非事务方式执行,并会抛出异常;
若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)
2.4、REQUIRES_NEW案例
Propagation.REQUIRES_NEW:新建事务。无论当前是否有事务,都新建事务运行。
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper bMapper;
@Transactional(propagation = Propagation.MANDATORY)
public void insertTableBData(TableB tableB){
bMapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper aMapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
aMapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则父方法以非事务方式执行,t_b数据回滚;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a数据回滚,t_b插入成功;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。
总结:
无论父方法有没有事务,子方法都创建新事务
2.5、NOT_SUPPORTED案例
Propagation.NOT_SUPPORTED:不支持事务。若当前存在事务,把当前事务挂起,然后运行方法
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper bMapper;
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void insertTableBData(TableB tableB){
bMapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper aMapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
aMapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则t_a和t_b插入成功;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a数据回滚,t_b插入成功;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a数据回滚,t_b插入成功;
总结:
若父方法无事务,则子方法以非事务方式执行;
若父方法有事务,父方法以事务方式执行,子方法以非事务方式执行
2.6、NEVER案例
Propagation.NEVER:不使用事务。若当前方法存在事务,则抛出IllegalTransactionStateException异常,否则继续使用无事务机制运行
2.7、NESTED案例
Propagation.NESTED:嵌套。如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与REQUIRED类似的操作
代码准备:
@Service
public class TableBService {
@Resource
TableBMapper bMapper;
@Transactional(propagation = Propagation.NESTED)
public void insertTableBData(TableB tableB){
bMapper.add(tableB);
// 抛出异常
// throw new RuntimeException("子方法抛出异常~~~~~");
}
}
@Service
public class TableAService {
@Resource
TableAMapper aMapper;
@Resource
TableBService bService;
@Transactional
public void insertTableAData(TableA tableA){
aMapper.add(tableA);
// 调用 TableBService 中带有事务的方法
bService.insertTableBData(new TableB(1, "bbb"));
// 抛出异常
// throw new RuntimeException("父方法抛出异常!!!");
}
}
各个情况的结果:
-
父方法无事务,子方法开启事务,子方法抛出异常,则t_a插入成功,t_b数据回滚;
-
父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚;
-
父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;
-
父方法开启事务,子方法开启事务,子方法报错,父方法并捕获,则t_a插入成功,t_b数据回滚
总结:
如果父方法没有事务,则子方法新开事务执行 如果父方法存在事务,则子方法在嵌套事务内执行
区别总结
1、NESTED和REQUIRES区别
区别的场景:当前存在事务,子方法抛异常 NESTED和REQUIRES_NEW在父方法可以选择捕获子方法,父方法数据不会回滚 REQUIRES无论捕不捕获,父方法数据都回滚
2、NESTED和REQUIRES_NEW区别
区别的场景:当前存在事务,父方法抛异常时 NESTED数据回滚,REQUIRES也是如此 REQUIRES_NEW数据不回滚
3、事务5种隔离级别
例如:@Transactional(isolation = Isolation.READ_COMMITTED)
三、事务使用事项与场景
1、事务使用注意事项
① 在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上
② @Transactional 注解应该只被应用在 public 修饰的方法上(注意)。 如果在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会抛出异常 (IDEA会有提示), 但事务并没有生效
③ @Transactional是基于动态代理的(注意),需要一个类调用另一个类,类内调用会失效
④ 被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:
被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制
被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务:事务由子方法B和C各自控制
被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,则即使子方法B和C各自声明事务,事务也不会生效,并且会抛出异常(没有可用的transactional)
被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!
如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:
方案1:子方法中不用 try-catch 处理运行异常
方案2:子方法的catch里面将运行异常抛出throw new RuntimeException();
⑤ 默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚;如果是checked异常则不回滚,例 如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型: @Transactional(rollbackFor = Exception.class)
⑥ 数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务
⑦ 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚 或者在catch里面将异常抛出throw new RuntimeException();有两种方案:
方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值)
try{ .... }catch(Exception e){ logger.error("fail",e); throw new RuntimeException; }
方案二:手动进行回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
try{ ... }catch(Exception e){ log.error("fail",e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; }