在当今的软件开发领域,事务管理是确保数据完整性和一致性的关键环节。而 Spring 事务,作为一个强大且广泛应用的事务管理机制,在构建可靠、高效的应用程序中发挥着至关重要的作用。
当我们踏上对 Spring 事务的探索之旅,就仿佛打开了一扇通往复杂而又精妙世界的大门。在这里,我们将见证事务如何在幕后默默工作,协调各种操作,保障数据的准确性和稳定性。它如同一位无声的守护者,精心呵护着系统的运行。
无论是应对复杂的业务逻辑还是处理大规模的数据交互,Spring 事务都展现出了其非凡的能力和灵活性。通过深入剖析它的原理、特性和应用场景,我们能够更好地理解如何充分发挥其优势,解决实际开发中面临的诸多挑战。让我们一同开启这场精彩的旅程,去揭开 Spring 事务那神秘的面纱,探寻其中蕴含的无尽智慧与可能。
一、详解Spring中的事务
1.什么是事务
事务在逻辑上可以认为就是把一组操作看作一个动作。这个动作的内容要么都成功,要么都失败,这样才能保证结果的准确性、一致性。
如下代码所示,如果下面这段代码两个插入操作不属于同一个事务的话,结束时只有张三被插入和李四没有插入,不符合业务上的准确性。
使用事务进行数据库增删改查操作时,必须保证当前使用的数据库引擎支持事务,以MySQL为例,MySQL默认引擎为innodb,他就是支持事务的。若时myisam则不支持事务,无法实现数据回滚。
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
//报错
throw new RuntimeException();
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
}
2.事务的特性ACID简介
英文翻译成中文大致是:原子性、隔离性、一致性、持久性。分别代表的含义是:
- 原子性(Atomicity): 属于同一个事务的操作要么都成功,要么都失败,直接回滚,数据库的数据像是没有被动过一样。
- 一致性(Consistency):在事务开始前和事务结束后,数据库的完整性没有被破坏,即操作符合数据库级联回滚、预设约束、触发器要求。
- 隔离性(Isolation)数据库允许并发操作,使用准确的隔离性原则才能保证数据一致性,而隔离级别有:读未提交(Read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(Serializable)。
- 持久性(Durability):持久化的数据不会丢失,即使系统发生故障。
3.Spring支持的两种事务管理
有两种姿势,分别是手动式事务和注解式事务,前者是手动的,比较少使用,对应的类是TransactionTemplate或者TransactionManager,使用示例如下所示:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
或者下面这样一段代码,都是通过都是传入需要进行事务管理的bean定义,进行手动操作管理:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
而后者就比较常用了,基于注解(底层是通过AOP实现的),使用的示例代码如下所示:
@Transactional
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
4.Spring事务隔离级别和传播行为有哪些(笔试常问)
先来说说事务隔离级别:
- default(默认):PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,除了default 其它几个Spring事务隔离级别与JDBC事务隔离级别相对应。
- read_uncommited(读未提交):一个事务可以读取另外一个事务未提交的数据,这可能出现脏读 而且不可重复度,出现幻像读等。
- read_commited(读已提交):一个事务可以读取另一个事务已经提交的数据,不可以读取未提交的数据 可以避免脏读 但是无法避免不可重复读和幻像读。
- repeatTable_read(可重复读):一个事务可以读取另外一个事务已经提交的数据,可以避免脏读的前提下 ,也可以避免不可重复读,但是还是无法避免幻像读。
- serializable(串行化):这是一个花费较高但是比较可靠的事务隔离级别,可以避免脏读 幻像读和不可重复读(事务被处理为顺序执行)
Spring事务传播属性:
- required(默认属性):Propagation.REQUIRED内外部属于统一事务,一个回滚全部回滚。(后文会有代码演示)
- Mandatory:如果当前存在事务,则支持当前事务,如果不存在事务,则抛出异常
- Never:以非事务方式执行,如果当前存在事务,则抛出异常
- Supports:如果当前存在事务,则支持当前事务,.如果不存在事务,以非事务方式执行
- Not_Supports:以非事务方式执行操作,如果存在事务,则挂起当前事务
- required_new:在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
- Nested:嵌套,支持当前事务,内层事务的执行失败不会导致外层事务的回滚,但是外层事务的回滚会影响内层事务导致内层事务随外层事务一同回滚.
二、事务传播行为进阶知识
1.前置铺垫,代码示例编写
为了更好的解答后续的问题,这里我们给出了一个示例,user1表的service:
@Service
public class User1Service {
@Resource
private User1Mapper user1Mapper;
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User1 user){
user1Mapper.insert(user);
}
@Transactional(propagation = Propagation.NESTED)
public void addNested(User1 user){
user1Mapper.insert(user);
}
}
user2表的service,可以看到对于数据库的操作都在注解上标出不同的传播行为:
@Service
public class User2Service {
@Resource
private User2Mapper user2Mapper;
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRED)
public void addRequiredException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNewException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
public void addNested(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.NESTED)
public void addNestedException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}
2. TransactionDefinition.PROPAGATION_REQUIRED
它是Spring的默认传播行为,说白了发生嵌套在内部的事务会和外部的事务融合,所以外部事务报错了内部事务也会回滚。
如下面这段代码,外部的方法没有加事务,且user1Service、user2Service的方法都是PROPAGATION_REQUIRED这个传播级别,所以外部报错不影响两者的内部提交
/**
* 彼此都有独立的事务,外部没有开事务,所以两者数据都会入库
*/
@GetMapping("/test/add1")
public void notransaction_exception_required_required() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
然后我们再来看看这样一段代码,外部没有加事务,所以内部两个事务彼此独立。可以看到user2Service报错,所以只有user1Service插入成功:
@GetMapping("/test/add2")
public void notransaction_required_required_exception() {
//插入成功
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
//事务是独立的插入失败
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
throw new RuntimeException();
}
最后在看看这个,外部加了事务,也是REQUIRED,所以内部两个事务与其融合成为一个事务,当外部方法报错,两者插入操作都失败,数据直接回滚:
/**
* 外部开启事务,报错均回滚
*/
@GetMapping("/test/add3")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_required() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
再来看看一个比较好玩的,内外部都有事务,第2个内部事务报错,由于三者事务融为一体,所以user2Service的错误被外部transaction_required_required_exception方法感知,user1Service插入也是失败的,所以这个方法两张表都没有插入数据
@GetMapping("/test/add4")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_required_exception() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//错误被外部感知,所以所有user1的插入也被回滚了
user2Service.addRequiredException(user2);
}
这个也比较特殊,由于三个事务合为一体,所以即使user2Service报错不被感知,两张表的数据也还是没有插入:
@GetMapping("/test/add5")
@Transactional
public void transaction_required_required_exception_try() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
try {
//虽然异常被捕获,但是三个内外部事务融合了,一个报错就全部插入回滚
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
总结:Propagation.REQUIRED内外部属于统一事务,一个回滚全部回滚,无视try块代码的捕获。
3. TransactionDefinition.PROPAGATION_REQUIRES_NEW
我们还是通过看代码的方式来讲述吧:第一个例子,外部没有加事务,两个service彼此事务独立,外部报错,但是两者事务都已提交,所以都插入了:
@GetMapping("/test/add6")
public void notransaction_exception_requiresNew_requiresNew() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
throw new RuntimeException();
}
外部还是没有开启事务,user2Service报错事务回滚,所以只有user1Service插入了。
@GetMapping("/test/add7")
public void notransaction_requiresNew_requiresNew_exception() {
User1 user1 = new User1();
user1.setName("张三");
//正常插入
user1Service.addRequiresNew(user1);
User2 user2 = new User2();
user2.setName("李四");
//保存回滚了
user2Service.addRequiresNewException(user2);
}
来看一个综合的,外部加了REQUIRED,所以内部第一个事务和外部融合,后两个事务独立,在外部报错的情况下只有addRequired回滚。李四、王五均被插入:
@GetMapping("/test/add8")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_requiresNew_requiresNew() {
User1 user1 = new User1();
user1.setName("张三");
//和外部事务融合,外部报错插入被回滚
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
// 事务独立,不受外部影响,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
// 事务独立,不受外部影响,正常插入
user2Service.addRequiresNew(user3);
throw new RuntimeException();
}
外部加了事务,由于王五报错被外部感知,张三的事务和外部融合,所以张三没有被插入,这题只有李四被插入了:
@GetMapping("/test/add9")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception() {
User1 user1 = new User1();
user1.setName("张三");
//和外部融合
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//事务独立,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
//报错,插入被回滚,外部感知到了错误,所以张三的插入也被回滚了
user2Service.addRequiresNewException(user3);
}
王五报错回滚,但是错误没有被外部感知到,张三和外部事务融合,正常插入、李四正常插入。
@GetMapping("/test/add10")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception_try() {
User1 user1 = new User1();
user1.setName("张三");
//正常插入
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//和外部事务彼此独立,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
try {
// 报错回滚,但错误并没有被外部感知,所以只有这个事务被回滚
user2Service.addRequiresNewException(user3);
} catch (Exception e) {
System.out.println("回滚");
}
}
总结: 在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
4. TransactionDefinition.PROPAGATION_NESTED
代码如下,外部没有事务,张三、李四彼此独立一个事务,数据均插入,外部异常不影响成功提交:
@GetMapping("/test/add11")
public void notransaction_exception_nested_nested() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
同理,外部没有事务,后者报错不影响前者正常插入:
@GetMapping("/test/add12")
public void notransaction_nested_nested_exception() {
User1 user1 = new User1();
user1.setName("张三");
//独立的事务,不受下方报错影响
user1Service.addNested(user1);
User2 user2 = new User2();
user2.setName("李四");
//外部没有事务,报错回滚
user2Service.addNestedException(user2);
}
外部开启事务(默认级别),内部事务与其融合,一错全部回滚:
@GetMapping("/test/add13")
@Transactional
public void transaction_exception_nested_nested(){
//外部开启事务,所有nest的事务都与外部事务融合,一个报错全部回滚
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
同上,外部开启事务后内部事务与外部融合,异常能被感知后回滚了:
@GetMapping("/test/add14")
@Transactional
public void transaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}
异常捕获李四的报错自己消化了,外部不回滚,这就是transaction_nested和REQUIRED的区别:
@GetMapping("/test/add15")
@Transactional
public void transaction_nested_nested_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addNestedException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
总结:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
5.事务的几个回滚规则
这个我们从源码的注释中就能看出端倪了,如下所示,注释中已经说明了只有运行时异常或者Error可以触发回滚,对于检查型异常是不会回滚。
/**
* <p>By default, a transaction will be rolling back on {@link RuntimeException}
* and {@link Error} but not on checked exceptions (business exceptions). See
* org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
Class<? extends Throwable>[] rollbackFor() default {};
那我如果要自定义一个异常使用怎么办?如下所示即可,在注解上声明我们需要回滚的错误类型即可。
@Transactional(rollbackFor= MyException.class)
6.详解@Transactional 注解
@Transactional 作用在不同的地方会有不同的效果,我们最常见的用法就是作用于方法上,如果在方法上加该注解,就会将当前方法中的数据库操作加入事务中。注意方法必须是public否则事务不会生效。 如果作用于类上,则意味着这个类中所有public的方法都会用到事务。接口同理,但我们不建议这么用。
它的常见参数配置如下:
- 传播属性(propagation):事务的传播行为,默认值为REQUIRED,可选的值在上面介绍过
- 隔离级别(isolation):事务的隔离级别,默认值采用DEFAULT,可选的值在上面介绍过
- 回滚规则(rollbackFor):用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
- 只读属性(readOnly):指定事务是否为只读事务,默认值为 false。
- 超时时间(timeout):事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
7.Spring AOP自调用问题
这个问题,我们不妨举个例子来说吧,首先我们看看下面这段代码,很明显如果我们直接调用add17报错了事务会回滚,原因很简单,这个method1加了注解,所以如果我们通过api等工具调用method1时,真正执行这段代码的对象是结果Spring容器bpp处理过的cglib代理类:
@Transactional
@GetMapping("/test/add17")
public void method1(){
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1/0);
}
好了,有了上文的铺垫,我们再来说说嵌套调用失效问题,代码如下所示,当我们使用接口调用工具调用时,发现method1执行出错,李四还是被成功插入了,这是为什么呢? 原因很简单method1执行者并不是cglib代理对象,下面这段method1,完整的代码应该是this.method1,
@GetMapping("/test/add16")
public void add16() {
method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1(){
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1/0);
}
这就导致执行调用method1调用者是this,而不是cglib代理的增强类,如下图所示,正是因为调用者不是代理,导致代理根本不知道method1被调用了,所以事务就失效了
如何解决spring自调用问题呢?
最干脆就是调用时避免嵌套使用就好了,但是有时候应该这个方法要依赖外部的处理逻辑,而外部方法又臭又长改造两量很大导致无法重构。这时候我们只能想别的办法。我以前解决的办法就比较干脆了,既然问题的根源是调用对象错误,那我就干脆找出这个对象来调用不就解决了?
所以我们的思路是这样的,如下代码所示:
首先的controller中假如应用上下文:
@Autowired
private ApplicationContext applicationContext;
用这个上下文去容器中把他捞出来调用method1,问题解决
@GetMapping("/test/add16")
public void add16() {
TestController t = (TestController) applicationContext.getBean("testController");
t.method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1() {
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1 / 0);
}
当然这里还有一种方法,将代理的service类注入,因为spring注入的类都是经过cglib增强的类,所以使用注入的bean也能解决问题,只不过写法很丑陋而已。
@Autowired
private TestController t;
@GetMapping("/test/add16")
public void add16() {
t.method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1() {
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1 / 0);
}
8.事务场景注意事项
整体大概有以下几点:
- 正确的设置@Transactional 的rollbackFor 和propagation 属性,否则事务可能会回滚失败;
- 避免同一个类中调用@Transactional 注解的方法,这样会导致事务失效
- @Transactional 注解的方法所在的类必须被Spring 管理,否则不生效;
- @Transactional 注解只有作用到public 方法上事务才生效;
- 底层使用的数据库必须支持事务机制,否则不生效;
小结
自此我们Spring事务和设计理念和底层源码实现并结合相应案例对此进行了深入的分析,希望对你有帮助。