spring事务的隔离级别和传播特性详解(附实例)

spring支持编程式事务管理和声明式事务管理两种方式。

        编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

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

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

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

一、传播特性(概述)

 所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

二、隔离级别(概述)

  隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:

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

三、事务超时

        所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。

        默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。

四、事务只读属性

        只读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化,比如使用Hibernate的时候。默认为读写事务。

        “只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。 

但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。 

因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可。

五、spring事务回滚规则

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

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

六、传播特性(详述)

        在我们用SSH开发项目的时候,我们一般都是将事务设置在Service层 那么当我们调用Service层的一个方法的时候它能够保证我们的这个方法中执行的所有的对数据库的更新操作保持在一个事务中,在事务层里面调用的这些方法要么全部成功,要么全部失败。那么事务的传播特性也是从这里说起的。 
       如果你在你的Service层的这个方法中,除了调用了Dao层的方法之外,还调用了本类的其他的Service方法,那么在调用其他的Service方法的时候,这个事务是怎么规定的呢,我必须保证我在我方法里掉用的这个方法与我本身的方法处在同一个事务中,否则如果保证事物的一致性。事务的传播特性就是解决这个问题的,“事务是会传播的”在Spring中有针对传播特性的多种配置我们大多数情况下只用其中的一种:PROPGATION_REQUIRED:这个配置项的意思是说当我调用service层的方法的时候开启一个事务(具体调用那一层的方法开始创建事务,要看你的aop的配置),那么在调用这个service层里面的其他的方法的时候,如果当前方法产生了事务就用当前方法产生的事务,否则就创建一个新的事务。这个工作使由Spring来帮助我们完成的。 
        以前没有Spring帮助我们完成事务的时候我们必须自己手动的控制事务,例如当我们项目中仅仅使用hibernate,而没有集成进spring的时候,我们在一个service层中调用其他的业务逻辑方法,为了保证事物必须也要把当前的hibernate session传递到下一个方法中,或者采用ThreadLocal的方法,将session传递给下一个方法,其实都是一个目的。现在这个工作由spring来帮助我们完成,就可以让我们更加的专注于我们的业务逻辑。而不用去关心事务的问题。

         默认情况下当发生RuntimeException的情况下,事务才会回滚,所以要注意一下 如果你在程序发生错误的情况下,有自己的异常处理机制定义自己的Exception,必须从RuntimeException类继承 这样事务才会回滚!

                         传播行为

                                           含义

PROPAGATION_REQUIRED(XML文件中为REQUIRED)

表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)

PROPAGATION_SUPPORTS(XML文件中为SUPPORTS)

表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行

PROPAGATION_MANDATORY(XML文件中为MANDATORY)

表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常

PROPAGATION_NESTED(XML文件中为NESTED)

表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样

PROPAGATION_NEVER(XML文件中为NEVER)

表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常

PROPAGATION_REQUIRES_NEW(XML文件中为REQUIRES_NEW)

表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。

PROPAGATION_NOT_SUPPORTED(XML文件中为NOT_SUPPORTED)

表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行 

七、举例说明上面传播特性

        假设有类A的方法methodB(),有类B的方法methodB().

7.1  PROPAGATION_REQUIRED

        如果B的方法methodB()的事务传播特性是propagation_required,那么如下图

                                   

        A.methodA()调用B的methodB()方法,那么如果A的方法包含事务,则B的方法则不从新开启事务,

        1、如果B的methodB()抛出异常,A的methodA()没有捕获,则A和B的事务都会回滚;

        2、如果B的methodB()运行期间异常会导致B的methodB()的回滚,A如果捕获了异常,并正常提交事务,则会发生Transaction rolled back because it has been marked as rollback-only的异常。

        3、如果A的methodA()运行期间异常,则A和B的Method的事务都会被回滚

 7.2  PROPAGATION_SUPPORTS

        如果B的方法methodB()的事务传播特性是propagation_supports,那么如下图

                                         

        A.methodA()调用B的methodB()方法,那么如果A的方法包含事务,则B运行在此事务环境中,如果A的方法不包含事务,则B运行在非事务环境;

        1、如果A没有事务,则A和B的运行出现异常都不会回滚。

        2、如果A有事务,A的method方法执行抛出异常,B.methodB和A.methodA都会回滚。

        3、如果A有事务,B.method抛出异常,B.methodB和A.methodA都会回滚,如果A捕获了B.method抛出的异常,则会出现异常Transactionrolled back because it has been marked as rollback-only。

7.3  PROPAGATION_MANDATORY

        表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常,如下图调用关系:

                                    

        B.methodB()事务传播特性定义为:PROPAGATION_MANDATORY

        1、如果A的methoda()方法没有事务运行环境,则B的methodB()执行的时候会报如下异常:No existingtransaction found for transaction marked with propagation 'mandatory'

        2、如果A的Methoda()方法有事务并且执行过程中抛出异常,则A.methodA()和B.methodB()执行的操作被回滚;

        3、如果A的methoda()方法有事务,则B.methodB()抛出异常时,A的methoda()和B.methodB()都会被回滚;如果A捕获了B.method抛出的异常,则会出现异常Transaction rolled back because ithas been marked as rollback-only

7.4  PROPAGATION_NESTED

如有一下方法调用关系,如图:

                                   

        B的methodB()定义的事务为PROPAGATION_NESTED;

        1、如果A的MethodA()不存在事务,则B的methodB()运行在一个新的事务中,B.method()抛出的异常,B.methodB()回滚,但A.methodA()不回滚;如果A.methoda()抛出异常,则A.methodA()和B.methodB()操作不回。

        2、如果A的methodA()存在事务,则A的methoda()抛出异常,则A的methoda()和B的Methodb()都会被回滚;

        3、如果A的MethodA()存在事务,则B的methodB()抛出异常,B.methodB()回滚,如果A不捕获异常,则A.methodA()和B.methodB()都会回滚,如果A捕获异常,则B.methodB()回滚,A不回滚;

 7.5  PROPAGATION_NEVER

        表示事务传播特性定义为PROPAGATION_NEVER的方法不应该运行在一个事务环境中

有如下调用关系:

                                  

        如果B.methodB()的事务传播特性被定义为PROPAGATION_NEVER,则如果A.methodA()方法存在事务,则会出现异常Existingtransaction found for transaction marked with propagation 'never'。

 7.6  PROPAGATION_REQUIRES_NEW

        表示事务传播特性定义为PROPAGATION_REQUIRES_NEW的方法需要运行在一个新的事务中。

        如有一下调用关系:B.methodB()事务传播特性为PROPAGATION_REQUIRES_NEW.

                                           

        1、如果A存在事务,A.methodA()抛出异常,A.methodA()的事务被回滚,但B.methodB()事务不受影响;如果B.methodB()抛出异常,A不捕获的话,A.methodA()和B.methodB()的事务都会被回滚。如果A捕获的话,A.methodA()的事务不受影响但B.methodB()的事务回滚。

 7.7  PROPAGATION_NOT_SUPPORTED

        表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行。

        如有一下调用关系图:

                                    

        如果B.methodB()方法传播特性被定义为:PROPAGATION_NOT_SUPPORTED。

        1、如果A.methodA()存在事务,如果B.methodB()抛出异常,A.methodA()不捕获的话,A.methodA()的事务被回滚,而B.methodB()出现异常前数据库操作不受影响。如果A.methodA()捕获的话,则A.methodA()的事务不受影响,B.methodB()异常抛出前的数据操作不受影响。

 八、实际场景中的七大事务传播行为使用

1、  在一个话费充值业务处理逻辑中,有如下图所示操作:

        业务需要扣款操作和创建订单操作同成功或者失败,因此,charger()和order()的事务不能相互独立,需要包含在chargeHandle()的事务中;

通过以上需求,可以给charge()和order()的事务传播行为定义成:PROPAGATION_MANDATORY

只要charge()或者order()抛出异常整个chargeHandle()都一起回滚,即使chargeHandle()捕获异常也没用,不允许提交事务。

2、  如果业务需求没接受到一次请求到要记录日志到数据库,如下图:

        因为log()的操作不管扣款和创建订单成功与否都要生成日志,并且日志的操作成功与否不影响充值处理,所以log()方法的事务传播行为可以定义为:PROPAGATION_REQUIRES_NEW.

3、  在订单的售后处理中,更新完订单金额后,需要自动统计销售报表,如下图所示:

                根据业务可知,售后是已经处理完订单的充值请求后的功能,是对订单的后续管理,统计报表report()方法耗时较长,因此,我们需要设置report()的事务传播行为为:PROPAGATION_NEVER,表示不适合在有事务的操作中调用,因为report()太耗时。

4、  在银行新增银行卡业务中,需要执行两个操作,一个是保存银行卡信息,一个是登记新创建的银行卡信息,其中登记银行卡信息成功与否不影响银行卡的创建。

        由以上需求,我们可知对于regster()方法的事务传播行为,可以设置为PROPAGATION_NESTED,action()事务的回滚,regster()保存的信息就没意义,也就需要跟着回滚,而regster()的回滚不影响action()事务;insert()的事务传播行为可以设置为PROPAGATION_REQUIRED, PROPAGATION_MANDATORY,即insert()回滚事务,action()的事务必须跟着回滚。

九、@Transactional注解

@Transactional属性

属性 类型 描述
value String 可选的限定描述符,指定使用的事务管理器
propagation enum: Propagation 可选的事务传播行为设置
isolation enum: Isolation 可选的事务隔离级别设置
readOnly boolean 读写或只读事务,默认读写
timeout int (in seconds granularity) 事务超时时间设置
rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
rollbackForClassName 类名数组,必须继承自Throwable 导致事务回滚的异常类名字数组
noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
noRollbackForClassName 类名数组,必须继承自Throwable 不会导致事务回滚的异常类名字数组

用法

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

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

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

@Autowired  
private MyBatisDao dao;  
  
@Transactional  
@Override  
public void insert(Test test) {  
    dao.insert(test);  
    throw new RuntimeException("test");//抛出unchecked异常,触发事物,回滚  
}

noRollbackFor 

@Transactional(noRollbackFor=RuntimeException.class)  
    @Override  
    public void insert(Test test) {  
        dao.insert(test);  
        //抛出unchecked异常,触发事物,noRollbackFor=RuntimeException.class,不回滚  
        throw new RuntimeException("test");  
    }

类,当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性

@Transactional  
public class MyBatisServiceImpl implements MyBatisService {  
  
    @Autowired  
    private MyBatisDao dao;  
      
      
    @Override  
    public void insert(Test test) {  
        dao.insert(test);  
        //抛出unchecked异常,触发事物,回滚  
        throw new RuntimeException("test");  
    }  

propagation=Propagation.NOT_SUPPORTED

@Transactional(propagation=Propagation.NOT_SUPPORTED)  
@Override  
public void insert(Test test) {  
    //事物传播行为是PROPAGATION_NOT_SUPPORTED,以非事务方式运行,不会存入数据库  
    dao.insert(test);  
}  
  • myBatis为例   基于注解的声明式事务管理配置,xml配置

      主要为aop切面配置,只看xml就可以了

<!-- 事物切面配置 -->  
<tx:advice id="advice" transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method name="update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception"/>  
        <tx:method name="insert" propagation="REQUIRED" read-only="false"/>  
    </tx:attributes>  
</tx:advice>  
  
<aop:config>  
    <aop:pointcut id="testService" expression="execution (* com.baobao.service.MyBatisService.*(..))"/>  
    <aop:advisor advice-ref="advice" pointcut-ref="testService"/>  
</aop:config>  

十、隔离级别(详述)

        当两个事务对同一个数据库的记录进行操作时,那么,他们之间的影响是怎么样的呢?这就出现了事务隔离级别的概念。数据库的隔离性与并发控制有很大关系。数据库的隔离级别是数据库的事务特性ACID的一部分,ACID,即原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。

        Spring的事务隔离级别有四个:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE,还有一个,是数据库默认的隔离级别DEFAULT,MySQL默认是REPEATABLE_READ。 

10.1  READ_UNCOMMITTED

        READ_UNCOMMITTED:一个事务可以读取到另一个事务未提交的事务记录。

        换句话说,a transaction can read the data that is still uncommitted by other transactions。

        这是Spring事务最弱的隔离级别。见下面的图,事务A开启,写入一条记录,这时候,事务B读入数据,读到了这条记录,但是,之后事务A回滚。因此,事务B读到的数据不是有效的(the database is in an invalid state)。这种情况称为脏读(dirty read)。除了脏读的问题,READ_UNCOMMITTED还可能出现non-repeatable read(不可重复读)和phantom read(幻读)的问题。

                       

10.2  READ_COMMITTED

        READ_COMMITTED:一个事务只能读取到已经提交的记录,不能读取到未提交的记录。

        换句话说,a transaction can only read the committed data, and it can’t read the uncommitted data.因此,dirty read的情况不再发生,但可能会出现其他问题。见下图。

在事务A两次读取的过程之间,事务B修改了那条记录并进行提交。因此,事务A前后两次读取的记录不一致。这个问题称为non-repeatable read(不可重复读)。(两次读取的记录不一致,重复读取就会发现问题。)

除了non-repeatable read的问题,READ_COMMITTED还可能发生phantom read的问题。       

                             

10.3  REPEATABLE_READ

REPEATABLE_READ意思是,一个事务可以多次从数据库读取某条记录,而且多次读取的那条记录都是一致的,相同的。这个隔离级别可以避免dirty read和non-repeatable read的问题,但可能发生phantom read的问题。如下图。

事务A两次从数据库读取一系列记录,期间,事务B插入了某条记录并提交。事务A第二次读取时,会读取到事务B刚刚插入的那条记录。在事务期间,事务A两次读取的一系列记录不一致,这个问题称为phantom read。

                        

10.4  SERIALIZABLE

SERIALIZABLE是Spring最强的隔离级别。事务执行时,会在所有级别上加锁,比如read和write时都会加锁,仿佛事务是以串行的方式进行的,而不是一起发生的。这会防止dirty read、non-repeatable read和phantom read的出现,但是,会带来性能的下降。

10.5  DEFAULT

MySQL默认是REPEATABLE_READ。

这里解释下上面提到的几种异常:

1. 脏读:读到了其他事务还没有提交的数据。

2. 不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。

3. 幻读:事务 A 根据条件查询得到了 N 条数据,但此时事务 B 更改或者增加了 M 条符合事务 A 查询条件的数据,这样当事务 A 再次进行查询的时候发现会有 N+M 条数据,产生了幻读。

十一、隔离级别实际例子

**1.**现有表A,两个字段 id和money,id为主键,money为金钱,数据库中一条id=1,money=20的数据,如下表所示

id money
1 20

**2.**有如下事务操作

READ UNCOMMITTED——读未提交

        **事务执行过程中可以读取到并发执行的其他事务中未提交的数据。**在这种事务隔离级别下可能会引起的问题包括:脏读,不可重复读和幻读。

举栗子

设置场景中事务的隔离级别是读未提交,那么现象是什么样的呢?

时序描述如下

T1 时刻,开启事务1
T2 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T3 时刻,开启事务2
T4 时刻,事务2,更新A表id为1的记录的money为100
T5 时刻,事务1,查询A表id=1的记录的money,结果为money=100(读取到T4时刻,事务2更新未提交数据)
T6 时刻,事务2,更新A表id为1的记录的money为200
T7 时刻,事务1,查询A表id=1的记录的money,结果为money=200(读取到T6时刻,事务2更新未提交数据)
T8 时刻,事务2提交
T9 时刻,事务1,查询A表id=1的记录的money,结果为money=200
T10 时刻,事务1提交

        从上面描述可以看出,事务1在T5和T7时刻查询的数据都是事务2更新了但是还没有进行提交的数据,事务2是在T8时刻才提交,所以这就是读未提交,读取到了其他事务没有提交的数据。

        这是事务隔离中最低的级别,这个隔离级别下的事务会读取到其他事务未提交的数据,所以在其他事务回滚的时候,当前事务读取的数据就成了脏数据(脏读我们后文细讲)

READ COMMITTED——读已提交

        **事务执行过程中只能读到其他事务已经提交的数据。**读已提交保证了并发执行的事务不会读到其他事务未提交的修改数据,避免了脏读问题。在这种事务隔离级别下可能引发的问题包括:不可重复读和幻读

举栗子

设置场景中事务的隔离级别是读已提交,那么现象是什么样的呢?

时序描述如下

T1 时刻,开启事务1
T2 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T3 时刻,开启事务2
T4 时刻,事务2,更新A表id为1的记录的money为100
T5 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T6 时刻,事务2,更新A表id为1的记录的money为200
T7 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T8 时刻,事务2提交
T9 时刻,事务1,查询A表id=1的记录的money,结果为money=200(读取到事务2提交的数据)
T10 时刻,事务1提交

        从上面的描述可以看出,在读已提交的事务隔离级别下,事务1不会读到事务2中修改但未提交的数据,在T8时刻事务2提交后,T9时刻事务1读取的数据才发生变化,这种现象就是读已提交。

REPEATABLE READ——可重复读

        **当前事务执行开始后,所读取到的数据都是该事务刚开始时所读取的数据和自己事务内修改的数据。**这种事务隔离级别下,无论其他事务对数据怎么修改,在当前事务下读取到的数据都是该事务开始时的数据,所以这种隔离级别下可以避免不可重复读的问题,但还是有可能出现幻读,那是为什么呢?

        答案我们在下文第三部分讲解幻读时详细讲,现在我们还是先看看在可重复读隔离级别下,上面场景会变成什么样。

举栗子

设置场景中事务的隔离级别是可重复读,那么现象是什么样的呢?

时序描述如下

T1 时刻,开启事务1
T2 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T3 时刻,开启事务2
T4 时刻,事务2,更新A表id为1的记录的money为100
T5 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T6 时刻,事务2,更新A表id为1的记录的money为200
T7 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T8 时刻,事务2提交
T9 时刻,事务1,查询A表id=1的记录的money,结果为money=20
T10 时刻,事务1提交

        从上面描述看出,不论事务2对数据进行了几次更新,到后面即使事务2进行事务提交,事务1里面始终读取的还是自己开始事务前的数据,这种情况就是一种快照读(类似于Oracle下的Read-Only级别),但是这种情况下事务中也是可以进行insert,update和delete操作的。

SERIALIZABLE——串行化

        **事务的执行是串行执行。**这种隔离级别下,可以避免脏读、不可重复读和幻读等问题,但是由于事务一个一个执行,所以性能就比较低。

        所以在这种隔离级别下,上面的场景中是事务1执行完成后,才会执行事务2,两个事务在执行时不会相互有影响。

十二、可能会引发的三种问题以及危害

        讲完了标准SQL的事务的四种隔离级别,那么接下来,看一下,这四种隔离级别下,会发生的问题。前面咱们也提到了主要有三种问题:脏读、不可重复读和幻读。那么这些问题要怎么理解?锤子接下来,会一个一个举例进行详细讲解

脏读

        脏读其实就是读到了,其他事务回滚前的未提交的脏数据。四种事务隔离级别中只有读未提交才会出现这种问题。咱们举个简单的栗子,理解一下脏读。

现在某电商平台有手机10库存10部,用户A在平台下单购买了10台手机,此时手机库存为0,因为地址填错了,A用户执行取消订单操作,取消订单的事务操作步骤如下:

1.开启事物
2.订单状态改为取消
3.库存还原
4.执行其他业务逻辑
5.提交事物

而在取消订单的事务中的第4步执行其他逻辑操作的时刻,用户B在查看该手机的库存时就看到了库存为10(读取到了其他事务未提交的数据),于是用户B下单买下来10台手机,后来由于A用户的取消订单的部分操作失败了,订单没有取消,数据发生了回滚,那么现在就出现了实际库存为10的手机被卖出了20台,导致了超卖问题。

这样就由脏读造成了一个实际生产中超卖问题,在实际生产中,脏读会导致很多问题,所以我们在使用事务的时候,不要轻易将事务隔离级别设置为读未提交,一定要仔细思考后再选择。

不可重复读

        不可重复读就是在一个事务多次相同的读取可能会读出不同的数据。读未提交和读已提交的隔离级别下都可能会出现不可重复读的问题。

        在读未提交隔离级别下,当前事务是可以读到其他事务未提交的数据,所以其他事务一直对数据进行修改的话,当前事务多次读取的数据就会不同。

        在读已提交的隔离级别下,当前事务只能读到其他事务已提交的数据,所以在其他事务修改还未提交时,当前事务的多次读取是一样的,但是一旦其他事务提交了修改,当前事务再读取到的数据,就与之前不一致了,这也是不可重复读的现场。

这样看起来在读已提交隔离级别下,当前事务都是读取的最新的已提交到数据库的数据,也不会有脏数据,好像并没有什么不妥,而且也合情合理,本来数据就是要读取最新的操作嘛,那么为什么不可重复读,还要被列为一个问题呢?相信你看了下面的例子就明白了

场景如下:

某电商平台做活动,用户在平台消费5000-10000元的送一个手机,消费超过10000元的送电脑。现在有生成获奖用户报表的事务如下:

1.开始事务
2.查询消费在5000到10000元的用户
3.打印送手机用户名单
4.查询消费在10000元以上的用户
5.打印送电脑用户名单
6.结束事务

锤子是该电商平台的用户,锤子之前的消费是6000元,在上述第2步的时候,锤子符合送手机的条件,而在上述第3步操作的时候,锤子又在电商平台消费了5000元,那么上述事务走到第4步的时候,锤子也符合了送电脑的条件,那么最终锤子就即获得了手机又获得了电脑,这个时候锤子是高兴了,可是电商平台就不高兴了,要吊起来打写这个功能的程序员哥哥喽。

看完上面的场景是不是发现,总是读取最新的数据并不是最好的,在某些场景下就是需要快照读,特别是对截至时间要求非常精确的地方,在事务开始的那一刻所有数据就应该固定,在整个事务的过程中,所有数据读取都要以事务开始的那一刻为准。

要处理这种情形下的问题,就要提高一下事务隔离级别到可重复读,在查出送手机用户的名单后加行锁,这样锤子又消费5000元的操作完成就在生成整个名单之后了,这就保证了锤子不会收到两个奖品。所以铁汁们在开发中要注意了,千万别踩到这样的坑,不然要被吊起来打了。

幻读

        其他事务在一个尚未提交的当前事务的读取的行的范围中插入新行或删除现有行,会对当前事务的对数据的读取产生幻象。幻读在读未提交、读已提交和可重复读三种事务隔离级别下都会出现。

那么下面咱们就来举栗子说明一下幻读

场景如下:

某电商平台还在做活动,用户在平台消费5000-10000元的送一个手机,消费超过10000元的送电脑。现在有生成获奖用户报表的事务如下:

1.开始事务
2.查询消费在5000到10000元的用户
3.打印送手机用户名单
4.查询消费在10000元以上的用户
5.打印送电脑用户名单
6.结束事务

        这个时候锤子和郝大都还不是该电商平台的用户,他们看到好消息,都想参加这个活动,于是两个人都去注册用户并消费。重点来了生成中间获奖名单的事务执行到第3步的时刻,锤子和郝大在此刻都注册了用户,并且锤子消费了6000元,郝大消费了12000元,两个人都以为可以得到自己想要的奖品了,结果最后中奖名单出来后,发现郝大获得了电脑,而锤子什么也没有,可是明明锤子和郝大一起注册的用户,但是郝大却获得了奖品,锤子却没获得。

        上面描述的这种现象就是读已提交隔离级别下的一种幻读现象,两个用户同时注册(同时向表中插入数据),且各自都符合不同的奖品条件要求,但是一个有奖品,一个没有奖品,就会让人感觉,这个福利有内幕的感觉。这就是读已提交下幻读造成的一种影响。

        同样上面的场景,如果事务隔离级别提高到可重复读,那么在不改变上述流程的情况下,在MySQL下就不会出现幻读了,因为他们的注册事务是在生成中奖名单之后,所以郝大和锤子都不会有奖品。因为在MySql的设计中可重复读的事务隔离级别的数据读取是快照读,即使其他事务进行insert或是delete,在当前事务中仅仅读取的话是读不到其他事务提交的数据,但是这并不代表MySQL中的可重复读隔离级别就可以完全避免幻读了。

        上面的场景下,我们提升事务隔离级别到可重复读,然后再修改一下生产获奖名单的事务,在第3步的后面添加一步update的操作(将用户表中所有用户记录的更新时间都更新一下),那么在update之后,再执行查询消费在10000元以上的用户的时候,郝大的数据又会被查出来,这个时候,又出现了,同时注册的两个人郝大有奖品,锤子没有奖品。

        那么上面为什么进行一次update后,郝大的数据又会被查出来呢?

        想知道这个原因还要知道两个概念:当前读和快照读(详解如下)

        当前读:读取的是最新版本数据, 会对读取的记录加锁, 阻塞并发事务修改相同记录,避免出现安全问题。
        快照读:可能读取到的数据不是最新版本而是历史版本,而且读取时不会加锁。

        现在知道了这两个概念,那么下面就描述一下,MySQL在可重复读的隔离级别下开启事务时,默认是使用的快照读,所以在整个事务中如果只有查询,那么查询的都是快照数据,就不会受到其他事务影响,但是我们上面又在事务中添加了一个更新语句,当进行更新时快照读就会变成当前读,因为在事务中更新数据是需要对数据进行加锁,直到事务提交才会释放锁,所有由快照读变为当前读后,读取的数据就是最新的,也就把后来添加的郝大账户计算了进去。

        到此我们把四种隔离级别和会引发的三种问题都进行了分析,所以大家在实际使用中要根据自己的业务进行合理选择,避免被老板吊着打,哈哈。

---------------------------------------------------------------------------------------------------------------------------------

一、数据库事务隔离级别

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。
注意我们讨论隔离级别的场景,主要是在多个事务并发 的情况下,因此,接下来的讲解都围绕事务并发

Read uncommitted 读未提交

公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高 兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有 2000元,singo空欢喜一场。

出现上述情况,即我们所说的脏读 ,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。

当隔离级别设置为Read uncommitted 时,就可能出现脏读,如何避免脏读,
请看下一个隔离级别

Read committed 读已提交

singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在 singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为 何…

出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

当隔离级别设置为Read committed 时,避免了脏读,但是可能会造成不可重复读。

大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,
请看下一个隔离级别

Repeatable read 重复读

当隔离级别设置为Repeatable read 时,可以避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账。

虽然Repeatable read避免了不可重复读,但还有可能出现幻读 。

singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction … ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出 现了幻觉,幻读就这样产生了。

注:Mysql的默认隔离级别就是Repeatable read。

Serializable 序列化

Serializable 是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

二、脏读、幻读、不可重复读

1.脏读:

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

2.不可重复读:

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

3.幻读:

是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象
发生了幻觉一样。
例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。

****1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表**
————————————————

参考文档一

参考文档二

参考文档三

猜你喜欢

转载自blog.csdn.net/qq_33371766/article/details/119835081