姥姥都能看懂的@Transactional分析

本文详细解释了@Transactional注解在SpringAOP中的应用,包括其作用于类、方法和接口的规则,以及隔离级别、传播机制的概念、不同级别的解释和实际案例。同时讨论了如何正确配置propagation属性和避免AOP自调用问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

原理

@Transactional注解作用于哪些地方

作用于类

作用于方法

作用于接口

属性

isolation(隔离级别)

为什么要有隔离级别这个概念?

数据问题有哪些

概念

@Transactional有哪些隔离级别?

READ_UNCOMMITTED 读未提交

READ_COMMITTED 读已提交

REPEATABLE_READ 可重复读

SERIALIZABLE 串行化

枚举源码

propagation(传播机制)

为什么要有传播机制这个概念?

概念:简单理解,就是多个事务方法互相调用过程中,事务的传播形式

@Transactional有哪些传播机制?

REQUIRED (spring默认)

SUPPORTS

MANDATORY

REQUIRES_NEW(新建一个事务)

NOT_SUPPORTED

NEVER

NESTED(嵌套事务)

枚举源码

rollbackFor & rollbackForClassName

概念

noRollbackFor & noRollbackForClassName

概念

timeout & readonly

为什么要有超时和只读属性

概念

注意事项

正确配置propagation属性

注意正确设置rollbackFor,保证发生异常后能正确回滚。

@Transactional注解只能应用到public方法才能生效。

避免Spring的AOP自调用问题

可能遇到的问题

1 @Transtional注解在什么情况下会失效?

2 现在有两个不同的服务A、B各有methodA和methodB,此时methodA中调用了methodB,methodA上加了@Transactional注解,事务默认隔离级别,传播机制为REQUIRED;methodB上也加了同样的@Transactional注解。然后运行到methodB时发生异常,此时methodB的操作会不会被回滚?methodA的操作会不会被回滚?为什么

3 上述场景 ,假设methodB的传播机制被调整为REQUIRED_NEW,如果此时methodB的异常抛出到methodA中被捕获了,methodA会不会回滚?为什么


原理

① 事务开始时,通过AOP生成一个代理connection对象。并将其放入DataSource实例的某个与

DataSourceTransactionManager相关的某处容器中。

②在接下来的整个事务中,客户端代码都应该使用该connection连接数据库,执行所有数据库命令。DataSource与TransactionManager需要配置相同的数据源。

③事务结束时,回滚①中得到的代理 connection对象上执行的数据库命令,然后关闭改代理connection对象。

@Transactional注解作用于哪些地方

作用于类

        表示所有该类的public方法都配置相同的事务属性信息。

@Transactional
public class ServiceA{

}

作用于方法

        当类配置了@Transactional注解并且方法也配置了@Transactional,方法上的事务属性会覆盖类的配置。

public class ServiceA{
    
	@Transactional
    public void methodA(){
    
    }
}

作用于接口

        不推荐这种使用这种用法,因为一旦标注在Interface上并且配置了SpringAop使用CGLib动态代理,将会导致@Transactional注解失效

// !!! 如果AOP使用cglib动态代理,将导致事务失效
@Transactional
public interface IService{

}

属性

// transational 注解的属性
@Transactional(transactionManager = "txManager",
            timeout = 1000,
            propagation = Propagation.REQUIRED,
            isolation = Isolation.READ_COMMITTED,
            readOnly = true,
            rollbackForClassName = {"classA","classB"},
            noRollbackFor = {ExcelCommonException.class},
            noRollbackForClassName = {"classC","classD"},
            rollbackFor = {Exception.class,RuntimeException.class})

isolation(隔离级别)

为什么要有隔离级别这个概念?

1.为了防止在并发操作数据库时产生脏读、不可重复读、幻读 这些数据问题而引申出来的一个处理机制。

2.隔离的本质是使事务在一定程度上串行化执行。也正因为如此,数据库事务隔离越严格,并发副作用越小,系统开销代价越大。 隔离可以理解成数据库操作的串行。

数据问题有哪些

  • 脏读: 按一个事务在处理过程中读取了另一个事物未提交的数据。(你还没提交就被我读出来了,万一你回滚了怎么办?够脏吧)下表格说明复现场景:

操作顺序

事务A

事务B

1

begin

2

begin

3

select amount from salary where id = 1; // 假设结果是1

4

update salary set amount = 2 where id = 1; //结果被变更为2,但未提交

5

select amount from salary where id = 2;//发现结果为2

  • 不可重复读:一个事务内,多次查询某个数据,却得到不同的结果。(脏读是读到未提交的数据,不可重复读读到的却是已经提交的数据,但实际上违背了事务一致性原则

操作顺序

事务A

事务B

1

begin

2

begin

3

select amount from salary where id = 1; // 假设结果是1

4

update salary set amount = 2 where id = 1;//变更

5

select amount from salary where id = 1; // 结果是1

6

commit

7

select amount from salary where id = 1; // 结果是2

  • 幻读:在可重复读(repeatable read 隔离级别下),一个事务可能会遇到幻读的问题。事务A按照指定搜索条件去读取若干行,事务B以C、U、D等行为来修改事务A的结果集,然后再提交。这时事务A再次按照刚才的条件去读取发现数据条数发生了变化,数据时多时少似乎是产生了幻觉。

操作顺序

事务A

事务B

1

begin

2

begin

3

select amount from salary where amount > 10000; // 假设结果有10条 (id 1~10)

4

insert into salary (id,amount)value(11,20000);//插入一条amount 为 20000 id为11的数据。

5

commit

6

select amount from salary where amount > 10000; // 发现结果有11条 (id 1~11)

  • 丢失修改:有两个事务先后修改一条数据,前者修改后,后者立即修改同一条数据。最终导致数据丢失。

概念

为了解决隔离与并发的矛盾,SQL92 数据库标准中定义了4个隔离级别:

@Transactional有哪些隔离级别?

READ_UNCOMMITTED 读未提交

概念:表示事务A可以读取事物B修改了但还未提交的数据。 会存在 脏读、幻读、不可重复读、丢失修改等问题

重点掌握: 此种隔离级别最不安全,并发量大时很容易出现数据问题。

READ_COMMITTED 读已提交

概念:表示事务A可以读取到事务B提交了的数据。会存在 不可重复度、幻读问题。解决了脏读问题。

如何解决脏读举例:

操作顺序

事务A

事务B

1

begin

2

select stock from warehouse; // 库存量为2

begin

3

update warehouse set stock = (2-1);//扣减库存

select stock from warehouse; // 由于A未提交,库存量仍为2

4

update warehouse set stock = (2-1);//扣减库存

5

rollback;//执行失败出现异常,回滚A

commit;//B执行完后,最终库存为1(正确),克服A的脏数据。

重点掌握: PostGreSql,Oracle,SQLserver 均是这种隔离级别

REPEATABLE_READ 可重复读

概念:可重复读,表示在同一事务内,事务第一次或者第n次读取某一条数据其结果都是一致的。解决了脏读、不可重复读的问题,但仍存在幻读问题。

如何解决不可重复读举例:

操作顺序

事务A

事务B

1

begin

2

select stock from warehouse; // 库存量为1

begin

3

update warehouse set stock = (2-1);//扣减库存

select stock from warehouse; //由于事务A已占用这条数据的锁,不允许读取直到A提交或回滚。

4

commit;//库存为 0

select stock from warehouse; // 库存量为0,库存不足,无法扣减

5

commit;

重点掌握: Mysql是这种隔离级别。

SERIALIZABLE 串行化

概念:串行化,相当于事务之间串行执行、提交。杜绝了脏读、不可重复读、幻读、丢失修改的问题。

重点掌握: 绝对安全,效率底下。

枚举源码

package org.springframework.transaction.annotation;

public enum Isolation {
    DEFAULT(-1),//默认值,表示使用底层数据默认隔离级别。 常见数据库的默认隔离级别mysql ---repeatable,oracle,pg,sql server ---read commited”
    READ_UNCOMMITTED(1),//读未提交,表示事务A可以读取事物B修改了但还未提交的数据。 会存在 脏读、幻读、不可重复读、丢失修改等问题
    READ_COMMITTED(2),//读已提交,表示事务A可以读取到事务B提交了的数据。会存在 不可重复度、幻读问题。解决了脏读问题。
    REPEATABLE_READ(4),	//可重复读,表示在同一事务内,事务第一次或者第n次读取某一条数据其结果都是一致的。解决了脏读、不可重复读的问题,但仍存在幻读问题。
    SERIALIZABLE(8);//串行化,相当于事务之间串行执行、提交。杜绝了脏读、不可重复读、幻读、丢失修改的问题。

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

propagation(传播机制)

为什么要有传播机制这个概念?

在方法的嵌套调用中,是重新创建事务还是使用父方法的事务?以及父方法的回滚对子方法的事务是否有影响。这些因素需要被精确地控制,于是引进了传播机制这个概念。

概念:简单理解,就是多个事务方法互相调用过程中,事务的传播形式

@Transactional有哪些传播机制?

//事务a
A {
       
     void methodA() {
         //在A中调用B的methodB()
         B.methodB();
     }
  
}

//服务b
B {
       
     void methodB() {
     }
       
}
REQUIRED (spring默认)

概念:无则新建,有则加入。

举例分析:

①如果A中的methodA已有事务,此时B.methodB()的操作直接加入到A的事务中,无论methodA还是methodB发生异常,将会一起回滚methodA所在的事务中的整体操作。

②如果A中的methodA没有事务,此时B.methodB()的操作新建一个属于methodB()的事务,methodB方法中发生异常仅回滚自身的事务,不影响methodA()。

SUPPORTS

概念:当前在事务中则以事务形式运行,当前不在事务中则和普通代码运行。

举例分析:

①如果A中的methodA已有事务,此时B.methodB()的操作直接加入到A的事务中,无论methodA还是methodB发生异常,将会一起回滚methodA所在的事务中的整体操作。

②如果A中的methodA没有事务,此时B.methodB()的操作将和普通代码一样不受事务控制。methodB方法即是发生异常也不会回滚,不影响methodA()。

MANDATORY

概念:当前方法必须被一个父事务调用,否则抛出异常。

举例分析:

①methodA如果被调用,则父方法中必须包含事务控制,否则抛异常。

REQUIRES_NEW(新建一个事务)

概念:新建一个事务,如果当前处在事务中,则将当前事务挂起。

举例分析:

①methodB如果被调用,直接新建一个事务。如果当前methodB的调用方处在事务中,则挂起当前事务,等methodB的事务提交后恢复继续执行。methodB中发生异常仅回滚methodB中的事务操作。

NOT_SUPPORTED

概念:如果当前处在事务中,则挂起当前事务,然后以非事务方式运行。运行结束后恢复当前事务。

举例分析:

①如果methodB被调用,当前处在事务中,则挂起当前事务,等methodB以非事务方式运行完毕后再恢复当前事务。

②如果methodB被调用,当前不处在事务中 ,则以普通方式运行。

NEVER

概念:不能在事务中运行。否则抛异常。

举例分析:

①如果methodB被调用,当前关联了任何一个事务则直接抛异常。否则可以正常以非事务方式运行。

NESTED(嵌套事务)

概念:开始一个“嵌套”的事务,他是已经存在的事务的一个真正的子事务,嵌套事务开始执行时,它将取得一个savepoint保存点,如果这个嵌套事务失败,将回滚到此savepoint。嵌套事务并不是一个全新的事务,它是外部事务的一部分,只有外部事务提交后,它才会被提交。

举例分析:

//服务A
A{

	@Transactional(propagation = Propagation.REQUIRED)
    methodA(){
    	B.methodB();
    }
}

//服务B
B{
    @Transactional(propagation = Propagation.NESTED)
	methodB(){
    
    }
}

①methodA执行时将新建一个新的事务,B.methodB()执行时,将挂起methodA的事务,并得到一个savepoint。新建一个属于methodA事务的子事务。methodA的事务一直挂起到methodB事务中的所有操作执行完毕后,继续执行。

②如果methodB()中发生异常,将回滚methodB的事务操作。 如果它抛出的异常被methodA捕获,methodA的事务仍有可能提交,否则methodA的事务将一起回滚。

③如果methodA()中发生异常,将一起回滚methodA事务以及它在methodB中新建的子事务。

枚举源码

package org.springframework.transaction.annotation;

public enum Propagation {
    REQUIRED(0),//默认 有则新建 无则加入
    SUPPORTS(1),//有则支持 无则非事务运行
    MANDATORY(2),//必须有 否则异常
    REQUIRES_NEW(3),//挂起当前,另起炉灶
    NOT_SUPPORTED(4),//挂起当前,普通运行
    NEVER(5),//坚决反对事务
    NESTED(6);//嵌套父子 荣辱与共

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

rollbackFor & rollbackForClassName

概念

指定遇到时必须进行回滚的异常类型,可以为多个。

noRollbackFor & noRollbackForClassName

概念

指定遇到时不回滚的异常类型,可以为多个。

timeout & readonly

为什么要有超时和只读属性

        由于事务可以在行和表上获得锁,因此事务会占用资源,并且对整体性能产生影响,如果一个事务只读取数据但不做修改,数据库引擎就可以对这个事务进行优化。

概念

timeout(超时时间,int,单位秒):事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。

readonly(事务的读写性,boolean):表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。

注意事项

正确配置propagation属性

有些传播机制配置后可以不启动事务,如果期望目标方法进行事务管理则不能配置以下三种:

①SUPPORTS 存在则加入,不存在则以非事务运行。不启动事务

②NOT_SUPPORTS 不支持事务运行,如果当前处在事务中则挂起当前事务,然后以非事务方式运行完,然后恢复当前事务。

③NEVER 以非事务方式运行,如果当前存在事务则抛异常。

注意正确设置rollbackFor,保证发生异常后能正确回滚。

        默认情况下,事务中抛出 继承自 RuntimeException 的异常或者Error,Spring将回滚事务。其他异常将不会回滚。

        如果期望事务中抛出其他类型的异常,并希望Spring能够回滚事务

可以指定 rollbackFor。通过源码分析可知,如果目标方法中抛出的rollbackFor指定的异常的子类,事务同样会回滚。

private int getDepth(Class<?> exceptionClass, int depth) {
        if (exceptionClass.getName().contains(this.exceptionName)) {
            // Found it!
            return depth;
}
        // If we've gone as far as we can go and haven't found it...
        if (exceptionClass == Throwable.class) {
            return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

@Transactional注解只能应用到public方法才能生效。

        因为在使用Spring AOP代理时,Spring在调用TransactionInterceptor在目标方法执行前后进行拦截之前,会通过DynamicAdviseInterceptor(CglibAopProxy的内部类)的intercept方法或者jdkDynamicAopProxy的invoke方法,这两个方法会间接调用AbstractFallbackTransactionAttributeSource(Spring通过这个类获取@Transactional注解的事务属性配置信息)的computeTransactionAttribute方法。而computeTransactionAttribute会检查目标方法是不是public修饰,如果不是则不会获取@Transactional的属性配置信息,最终造成不会对目标方法进行拦截并进行事务控制。

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;}

避免Spring的AOP自调用问题

在Spring 的Aop代理下,只有目标方法由外部调用,目标方法才由Spring生成的代理对象来管理。如果一定要解决,可以使用AspectJ取代SpringAop代理,具体如何取代就不展开了。

以下为自调用问题代码举例:

transationalMethod方法虽然有@Transactional注解,但是它被内部方法insert调用,事务会被忽略,出现异常后事务不会回滚。

@Service
public class Service{
 	
    private void insert(){
    	transationalMethod();
    }
    
    @Transactional
    public void transationalMethod(){
    	//insert
        //update
        //...
    }
    
}

可能遇到的问题

1 @Transtional注解在什么情况下会失效?

① 注解修饰的非public方法

②错误配置了rollbackFor,导致抛出非RuntimeException后事务不会正常回滚

③传播机制propagation设置错误。三种 supports\not_supported\never

④同一个类中方法调用,即Spring Aop的自调用。

⑤异常被catch吃掉,导致没有抛出,事务不会回滚。

⑥数据库引擎不支持事务。比如mysql默认用的InnoDB引擎,但是如果切换为myisam引擎,事务则彻底失效。

2 现在有两个不同的服务A、B各有methodA和methodB,此时methodA中调用了methodB,methodA上加了@Transactional注解,事务默认隔离级别,传播机制为REQUIRED;methodB上也加了同样的@Transactional注解。然后运行到methodB时发生异常,此时methodB的操作会不会被回滚?methodA的操作会不会被回滚?为什么

//服务A
A{

	@Transactional(propagation = Propagation.REQUIRED)
    methodA(){
    	B.methodB();
    }
}

//服务B
B{
    @Transactional(propagation = Propagation.REQUIRED)
	methodB(){
    
    }
}

        由于传播机制时REQUIRED,methodA交给事务管理,methodB将加入到methodA的事务中,无论methodA或者methodB哪个地方发生异常,methodB和methodA都将视为一个整体事务回滚。

3 上述场景 ,假设methodB的传播机制被调整为REQUIRED_NEW,如果此时methodB的异常抛出到methodA中被捕获了,methodA会不会回滚?为什么

//服务A
A{

	@Transactional(propagation = Propagation.REQUIRED)
    methodA(){
    	B.methodB();
    }
}

//服务B
B{
    @Transactional(propagation = Propagation.REQUIRED_NEW)
	methodB(){
    
    }
}

        由于传播机制时REQUIRED_NEW所以methodB的操作将启动一个新的事务,methodB发生异常只会回滚自身事务的操作,如果methodB抛出的异常在methodA中被捕获而没有抛出,methodA的操作将不会回滚。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值