当前位置 : 首页 » 文章分类 :  开发  »  Spring-Transaction

Spring-Transaction

Spring 事务相关笔记


Spring事务

编程式事务

即手动写代码开启事务

什么是声明式事务?

声明式事务管理建立在 AOP 之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于 @Transactional 注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

如何使用声明式事务?

Spring 事务在 spring-tx-xxx.jar 包中,spring 所有的事务管理策略类都继承自 org.springframework.transaction.PlatformTransactionManager 接口。
其中 TransactionDefinition 接口中定义了事务的传播属性和隔离级别标志。

声明式事务管理也有两种常用的方式:
1、一种是基于tx和aop名字空间的xml配置文件
2、另一种就是基于 @Transactional 注解。
显然基于注解的方式更简单易用,更清爽。

spring事物配置,声明式事务管理和基于@Transactional注解的使用
https://blog.csdn.net/bao19901210/article/details/41724355


@Transactional 注解

@Transactional 可以作用于接口、接口方法、类以及类方法上。
当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

默认情况下,只有来自外部的方法调用才会被 AOP 代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用 @Transactional 注解进行修饰。

spring事物配置,声明式事务管理和基于@Transactional注解的使用
https://blog.csdn.net/bao19901210/article/details/41724355

@Transactional 工作原理

那么 Spring 是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?
解决这个问题,也就可以从整体上理解 Spring 的事务管理实现原理了。下面简单地介绍下,注解方式为例子
1、配置文件开启注解驱动,在相关的类和方法上通过注解 @Transactional 标识。
2、spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据 @Transaction 的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
3、真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

深入理解 Spring 事务原理
https://www.cnblogs.com/fjdingsd/p/5632949.html


@Transactional注解源码

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.TransactionDefinition;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

基于@Transactional 自定义事务注解

继承 @Transactional 自定义事务注解。
比如可以定义如下 MyTransaction 注解,继承自 @Transactional 注解,约定好本部门内部的事务标准。
1、@Inherited 表示可被继承,在具体的项目中可以继承此注解后再自定义一个事务注解,约定适用于本项目的事务参数。
2、@Target 指明可用于方法、类、接口上。
3、@Transactional 继承Spring的注解,约定隔离级别为读已提交,对于任意 Exception 及其子类都回滚,传播属性没指定的话会使用 @Transactional 默认的 Propagation.REQUIRED

package com.masikkk.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public @interface Transaction {
}

@Transactional注解事务不生效

配置正确的前提下, 出现事务不起作用的原因可能有:
1、同一个类中, 一个非 transactional 的方法去调用 transactional 的方法, 事务会失效
当本类的使用 @Transactional 的方法被本类的其它没有开启事务的方法调用时,不会开启事务。使用 @Transactional 的方法被其它类调用时,按照正常的事务传播行为规则开启事务
If you use (default) Spring Proxy AOP, then all AOP functionality provided by Spring (like @Transactional) will only be taken into account if the call goes through the proxy. – This is normally the case if the annotated method is invoked from another bean.

2、异常被try{}catch(){}捕捉到了,有异常就不会回滚。

3、在private方法上标注transactional, 事务无效
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

Spring 下默认事务机制中@Transactional 无效的原因
https://www.cnblogs.com/milton/p/6046699.html


同类内部调用无法开启事务

同类内部调用无法开启事务原因

在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,这会造成自调用问题。若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务被忽略,不会发生回滚。

如果同一个类中有方法:methodA(); methodB()。methodA()没有开启事务,methodB()开启了事务,且methodA()会调用methodB()。
那么,methodA()调用methodB()时,不会开启事务!!!
即:同一个类中,无事务的方法调用有事务的方法,结果就是没有事务!!!

假如有个接口,它包含两个方法a和b,然后有一个类实现了该接口。在该实现类里在a上标上事务注解、b上不标,在方法b里调用方法a
问:此时再调用方法b,会有事务吗?
答:没有

大致实现代码是类似这样的

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false)
    public void insert01(User u){
        this.userMapper.insert(u);
        throw new RuntimeException("测试插入事务");
    }

    public void insert02(User u){
        this.insert01(u);
        throw new RuntimeException("测试插入事务");
    }
}

当controller或其他service直接调用insert01(User u)时,事务是正常的,数据库里面的确没数据;但是如果调用的是insert02(User u)方法,异常抛出了,但是数据库里面居然有数据,说明事务不正常了。

原因很好理解, @Transactional 事务是基于代理实现的,经过代理则生效,不经过代理则不会生效
1、从代理对象进入目标对象,会被拦截
2、从目标对象进入目标对象,不会被拦截。

此问题也适用于所有同类内调用无法被 Spring 代理拦截的问题。


同类内部调用不经过代理相关引申

事务传播属性不生效
@Cacheable同一个类内调用不生效

同类内部调用无法开启事务解决方法

自注入使内部调用走代理

spring Aop嵌套调用的解决办法:
方法1,将自身注入到自身
方法2,使用AopContext.currentProxy())来操作

弄懂了上面的分析, 那么解决这个问题就十分简单了. 既然 test() 方法调用没有触发 AOP 逻辑的原因是因为我们以目标对象的身份(target object) 来调用的, 那么解决的关键自然就是以代理对象(proxied object)的身份来调用 test() 方法.
因此针对于上面的例子, 我们进行如下修改即可:

@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SomeService self;

    public void hello(String someParam) {
        logger.info("---SomeService: hello invoked, param: {}---", someParam);
        self.test();
    }

    @CatMonitor
    public void test() {
        logger.info("---SomeService: test invoked---");
    }
}

上面展示的代码中, 我们使用了一种很 subtle 的方式, 即将 SomeService bean 注入到 self 字段中(这里再次强调的是, SomeService bean 实际上是一个代理对象, 它和 this 引用所指向的对象并不是同一个对象), 因此我们在 hello 方法调用中, 使用 self.test() 的方式来调用 test() 方法, 这样就会触发 AOP 逻辑了.


从ApplicationContext中获取Bean对象来调用

实现 ApplicationContextAware 接口获取 ApplicationContext 实例保存下来,做成一个 ApplicationContextProvider 工具类

public void insert02(User u){
    getService().insert01(u);
}
private UserService getService(){
    return ApplicationContextProvider.getBean(this.getClass());
}

AopContext.currentProxy()获取代理对象调用
public interface UserService{
    public void a();
    public void b();
}

public class UserServiceImpl implements UserService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void a(){
        this.b();
    }

    @Transactional(propagation = Propagation.REQUIRED_NEW)
    public void b(){
        System.out.println("b has been called");
    }
}

Q1:b中的事务会不会生效?
A1:不会,a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。

Q2:如果想要b中有事务存在,要如何做?
A2:@EnableAspectJAutoProxy(exposeProxy = true) ,设置 expose-proxy 属性为 true,将代理暴露出来
使用 AopContext.currentProxy() 获取当前代理,将 this.b() 改为
((UserService)AopContext.currentProxy()).b()

例如

private UserService getService() {
    // 采取这种方式的话, @EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true) 必须设置为true
    return AopContext.currentProxy() != null ? (UserService)AopContext.currentProxy() : this;
}

其中 AopContext 是 Spring 内置的一个功能类
注意:调用 currentProxy() 方法时, @EnableAspectJAutoProxy(exposeProxy=true) 必须设置为true,否则会抛出 IllegalStateException异常


非public方法无法开启事务

假设有这样一个类,它里面包含public方法,protected方法,private方法,package方法,final方法,static方法,我都给它们加上事务注解,哪些方法会有事务呢?
假如这个类没有实现任何接口,也就是说使用 CGLib 动态代理

CGLib 动态代理是通过继承目标类并重写同名方法来实现的
由于必须继承和重写才能代理目标方法, private方法不能被继承,final方法不能被重写,static静态方法和具体对象无关,所以它们3个的事务不起作用
public方法,protected方法可以被重写以添加事务代码,对于package方法来说,如果生成的子类位于同一个包里,就可以被重写以添加事务代码。确实CGLIB生成的代理子类和父类在同一个包下。不过Spring选择让protected方法和package方法不支持事务,所以只有public方法支持事务。

实现大概类似这种

class Target {

    @Transactional
    public void doNeedTx() {
        System.out.println("execute doNeedTx in Target");
    }

    //no annotation here
    public void doNotneedTx() {
        this.doNeedTx();
    }
}

class ProxyByCGLIB extends Target {
    @Override
    public void doNeedTx() {
        System.out.println("-> create Tx in Proxy");
        super.doNeedTx();
        System.out.println("<- commit Tx in Proxy");
    }

    @Override
    public void doNotneedTx() {
        super.doNotneedTx();
    }
}

【面试】我是如何在面试别人Spring事务时“套路”对方的
https://mp.weixin.qq.com/s/rHlfKmughNeI8-gafcW8VQ


Spring事务的隔离级别(默认READ_COMMITTED)

隔离级别是指若干个并发的事务之间的隔离程度。
Isolation 枚举中定义了五个表示隔离级别的常量:
Isolation.DEFAULT 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值是 Isolation.READ_COMMITTED
Isolation.READ_UNCOMMITTED 一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
Isolation.READ_COMMITTED 一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
Isolation.REPEATABLE_READ 一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
Isolation.SERIALIZABLE 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

spring事物配置,声明式事务管理和基于@Transactional注解的使用
https://blog.csdn.net/bao19901210/article/details/41724355


Spring事务的传播属性(默认Propagation.REQUIRED)

事务的传播行为(propagation behavior)。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

Spring定义了七种传播行为:
Propagation.REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。有则加入,无则新建
Propagation.REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
Propagation.SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
Propagation.NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
Propagation.NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
Propagation.MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
Propagation.NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

spring事物配置,声明式事务管理和基于@Transactional注解的使用
https://blog.csdn.net/bao19901210/article/details/41724355


Spring事务传播属性不生效问题排查

事务方法 methodA() 调用 methodB() 时,在 methodB() 上注解 @Transactional(propagation = Propagation.REQUIRES_NEW) 但没生效, methodB() 没有独自提交,还是在 methodA() 的大事务中一起提交。

原因:
methodA() 和 methodB() 在同一个类内部,调用不走代理, @Transactional 注解不生效,传播属性自然也不生效。

解决:
拆成两个类。


Spring事务嵌套

假设外层事务 Service A 的 Method A() 调用 内层Service B 的 Method B()

PROPAGATION_REQUIRED(spring 默认)
如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。
假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。

PROPAGATION_REQUIRES_NEW
比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。
那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。

他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。

PROPAGATION_SUPPORTS
假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

深入理解 Spring 事务原理
https://www.cnblogs.com/fjdingsd/p/5632949.html


事务传播的实现原理(ThreadLocal中的连接)

事务传播机制看似神奇,实际上是使用简单的ThreadLocal的机制实现的,所以如果调用的方法是在新线程调用的,事务传播实际上是会失效的;

spring在底层数据源的基础上,利用 ThreadLocal,SavePoint等技术点实现了多种事务传播属性,便于实现各种复杂的业务。只有理解了传播属性的原理才能更好的驾驭spring事务。Spring回滚事务依赖于对异常的捕获,默认情况下,只有抛出RuntimeException和Error才会回滚事务,当然可以进行配置,更多信息可以查看 @Transactional 这个注解。

spring事务-说说Propagation及其实现原理
https://blog.csdn.net/yanyan19880509/article/details/53041564/


Spring事务属性接口TransactionDefinition源码

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;

    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;  // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
    int ISOLATION_READ_COMMITTED = 2;  // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;
    int ISOLATION_REPEATABLE_READ = 4;  // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;
    int ISOLATION_SERIALIZABLE = 8;  // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;
    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() {
        return PROPAGATION_REQUIRED;
    }
    default int getIsolationLevel() {
        return ISOLATION_DEFAULT;
    }
    default int getTimeout() {
        return TIMEOUT_DEFAULT;
    }
    default boolean isReadOnly() {
        return false;
    }
    @Nullable
    default String getName() {
        return null;
    }
    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

此外,Spring 还定义了 Propagation 枚举 和 Isolation 枚举,方便在 @Transactional 注解中使用。

Propagation 枚举

package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

public enum Propagation {
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
    NESTED(TransactionDefinition.PROPAGATION_NESTED);

    private final int value;

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

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

Isolation 枚举

package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

public enum Isolation {
    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

    private final int value;

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

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

}

Spring 事务回滚

Spring事务何时回滚?(抛出 RuntimeException 时)

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。

spring事物配置,声明式事务管理和基于@Transactional注解的使用
https://blog.csdn.net/bao19901210/article/details/41724355


UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

更新数据库报错:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:728) ~[spring-tx-4.3.22.RELEASE.jar!/:4.3.22.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:518) ~[spring-tx-4.3.22.RELEASE.jar!/:4.3.22.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292) ~[spring-tx-4.3.22.RELEASE.jar!/:4.3.22.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.22.RELEASE.jar!/:4.3.22.RELEASE]

类似报错有:

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)

代码结构如下,在外部调用 ServiceA 的 methodA 方法

public class ServiceA {

  @Transactional
  public void methodA() {
    // 其他增删查改操作
    ... ...

    ModelB modelB = getModelB();
    try {
      serviceB.insertB(modelB);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

public class ServiceB {
  @Transactional
  public void insertB(modelB) {
    dao.insert(modelB);
  }
}

ServiceA 的 methodA 方法通过注解 @Transactional 开启了一个事务,其中调用了 ServiceB 的一个也注解了 @Transactional 的方法 insertB(),由于 insertB() 的成功与否不能影响 methodA() 的主流程,所以加了 try catch,意思是即使 insertB 失败了也要继续提交 methodA() 的事务 ,但生产环境发现一个问题,insertB() 里面抛了个 RuntimeException 导致 methodA 中在前面插入的数据回滚了,同时报错
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
我理解是已经 try catch 了 insertB 方法,即便里面抛异常也是不会影响外层 methodA() 的事务提交的,但事实并非如此。

原因:
Spring 事务默认的传播属性为 Propagation.REQUIRED,表示 有则加入无则新建事务,所以内部事务方法 insertB 会加入外部事务方法 methodA
此时,如果内层事务方法 insertB 执行时抛运行时异常了,Spring 会将整个事务打上全局回滚标记 rollback-only,外层 methodA 中捕获了异常可继续执行,但最终提交整个事务时,发现事务被标记为 rollback-only,导致事务回滚

解决方法:
把内部方法 insertB 上的事务传播类型改为 Propagation.REQUIRES_NEW 挂起当前事务,创建一个与原事务无关的新事务,使内部事务不影响外部事务。

RCA: Spring 事务 rollback-only异常
https://zhuanlan.zhihu.com/p/32720499

spring事务(Transaction )报 marked as rollback-only异常的原因及解决方法
https://blog.csdn.net/Mr_Smile2014/article/details/49455483

https://yunlongn.github.io/2019/05/06/记一次事务的坑Transaction-rolled-back-because-it-has-been-marked-as-rollback-only/


Spring事务的切面(环绕切面)

事务的切面是一个“around(环绕)”切面,在注解的业务方法前后都可以被调用。实现切面的具体类是TransactionInterceptor。

事务的切面有两个主要职责:
before时,切面提供一个调用点,来决定被调用业务方法应该在正在进行事务的范围内运行,还是开始一个新的独立事务。
after时,切面需要确定事务被提交,回滚或者继续运行。
before时,事务切面自身不包含任何决策逻辑,是否开始新事务的决策委派给事务管理器完成。

@Transactional 切面的优先级(最低)

Spring事务管理(Transaction Management),也是基于Spring AOP。
在Spring AOP的使用中,有时我们必须明确自定义aspect的优先级低于或高于事务切面(Transaction Aspect),所以我们需要知道:
@Transactional 事务切面优先级默认是最低的
LOWEST_PRECEDENCE = Integer.MAX_VALUE

所以,假如我如下代码想在 sendUserProfileMessage 方法上通过注解 @Transactional 开启事务,是行不通的,因为当前的切面优先级肯定高于 @Transactional 的切面,所以肯定是在事务外的。
执行顺序是:
before UserProfileMessageAspect
before @Transactional 开启事务
after @Transactional 提交或回滚事务
after UserProfileMessageAspect

@Slf4j
@Aspect
@Component
@Order(20)
public class UserProfileMessageAspect {
    @Autowired
    private UserProfileProducer userProfileProducer;

    @Pointcut("@annotation(com.nio.uds.user.annotation.UserProfileMessage)")
    public void userProfileMessagePointcut() {
    }

    @Transactional
    @Around("userProfileMessagePointcut()")
    public Object sendUserProfileMessage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();

        // 清除之前的消息上下文
        log.info("User profile message aspect in {}, args {}", MonitorUtils.getMethodShortName(method), JSONUtils.writeValue(proceedingJoinPoint.getArgs()));
        UserProfileContextHolder.clean();

        Object result = proceedingJoinPoint.proceed();

        // 发送User Profile Change kafka消息
        log.info("User profile message aspect, result {}", JSONUtils.writeValue(result));
        userProfileProducer.sendUserProfileChangeMessage();

        return result;
    }
}

Spring @Transactional工作原理
http://www.importnew.com/12300.html


事务管理器

事务管理器需要解决下面两个问题:
新的 Entity Manager 是否应该被创建?
是否应该开始新的事务?

这些需要事务切面’before’逻辑被调用时决定。事务管理器的决策基于以下两点:
事务是否正在进行
事务方法的 propagation 属性(比如 REQUIRES_NEW 总要开始新事务)

如果事务管理器确定要创建新事务,那么将:
创建一个新的 entity manager
entity manager 绑定到当前线程
从数据库连接池中获取连接
将连接绑定到当前线程

使用 ThreadLocal 变量将 entity manager 和数据库连接都绑定到当前线程。

事务运行时他们存储在线程中,当它们不再被使用时,事务管理器决定是否将他们清除。

程序的任何部分如果需要当前的entity manager和数据库连接都可以从线程中获取。

Spring @Transactional 工作原理
http://www.importnew.com/12300.html


ThreadLocal的使用

Spring 事务代码中用 ThreadLocal 来进行资源与事务的生命周期的同步管理。

TransactionSynchronizationManager 是 Spring 事务代码中对 ThreadLocal 使用最多的类,目前它内部含有6个 ThreadLocal,分别是:
resources
类型为 Map<Object, Object> 用于保存事务相关资源,比如我们常用的 DataSourceTransactionManager 会在开启物理事务的时候把 <DataSource, ConnectionHolder> 绑定到线程。
这样在事务作用的业务代码中可以通过 Spring 的 DataSourceUtils 拿到绑定到线程的 ConnectionHolder 中的 Connection。事实上对于 MyBatis 来说与 Spring 集成时就是这样拿的。

synchronizations
类型为 Set<TransactionSynchronization> 用于保存 transaction synchronization,这个可以理解为是回调钩子对象,内部含有beforeCommit, afterCommit, beforeCompletion等钩子方法。
我们自己如果需要的话也可以在业务方法或者切面中注册一些transaction synchronization对象用于追踪事务生命周期做一些自定义的事情。

currentTransactionName
当前事务名

currentTransactionReadOnly
当前事务是否只读

currentTransactionIsolationLevel
当前事务隔离级别

actualTransactionActive
是否存在物理事务,比如传播行为为 NOT_SUPPORTED 时就会是false。


TransactionSynchronizationManager

事务同步管理器,维护当前线程事务资源,信息以及TxSync集合

bindResource()

public static void bindResource(Object key, Object value) throws IllegalStateException
Spring 使用 TransactionSynchronizationManager 的 bindResource 方法将当前线程与一个事务绑定,采用的方式是 ThreadLocal

isSynchronizationActive()

public static boolean isSynchronizationActive()
返回当前线程是否开启了一个事务,也就是说是否在一个事务上下文中。

如何判断当前在不在一个事务中?

使用 TransactionSynchronizationManager.isSynchronizationActive() 静态方法可判断当前在不在一个事务上下文中。

Transaction synchronization is not active

在调用一个需要事务的组件的时候,管理器首先判断当前调用(即当前线程)有没有一个事务,如果没有事务会抛出此异常

registerSynchronization()

public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException
给当前线程的事务增加一个回调方法。
synchronization 参数可以通过实现 Ordered 接口来指定回调顺序。
如果当前线程没有开启一个事务,会抛出 IllegalStateException("Transaction synchronization is not active") 异常

源码如下:

public static void registerSynchronization(TransactionSynchronization synchronization)
    throws IllegalStateException {

  Assert.notNull(synchronization, "TransactionSynchronization must not be null");
  if (!isSynchronizationActive()) {
    throw new IllegalStateException("Transaction synchronization is not active");
  }
  synchronizations.get().add(synchronization);
}

如何在数据库事务提交成功后进行异步操作

业务需求上经常会有一些边缘操作,比如主流程操作A:用户报名课程操作入库,边缘操作B:发送邮件或短信通知。
要求:
操作A操作数据库失败后,事务回滚,那么操作B不能执行。
操作A执行成功后,操作B也必须执行成功

如何在spring事务提交之后进行异步操作,这些异步操作必须得在该事务成功提交后才执行,回滚则不执行。

使用 TransactionSynchronizationManager.registerSynchronization() 给当前线程的事务注册一个回调方法

private final ExecutorService executorService = Executors.newFixedThreadPool(5);

public void insert(TechBook techBook){
    bookMapper.insert(techBook);

    // send after tx commit but is async
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("send email after transaction commit...");
                    try {
                        Thread.sleep(10*1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("complete send email after transaction commit...");
                }
            });
        }
    }
    );

//        async work but tx not work, execute even when tx is rollback
//        asyncService.executeAfterTxComplete();

    ThreadLocalRandom random = ThreadLocalRandom.current();
    if(random.nextInt() % 2 ==0){
        throw new RuntimeException("test email transaction");
    }
    System.out.println("service end");
}

如何在数据库事务提交成功后进行异步操作
https://segmentfault.com/a/1190000004235193


上一篇 Apache-Shiro

下一篇 Spring-AOP

阅读
评论
7.5k
阅读预计30分钟
创建日期 2019-06-20
修改日期 2021-03-07
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论