深入Spring数据库事务管理

一、Spring数据库事务管理器的设计

在Spring中数据库事务是通过PlatformTransactionManager进行管理的,而真正能够支持事务的是org.springframework.transaction.support.TransactionTemplate模版,其核心方法源码如下:

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    //使用自定义的事务管理器
    if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
        return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
    } else {//系统默认管理器
        //获取事务状态
        TransactionStatus status = this.transactionManager.getTransaction(this);

        Object result;
        try {
            //接口回调方法
            result = action.doInTransaction(status);
        } catch (RuntimeException var5) {
            //回滚异常方法
            this.rollbackOnException(status, var5);
            //异常抛出
            throw var5;
        } catch (Error var6) {
            //回滚异常方法
            this.rollbackOnException(status, var6);
            //抛出错误
            throw var6;
        } catch (Exception var7) {
            //回滚异常方法
            this.rollbackOnException(status, var7);
            //抛出无法捕获异常
            throw new UndeclaredThrowableException(var7, "TransactionCallback threw undeclared checked exception");
        }
        //提交事务
        this.transactionManager.commit(status);
        //返回结果
        return result;
    }
}

源码中的注释是笔者加入的,从源码我们可以看出:

  • 事务的创建、提交和回滚都通过PlatformTransactionManager接口完成

  • 无异常时会提交事务,当事务产生异常时会回滚事务,默认所有异常都回滚,我们可以通过配置去修改发生某些异常时回滚或不回滚

Spring事务管理器结构如下图:
这里写图片描述

我们常用的是DataSourceTransactionManager,配置代码如下:

<!--事务管理器配置数据源事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource"/>
</bean>

二、编程式事务和声明式事务

编程式事务是以代码的方式管理事务,事务将由开发者通过代码来实现。这是最简单的使用方式,但是不推荐使用。

声明式事务是一种约定型事务,Spring给出一个约定,当你的业务方法不发生异常(或发生异常,但被配置信息设置允许提交)时,Spring就会让事务管理器提交事务,而发生异常(并且该异常不被配置信息所允许提交事务)时,则让事务管理器回滚事务。声明式事务允许自定义事务接口——TransactoinDefinition,它可以由xml或者注解@Transactional进行配置。

1、@Transactional 配置项

我们先看看Transactional源代码:

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

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

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

    String[] rollbackForClassName() default {};

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

    String[] noRollbackForClassName() default {};
}

根据源码我们给出配置项及其含义:

  • propagation 表示当前事务与父事务(同一个线程中之前申明事务)的关系。父子事务体现为,方法调用栈的调用先后顺序。说得更直白点,就是一个方法Method1调用另外一个申明Transaction的Method2,Method1可能申明Transaction,也可能没有,Method2如何处理Transaction的问题。
  • isolation 刻画不同事务之间数据的可见性,数据的隔离级别。ISOLATION_READ_UNCOMMITTED,ISOLATION_READ_COMMITTED,ISOLATION_REPEATABLE_READ,ISOLATION_SERIALIZABLE隔离级别逐渐增强。基本与关系型数据库的隔离级别一致。 timeout 以秒为单位的事务超时时间。默认值-1,表示没有超时时间。
    例子:@Transactional(timeout=30)
  • readOnly 提示事务管理器,方法内部只涉及到读操作,Spring默认为false。依赖于底层事务管理器的解释,并不一定有效果。
    例子:@Transactional(readOnly=true)
  • rollbackFor 遇到什么样的异常事务会回滚。Spring默认值是RuntimeException,Checked
    Exception不会回滚。 例子:@Transactional(rollbackFor=Exception.class)

其中isolation和propagation是最为重要的内容,这些属性被Spring放在事务定义器TransactionDefinition接口中。

2、声明式事务的约定流程

首先根据Spring通过事务管理器(PlatformTransactionManager)创建事务,同时会把事务中的隔离级别、超时时间等属性根据配置内容往事务上设置,通常采用注解方式(@Transactional),声明式事务流程如下图:
这里写图片描述
代码示例如下:

@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)

public int insertRole(Role role) {
    return roleMapper.insertRole(role);
}

三、数据库事务相关知识

1、数据库事务ACID特性

  • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
  • 一致性(Consistent):事务结束后系统状态是一致的;
  • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
  • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

2、事务的并发问题

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  • 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

3、数据库事务隔离级别

数据库的四种隔离级别:脏读、读/写提交、不可重复读、序列化,如下表:

隔离级别 隔离级别的值 脏读 不可重复读 幻读
读未提交(read-uncommitted) 0
不可重复读(read-committed) 1 ×
可重复读(repeatable-read) 2 × ×
串行化(serializable) 3 × × ×

四、选择隔离级别和传播行为

1、选择隔离级别

在互联网应用中,选择隔离级别要综合考虑数据库数据一致性和系统的性能。一般而言,从脏读到序列化,数据一致性明显提高,但系统性能直线下降。比如序列化,会严重压制并发,引发大量线程挂起,知道获得锁才能进一步操作,而且恢复时又需要大量等待时间。所以大部分场景下,企业选择读/写(Read-Committed)提交的方式设置事务。

Spring中的事务隔离级别如下表:

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别。
ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读。
ISOLATION_READ_COMMITTED 允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ 对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。
ISOLATION_SERIALIZABLE 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

2、传播行为

传播行为是指方法之间的调用事务策略的问题。有时通过方法一去调度方法二时产生一条新事务,如果方法二发生异常,则会回滚事务,此时方法一的主事务也被回滚,显然这是不合理的。我们通过对事务的特性进行传播配置,控制方法之间调度事务策略,称为传播行为。流程如下图:

这里写图片描述

Spring的七种传播行为如下图:

传播行为 意义
PROPAGATION_MANDATORY 表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常
PROPAGATION_NESTED 表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。
PROPAGATION_NEVER 表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。
PROPAGATION_NOT_SUPPORTED 表示该方法不应该在一个事务中运行。如果一个现有事务正在进行中,它将在该方法的运行期间被挂起。
PROPAGATION_SUPPORTS 表示当前方法不需要事务性上下文,但是如果有一个事务已经在运行的话,它也可以在这个事务里运行。
PROPAGATION_REQUIRES_NEW 表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。
PROPAGATION_REQUIRES 表示当前方法必须在一个事务中运行。如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。

七种传播行为在TransactionDefinition中定义,其中最常用的是REQUIRED,也是默认的传播行为。企业比较关注的是REQUIRES_NEW和NESTED两种。

五、@Transactional的自调用失效问题

1、什么是自调用

有时候配置了注解@Transactional,但是它会失效。原因是注解@Transactional的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。所以对于静态方法和非public方法,注解@Transactional失效。还有自调用的时候也会失效。

自调用是指本类方法调用自身类的方法,同时被调用的方法注解了@Transactional,此时被调用方法的@Transactional失效。代码解释如下:

public class RoleServiceImpl implements RoleService{
    @Autowired
    private RoleMapper roleMapper = null;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }

    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        for (Role role:roleList){
            try {
                //调用自身类方法,产生自调用问题
                insertRole(role);
            }catch (Exception ex){
                System.out.println(ex);
            }
        }
        return count;
    }
}

2、产生自调用注解失效的原因

出现自调用注解失效问题的根本原因在于AOP实现原理,以上代码中是自己调用自己方法的过程,不存在代理对象的调用,就不会产生AOP去为设置@Transactional配置的参数。

3、解决方案

解决方法有两个:

为两个方法使用不同的接口,Spring IoC容器会生成不同的代理对象

直接从容器中获取代理对象,代码如下:

public class RoleServiceImpl implements RoleService{
    @Autowired
    private RoleMapper roleMapper = null;
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }

    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        RoleService service = ctx.getBean(RoleService.class);
        for (Role role:roleList){
            try {
                //用代理对象调用方法,克服自调用问题
                service.insertRole(role);
            }catch (Exception ex){
                System.out.println(ex);
            }
        }
        return count;
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/81212060