14

Spring事务Transactional和动态代理(三)-事务失效的场景 | IT人生

 3 years ago
source link: http://www.itrensheng.com/archives/spring_transactional_uneffect
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

系列文章索引:

一. Spring事务分类

Spring 提供了两种事务管理方式:声明式事务管理和编程式事务管理。

1.1编程式事务

在 Spring 出现以前,编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中显式调用 beginTransaction()、commit()、rollback() 等事务管理相关的方法,这就是编程式事务管理。 简单地说,编程式事务就是在代码中显式调用开启事务、提交事务、回滚事务的相关方法。

1.2声明式事务

Spring 的声明式事务管理是建立在 Spring AOP 机制之上的,其本质是对目标方法前后进行拦截,并在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。而Spring 声明式事务可以采用 基于 XML 配置基于注解 两种方式实现 简单地说,声明式事务是编程式事务 + AOP 技术包装,使用注解进行扫包,指定范围进行事务管理。

本文内容是使用SpringBoot的开发的“基于注解”申明式事务管理,示例代码:https://github.com/qizhelongdeyang/SpringDemo

二. @Transacational实现机制

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,如下图中所示调用者 Caller 并不是直接调用的目标类上的目标方法(Target Method),而是 调用的代理类(AOP Proxy)。

根据 @Transactional 的属性配置信息,这个代理对象(AOP Proxy)决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截。在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务

image.png

三. @Transacational失效

在开发过程中,可能会遇到使用 @Transactional 进行事务管理时出现失效的情况,本文中代码请移步https://github.com/qizhelongdeyang/SpringDemo查看,其中建了两张表table1和table2都只有一个主键字段,示例都是基于两张表的插入来验证的,由表id的唯一性能来抛出异常。如下mapper:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table1")
public class Table1Entity implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
}

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table2")
public class Table2Entity implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
}

public interface Table1Mapper extends BaseMapper<Table1Entity> {
}

public interface Table2Mapper extends BaseMapper<Table2Entity> {
}

3.1 底层数据库引擎不支持事务

并非所有的数据库引擎都支持事务操作,如在MySQL下面,InnoDB是支持事务的,但是MyISAM是不支持事务的。在Spring事务操作中,如果底层表的创建是基于MyISAM引擎创建,那么事务@Transactional 就会失效

3.2 标注修饰无效

因为Spring AOP有两种实现方式:JDK(Spring事务Transactional和动态代理(一)-JDK代理实现)和cglib( Spring事务Transactional和动态代理(二)-cglib动态代理),所以在标注修饰失效的时候也有两种不能情况,如下:

1) 接口JDK动态代理

Spring AOP对于接口-实现类这种方式是基于JDK动态代理的方式实现的。这种方式除了实现自接口的非static方法,其他方法均无效。

由于接口定义的方法是public的,java要求实现类所实现接口的方法必须是public的(不能是protected,private等),同时不能使用static的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施AOP增强,也即不能进行Spring事务增强 如下代码:

public interface IJdkService {
    //非静态方法
    public void jdkPublic(Integer id1,Integer id2);
    
    //接口中的静态方法必须有body
    public static void jdkStaticMethod(Integer id1,Integer id2){
        System.out.println("static method in interface");
    }
}


@Service
public class JdkServiceImpl implements IJdkService {
    @Autowired
    private Table1Mapper table1Mapper;

    @Autowired
    private Table2Mapper table2Mapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void jdkPublic(Integer id1, Integer id2) {
        Table1Entity table1Entity = new Table1Entity();
        table1Entity.setId(id1);
        table1Mapper.insert(table1Entity);

        Table2Entity table2Entity = new Table2Entity();
        table2Entity.setId(id2);
        table2Mapper.insert(table2Entity);
    }

    //@Override 编译错误,方法不会覆写父类的方法
    @Transactional(rollbackFor = Exception.class)
    public static void jdkStaticMethod(Integer id1,Integer id2){
        System.out.println("static method in implation");
    }
}

上面代码中jdkPublic事务可以正常回滚, 而IJdkService中定义的jdkStaticMethod属于静态方法,调用不能通过@Autowired注入的方式调用,只能通过IJdkService.jdkStaticMethod调用,所以定义到实现类中的事务方法根本就不会被调用。

1) cglib动态代理

对于普通@Service注解的类(未实现接口)并通过 @Autowired直接注入类的方式,是通过cglib动态代理实现的。

cglib字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行AOP增强植入的,由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强。即除了public的非final的实例方法,其他方法均无效。 如下定义了@Service注解的CglibTranService,并使用@Autowired注入,测试事务能够回滚

@Service
public class CglibTranService {
    @Autowired
    private Table1Mapper table1Mapper;

    @Autowired
    private Table2Mapper table2Mapper;

    @Transactional(rollbackFor = Exception.class)
    public void testTran(Integer id1, Integer id2) {
        Table1Entity table1Entity = new Table1Entity();
        table1Entity.setId(id1);
        table1Mapper.insert(table1Entity);

        Table2Entity table2Entity = new Table2Entity();
        table2Entity.setId(id2);
        table2Mapper.insert(table2Entity);
    }
}

对于使用final修饰大的方法无法回滚事务的原因是:所注入的table1Mapper和table2Mapper会为null(为空的原因在系列文章后面会有分析),所以到table1Mapper.insert这行代码会抛出NullPointerException

image.png

而static修饰的方法就会变为类变量,因为JDK的限制,当在static方法中使用table1Mapper和table2Mapper的时候会报编译错误: 无法从静态上下文中引用非静态变量 table1Mapper

3.2 方法自调用

目标类直接调用该类的其他标注了@Transactional 的方法(相当于调用了this.对象方法),事务不会起作用。事务不起作用其根本原因就是未通过代理调用,因为事务是在代理中处理的,没通过代理,也就不会有事务的处理。 首先在table1和table2中都已经出入了1,并有如下示例代码:

@RestController
@RequestMapping(value = "/cglib")
public class CglibTranController {
    @Autowired
    private CglibTranService cglibTranService;
 @PutMapping("/testThis/{id1}/{id2}")
    public boolean testThis(@PathVariable("id1") Integer id1, @PathVariable("id2") Integer id2) {
        try {
            cglibTranService.testTranByThis(id1,id2);
            return true;
        }catch (Exception ex){
            ex.printStackTrace();
            return false;
        }
    }
}


@Service
public class CglibTranService {
    @Autowired
    private Table1Mapper table1Mapper;

    @Autowired
    private Table2Mapper table2Mapper;

    /**
     * 入口方法,这种方式事务会失效
     * @param id1
     * @param id2
     */
    public void testTranByThis(Integer id1, Integer id2) {
        //直接调用目标类的方法
        testTranByThis_insert(id1,id2);
    }

    @Transactional
    public void testTranByThis_insert(Integer id1, Integer id2){
        Table1Entity table1Entity = new Table1Entity();
        table1Entity.setId(id1);
        table1Mapper.insert(table1Entity);

        Table2Entity table2Entity = new Table2Entity();
        table2Entity.setId(id2);
        table2Mapper.insert(table2Entity);
    }
}

通过curl来调用接口

curl -X PUT "http://localhost:8080/cglib/testThis/2/1"

结果是table1中有1,2两条记录,table2中只有1一条记录。也就是说testTranByThis_insert上面标注@Transactional无效table1Mapper插入成功了,table2Mapper的插入并未导致table1Mapper插入回滚。

那如果必须要在方法内部调用@Transactional注解方法保证事务生效,该怎么做?当然是改为Spring AOP的方式调用

//定义一个ApplicationContext 工具类
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }

    public static Object getBean(Class c) {
        return applicationContext.getBean(c);
    }
}

并改造testTranByThis方法如下:

    public void testTranByThis(Integer id1, Integer id2) {
        //直接调用目标类的方法
//        testTranByThis_insert(id1,id2);
        //注解调用
        CglibTranService proxy = (CglibTranService)SpringContextUtil.getBean(CglibTranService.class);
        proxy.testTranByThis_insert(id1,id2);
    }

这样即使是内部调用,但是通过ApplicationContext 获取了Bean,改造后的事务是生效

3.3 多个事务管理器

当一个应用存在多个事务管理器时,如果不指定事务管理器,@Transactional 会按照事务管理器在配置文件中的初始化顺序使用其中一个。 如果存在多个数据源 datasource1 和 datasource2,假设默认使用 datasource1 的事务管理器,当对 datasource2 进行数据操作时就处于非事务环境。 解决办法是,可以通过@Transactional 的 value 属性指定一个事务管理器。在使用多个事务管理器的情况下,事务不生效的原因在本系列后续文章中会有分析

3.4 默认 checked 异常不回滚事务

Spring 默认只为 RuntimeException 异常回滚事务,如果方法往外抛出 checked exception,该方法虽然不会再执行后续操作,但仍会提交已执行的数据操作。这样可能使得只有部分数据提交,造成数据不一致。 要自定义回滚策略,可使用@Transactional 的 noRollbackFor,noRollbackForClassName,rollbackFor,rollbackForClassName 属性 如下代码事务不生效,table1Mapper插入成功。table2Mapper插入失败了,但是异常被捕获了并抛出了IOException,table1Mapper的插入不会回滚

    @Transactional(rollbackFor = RuntimeException.class)
    public void testCheckedTran(Integer id1, Integer id2) throws IOException {
        Table1Entity table1Entity = new Table1Entity();
        table1Entity.setId(id1);
        table1Mapper.insert(table1Entity);
        try {
            Table2Entity table2Entity = new Table2Entity();
            table2Entity.setId(id2);
            table2Mapper.insert(table2Entity);
        }catch (Exception ex){
            throw new IOException("testCheckedTran");
        }
    }

不会回滚的原因是check了rollbackFor = RuntimeException.class,但是抛出的是IOException,而IOException并不是RuntimeException的子类,如下的继承关系图

image.png
改造以上代码如下可以成功回滚事务,DuplicateKeyException是RuntimeException的子类:
    @Transactional(rollbackFor = RuntimeException.class)
    public void testCheckedTran(Integer id1, Integer id2) throws IOException {
        Table1Entity table1Entity = new Table1Entity();
        table1Entity.setId(id1);
        table1Mapper.insert(table1Entity);
        try {
            Table2Entity table2Entity = new Table2Entity();
            table2Entity.setId(id2);
            table2Mapper.insert(table2Entity);
        }catch (Exception ex){
            throw new DuplicateKeyException("testCheckedTran");
        }
    }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK