Spring事务从实现到本质

Spring事务的实现方式和本质

  • 一、Spring事务的基础知识
    • 1.脏读、不可重复读、幻读
    • 2.事务的隔离级别
    • 3.事务的传播机制
  • 二、Spring事务的实现方式
    • 1.编程式事务
    • 2.声明式事务
  • 三、Spring事务的本质
  • 四、Spring中事务常碰到的问题
    • 1.事务回滚
    • 2.事务嵌套
  • 五、总结

这一篇总结Spring中事务的两种实现方式:声明式事务、编程式事务以及他们的本质

一、Spring事务的基础知识

先回忆下Spring事务的基础知识事务的隔离级别与Spring事务的传播机制。我们知道数据库层面是不支持事务的传播机制的,这个是Spring独有的

1.脏读、不可重复读、幻读

  • 脏读:对于同一条数据在一个事务中多次读取的结果不一致,原因是第二次读取数据时读取的数据被其他未提交的事务修改了,当隔离级别是读未提交时就会有这种问题存在
  • 不可重复读:对于同一条数据在一个事务中多次读取的结果不一致,原因是第二次读取数据时读取的数据被其他已提交的事务修改了,当隔离级别是读已提交时就会有这种问题存在。
  • 幻读:脏读和不可重复读都是针对一条数据来说的,而我们使用重复读的隔离级别就可以解决脏读和不可重复读的问题了,但是还是会有幻读的问题,那什么是幻读呢?幻读指的是范围查询前后检索结果不一致,比如说事务里第一次查询customer表有10万记录,同一个事务第二次查询时有11万记录,这就是幻读。幻读产生的原因不是事务并发修改导致的(不可),而是查询时有其他事务在做插入,导致了数据在量上出现了变化。

2.事务的隔离级别

Spring支持的事务隔离级别与数据的隔离级别其实没有任何区别都是四种,说隔离级别必须要说事务的四大特性原子性、一致性、隔离性、持久性。需要拿出来说的便是隔离性,事务在支持隔离性时并不是将事务之间直接彻底隔离,而是给我们提供了几个级别来划分隔离的程度,也就是下面四种了。只有串行化才可以做到事务之间的完全隔离,而其他的隔离级别自然就会产生不同的问题了,因为事务之间有交叉。

  • 读未提交:这是最低的隔离级别,相当于事务之间的隔离性基本没有,所以这种隔离级别什么问题都解决不了,使用这种隔离级别会伴随脏读、不可重复读、幻读等问题。
  • 读已提交:同一个事务里读取的数据是其他事务里已经提交的数据,所以不会读取到其他事务未提交的数据,所有不会有脏读的问题,但是还是可能发生不可重复读、幻读的问题。
  • 重复读:同一个事务里支持重复读取,也就是同一个事务里前后读取的某条数据肯定一致(只针对某一条数据而言),但是仍然解决不了幻读的问题,幻读是因为并发插入导致的。重复读只能解决并发修改的问题。
  • 串行化:串行化可以解决隔离性产生的所有问题,但是他的效率特别的低,所有任务都会排队进行处理,在并发系统中效率非常低下。

3.事务的传播机制

事务的传播机制是Spring特有的机制,各个数据库是不支持的,那Spring的传播机制是什么呢,他们有什么作用呢?

  • PROPAGATION_REQUIRED(默认):如果当前方法没有事务,就创建一个新事务;如果当前方法已经有事务,就加入到当前事务中。

  • PROPAGATION_SUPPORTS:如果当前方法有事务,就加入到当前事务中;如果当前方法没有事务,就以非事务的方式执行。

  • PROPAGATION_MANDATORY:如果当前方法有事务,就加入到当前事务中;如果当前方法没有事务,就抛出异常。

  • PROPAGATION_REQUIRES_NEW:无论当前方法是否有事务,都创建一个新事务;如果当前方法已经有事务,就挂起当前事务。

  • PROPAGATION_NOT_SUPPORTED:以非事务的方式执行当前方法;如果当前方法有事务,就挂起当前事务。

  • PROPAGATION_NEVER:以非事务的方式执行当前方法;如果当前方法有事务,就抛出异常。

  • PROPAGATION_NESTED:在当前事务中创建一个嵌套事务;如果当前方法没有事务,就相当于PROPAGATION_REQUIRED。

二、Spring事务的实现方式

Spring提供了两种事务的支持方式一种常用的声明式事务,所谓声明式事务就是我们直接使用注解声明即可,而无需手动写事务的开启提交和回滚,这种事务的实现方式是AOP,AOP底层则是JDK的动态代理和CGLIB的动态代理。另一种支持的事务则是编程式事务,这种实现方式则是直接编写事务代码,底层通过ORM框架调用到数据库实现的事务,编程式事务具有更加灵活的特点,同时Spring为我们提供了两种编程式事务的实现方式,一种是TransactionTemplate,一种是PlatformTransactionManager。他们都能实现编程式事务,不过使用TransactionTemplate无需我们手动提交或者回滚,Spring会根据异常抛出与否进行提交或者回滚。使用PlatformTransactionManager就需要我们自己提交或者回滚了。下面看下他们的实现区别吧

1.编程式事务

  • 使用TransactionTemplate实现编程式事务
    下面是使用TransactionTemplate的伪代码,我们可以为TransactionTemplate指明他的隔离级别和传播机制,注意这里并没有配置数据源相关操作,数据源仍需要单独在配置文件中进行声明数据源的类型和驱动类以及其他的数据库访问的账号路径超时时间最大连接等信息。
    @Component
    public class TestTransactionTemplate {
    
        
        private TransactionTemplate transactionTemplate;
        
    	@Inject
        public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
            this.transactionTemplate = transactionTemplate;
            transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
            transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        }
    
        public void transferMoney(final String fromAccount, final String toAccount, final double amount) {
            transactionTemplate.execute(new TransactionCallback<Void>() {
                public Void doInTransaction(TransactionStatus status) {
                    try {
                        // 执行转账操作,将金额从fromAccount转到toAccount
    //                    accountService.transfer(fromAccount, toAccount, amount);
                        // 如果没有发生异常,则提交事务
                        return null;
                    } catch (Exception ex) {
                        // 如果发生异常,则回滚事务
                        status.setRollbackOnly();
                        throw new RuntimeException(ex);
                    }
                }
            });
        }
    }
    
  • 使用PlatformTransactionManager实现
    使用PlatformTransactionManager则必须我们手动进行提交或者回滚,下面是他的伪代码
    @Component
    public class TestPlatformTransactionManager {
    
        PlatformTransactionManager transactionManager;
    
        @Inject
        public void setTransactionManager(PlatformTransactionManager transactionManager){
            this.transactionManager = transactionManager;
        }
    
        public void transfer(String fromAccount, String toAccount, double amount) {
            DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
            txDef.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
            txDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            
            TransactionStatus txStatus = transactionManager.getTransaction(txDef);
            try {
                //执行转账操作,将金额从fromAccount转到toAccount
    //            accountService.transfer(fromAccount, toAccount, amount);
                //如果没有发生异常,则提交事务
                transactionManager.commit(txStatus);
            } catch (Exception ex) {
                //如果发生异常,则回滚事务
                transactionManager.rollback(txStatus);
                throw ex;
            }
        }
    }
    

2.声明式事务

声明式事务底层利用AOP的方式对我们的事务对象进行代理,然后通过前后的增强操作实现了事务的管理,其实底层他们还是一样的,使用声明式事务的伪代码如下,我们可以为注解声明需要的各种属性,常用的就是事务的传播机制、隔离级别、超时时间、回滚异常、非回滚异常等。

@Component
public class TestTransactional {

    @Transactional(propagation = Propagation.REQUIRES_NEW //设置传播机制
            ,isolation = Isolation.REPEATABLE_READ //设置隔离级别
            ,readOnly = false //设置是否只读
            ,timeout = 30 //设置数据库连接的超时时间
            ,transactionManager = "defaultTransactionManager" //设置事务管理器,一个工程多个时可以使用该方式
            ,rollbackFor = IllegalArgumentException.class // 指定回滚异常
            ,noRollbackFor = IndexOutOfBoundsException.class) // 指定非回滚异常
    public Boolean transfer(String fromAccount, String toAccount, double amount){

        // 业务操作...

        return Boolean.TRUE;
    }
}

三、Spring事务的本质

Spring虽然提供了多种事务的实现方式,其实最底层都是有一样的。他都必须依赖数据源来对数据库进行访问,根据数据源来进行不同的封装就实现了Spring的不同的事务实现方式。编程式事务是直接获取数据源后进行手动操作,我们使用的PlatformTransactionManager、或者TransactionTemplate都是需要利用数据源来进行操作的。Spring通过数据源来和数据库建立连接,开启连接后我们就可以为这个连接设置他的隔离级别和一些超时信息等。这样就会建立起一个事务了,最底层利用的还是数据库的事务的动作。声明式事务与编程式事务原理都是一致,只不过Spring通过AOP将我们的业务代码进行了代理,产生了一个代理对象,具体使用什么代理技术Spring会根据我们的实现类进行选择使用JDK还是CGLIB。产生的代理对象我们就可以在被Transactional注解修饰的方法的前后添加事务处理的相关代码了,这个代码和使用编程式事务的代码区别不大。所以说Spring事务的本质其实还是利用数据源打开和数据库的连接,在连接上进行事务的操作。Spring根据不同需要又封装了不同的事务实现,底层却都是一致的。

四、Spring中事务常碰到的问题

这里总结两个常见的事务问题事务的回滚和不回滚,以及事务嵌套的场景

1.事务回滚

在不声明回滚异常类时,只要被事务管理的方法发生异常,那么事务就是会回滚的。Spring根据抛出的异常来进行事务回滚。如果我们对异常进行了cath那Spring是无法进行事务回滚的,因为没有异常抛出了。如果想要回滚我们可以手动声明一个自定义异常或者指定的其他异常。这样就可以实现回滚。当然即使抛出了异常也不一定会回滚。这个还需要依赖我们指定的异常回滚类,一般可以为Transactional指明noRollBackFor。通过他可以指明在哪些异常下不回滚。通过rollBackFor指明哪些异常下回滚,需要满足回滚异常时才会去回滚。
那如果把异常catch了,又没有抛出异常我们有没有其他方式进行回滚呢(使用声明式事务时)?其实还有一种方式进行回滚,如下所示:

@Transactional
public void someMethod() {
    
    if (condition) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

这种操作就是拿到当前事务的状态手动更改为回滚状态,当执行到AOP的后置增强时,就会调用回滚方法,从而达到了回滚的目的。

2.事务嵌套

有没有思考过这个问题:Spring的事务的传播机制到底是做什么用的呢?
其实一般场景下Spring的事务传播机制很少用得到,我们通常都是不显示指定传播机制的,而是使用默认的PROPAGATION_REQUIRED。默认的这个传播机制就是有事务那我就使用你的事务,如果没有事务我就新建一个事务。假如有以下的场景存在,为了方便看就是用a、b命名方法了:

    @Transactional
    public void a(){ 
        //a方法被事务管理了,同时又调用了b这个事务方法
        b();
    }
    
    @Transactional
    public void b(){

    }

在此时我们不为a、b两个方法声明传播机制时,那a、b两个方法其实是共用一个事务的,因为他们的事务传播机制是PROPAGATION_REQUIRED。这个机制就是有事务就用已经存在的,没有则新建,很显然a方法时开启了一个事务,执行b方法时既然事务以及存在,就使用了a的事务。所以a、b方法其实是共用事务的。回看第一部分Spring中事务的传播机制其实有7种,其实这其中主要就是为了事务嵌套场景下使用的,也就是我们事务中又调用了事务的场景。此时我们就需要关注内层事务到底需要做什么,需不需要和上层事务保持一致的动作,如果不需要我们就可以选择PROPAGATION_REQUIRED_NEW,这样内层事务就是一个全新的事务。此时Spring是通过数据源和数据库新建立了一个连接,从而实现了新的事务开启。
此外在其中传播机制中最后一种需要单独说下:PROPAGATION_NESTED,他是嵌套事务。这个才是真正为嵌套事务使用的传播机制。上面的例子中有内层事务和外层事务其实他的原理还是不同的事务。而PROPAGATION_NESTED嵌套事务的底层却是使用的一个事务实现的嵌套事务。此时上面的代码可以改造如下:

    @Transactional(propagation = Propagation.REQUIRED)
    public void a(){
        //a方法被事务管理了,同时又调用了b这个事务方法
        b();
    }

    @Transactional(propagation = Propagation.NESTED)
    public void b(){

    }

此时b方法就是一个嵌套事务了,Spring的嵌套事务同样底层是依赖于数据库的嵌套事务,在Mysql里支持了一种伪嵌套事务,就是通过在一个事务中保存回滚点savepoint的方式来进行事务嵌套。当事务正常提交时都会提交,当事务异常时我们可以指定事务回滚到指定的回滚点,下面列举一个Mysql的回滚例子:假设有一个员工表employees表,对他进行了如下的操作:

START TRANSACTION;
SAVEPOINT sp1;

INSERT INTO employees (id, name, age) VALUES (1, 'Alice', 30);

SAVEPOINT sp2;

INSERT INTO employees (id, name, age) VALUES (3, 'Bob', 25);

SAVEPOINT sp3;

INSERT INTO employees (id, name, age) VALUES (4, 'Charlie', 27);

SAVEPOINT sp4;

INSERT INTO employees (id, name, age) VALUES (5, 'Dave', 29);

ROLLBACK TO sp3;
COMMIT;

上面的例子我们创建了4个回滚点,且我们最后是回滚到了sp3这个savepoint,那就意味着sp3之后的所有操作不会被写入数据库,而sp3之前的所有操作还是会正常入库,这样就实现了事务嵌套场景下的部分回滚机制。Spring事务传播机制中的PROPAGATION_NESTED底层正是利用了Mysql的这一功能进行了事务嵌套场景下的部分回滚。

五、总结

这篇先介绍了事务的基础知识,然后总结了Spring事务的支持方式,分析了他们的原理,最后总结下来就会发现Spring的事务其实全部都是依赖于数据源对数据库的事务操作,若是数据库事务不支持的动作,Spring也是不支持的。Spring事务的支持动作都是依赖于底层数据库事务的封装,包括了嵌套事务的场景,希望这一篇的总结可以帮助到路过的朋友。

你可能感兴趣的:(spring,java,数据库,事务)