SpringBoot exception handling rollback transaction details (automatic rollback, manual rollback, partial rollback)

Reference: https://blog.csdn.net/zzhongcy/article/details/102893309

concept

transaction definition

A transaction is a set of actions to operate a database. Transaction is one of the core concepts in modern database theory. A set of processing steps is called a transaction if either all of them occur or none of them are executed. When all the steps are completely performed as one operation, we say that the transaction is committed. Since some or more of the steps failed and no steps were committed, the transaction must be rolled back to the original system state.


Transaction characteristics

  • Atomicity: All operations on the database in a transaction are an indivisible sequence of operations, either all or none

  • Consistency: data will not be corrupted due to transaction execution

  • Isolation: the execution of a transaction is not interfered with by other transactions, that is, concurrently executed transactions do not interfere with each other

  • Persistence: Once a transaction is committed, its changes to the database are permanent.


Transaction Implementation Mechanism

Spring provides rich functional support for transaction management. Spring transaction management is divided into two ways: coded and declarative.

  • Programmatic transaction management: Programmatic transaction management uses TransactionTemplate or directly uses the underlying PlatformTransactionManager. For programmatic transaction management, spring recommends using TransactionTemplate.
  • Declarative transaction management: built on top of AOP. Its essence is to intercept the method before and after, then create or join a transaction before the target method starts, and submit or roll back the transaction according to the execution status after the target method is executed.
  • Declarative transaction management does not require intrusive code, is faster and simpler, and is recommended.

There are two types of declarative transactions:

  • One is to make relevant business rule declarations in the configuration file (xml)

  • The other is based on the @Transactional annotation. Annotation configuration is currently a popular way to use it and is recommended.


When the application system calls the target method that declares @Transactional, Spring Framework uses AOP proxy by default, and generates a proxy object when the code is running. According to the property configuration information of @Transactional, this proxy object determines whether the target method declared @Transactional is intercepted by the interceptor TransactionInterceptor. The abstract transaction manager AbstractPlatformTransactionManager operates the data source DataSource to commit or roll back the transaction.

There are two types of Spring AOP proxy, CglibAopProxy and JdkDynamicAopProxy. Taking CglibAopProxy as an example, for CglibAopProxy, the intercept method of DynamicAdvisedInterceptor of its internal class needs to be called. For JdkDynamicAopProxy, you need to call its invoke method.


open transaction

Spirng Boot enables transactions by default, no need to do anything, just use @Transactional directly


The way Spring starts a transaction:

Method 1: Pure XML configuration transaction in Spring

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="pooledDataSource"/>
</bean>
<aop:config>
    <aop:pointcut expression="execution(* cn.yuanyu.crud.service..*(..))" id="txPoint"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*"/>
        <tx:method name="get*" read-only="true"/>
    </tx:attributes>
</tx:advice>

Method 2: XML+annotation configuration transaction in Spring

Generally, coarse-grained control is configured in xml, and then annotations are used

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="pooledDataSource"/>
</bean>
<aop:config>
    <aop:pointcut expression="execution(* cn.yuanyu.crud.service..*(..))" id="txPoint"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*"/>
        <tx:method name="get*" read-only="true"/>
    </tx:attributes>
</tx:advice>
<!--
-->
<tx:annotation-driven transaction-manager="transactionManager"/>

Method 3: pure annotation configuration transaction in Spring, using **@EnableTransactionManagement** annotation can also enable transaction management function

@Configuration //声明配置类
@MapperScan("cn.yuanyu.tx.mapper")
@EnableTransactionManagement // 开启事务注解,等同于配置文件<tx:annotation-driven/>
public class MybatisPlusConfiguration {
    
    

Annotate the use of @Transactional

Annotation @Transactional common configuration

parameter name Functional description
readOnly It is used to set whether the current transaction is a read-only transaction, set to true means read-only, false means read-write, and the default value is false. For example: @Transactional(readOnly=true)
rollbackFor It is used to set the exception class array that needs to be rolled back. When an exception in the specified exception array is thrown in the method, the transaction will be rolled back. For example: specifying a single exception class: @Transactional(rollbackFor=RuntimeException.class); specifying multiple exception classes: @Transactional(rollbackFor={RuntimeException.class, Exception.class})
transactionManager / value When multiple transaction managers are hosted in the Spring container, specify the bean name of the transaction manager
rollbackForClassName It is used to set the array of exception class names that need to be rolled back. When an exception in the specified exception name array is thrown in the method, the transaction will be rolled back. For example: Specify a single exception class name @Transactional(rollbackForClassName="RuntimeException") Specify multiple exception class names: @Transactional(rollbackForClassName={"RuntimeException","Exception"})
noRollbackFor It is used to set the exception class array that does not need to be rolled back. When the exception in the specified exception array is thrown in the method, the transaction will not be rolled back. For example: specifying a single exception class: @Transactional(noRollbackFor=RuntimeException.class) specifying multiple exception classes: @Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName It is used to set the exception class name array that does not need to be rolled back. When the exception in the specified exception name array is thrown in the method, the transaction will not be rolled back. For example: Specify a single exception class name: @Transactional(noRollbackForClassName="RuntimeException") Specify multiple exception class names: @Transactional(noRollbackForClassName={"RuntimeException", "Exception"})
propagation Used to set the propagation behavior of transactions. For example: @Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)
isolation It is used to set the transaction isolation level of the underlying database. The transaction isolation level is used to deal with multi-transaction concurrency. Usually, the default isolation level of the database can be used, and there is basically no need to set it
timeout This property is used to set the timeout seconds of the transaction. The default value is -1, which means never timeout.
Timeout setting: @Transactional(timeout=30), set to 30 seconds

Properties of Propagation (Transaction Propagation Behavior)

例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

Propagation attribute meaning
REQUIRED The default value is executed when there is a transaction; if there is no current transaction, a new transaction will be created;
SUPPORTS If there is currently a transaction, it will be executed in the transaction state; if there is no current transaction, it will be executed in the non-transaction state;
MANDATORY It must be executed in a transaction state. If there is no transaction currently, an exception IllegalTransactionStateException will be thrown;
REQUIRES_NEW Create a new transaction and execute it; if there is a current transaction, suspend the current transaction;
NOT_SUPPORTED Execute in the state of no transaction; if there is a current transaction, suspend the current transaction;
NEVER Execute in no transaction state; if there is a current transaction, an exception IllegalTransactionStateException will be thrown.

5 isolation levels for transactions

例如:@Transactional(isolation = Isolation.READ_COMMITTED)

isolation level meaning
DEFAULT This is the default isolation level of PlatfromTransactionManager, using the default transaction isolation level of the database, and the other four correspond to the isolation level of JDBC;
READ_UNCOMMITTED The lowest isolation level. In fact, we should not call it an isolation level, because other transactions can see the data modified by the transaction until the transaction is completed. Before other transactions are committed, the transaction can also see the modifications made by other transactions. May cause dirty, phantom, non-repeatable read
READ_COMMITTED The default level for most databases. Until the transaction completes, other transactions cannot see the data modified by the transaction. Unfortunately, after that transaction commits, you can view data inserted or updated by other transactions. This means that at different points in the transaction, if other transactions have modified the data, you will see different data. Dirty reads are prevented, but phantom and non-repeatable reads can still occur.
REPEATABLE_READ Stricter than ISOLATION_READ_COMMITTED, this isolation level ensures that if you query a certain dataset within a transaction, you can at least query the same dataset again, even if other transactions modify the queried data. However, if other transactions insert new data, you can query the newly inserted data. Dirty reads and non-repeatable reads can be prevented, but phantom reads may still occur.
SERIALIZABLE 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。代价最大、可靠性最高的隔离级别,所有的事务都是按顺序一个接一个地执行。避免所有不安全读取。

使用注意事项(防止事务失效)

  • 在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。

  • @Transactional 注解应该只被应用在 public 修饰的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会报错(IDEA会有提示), 但事务并没有生效。

  • 被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务,则事务由子方法B和C各自控制

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,则无论子方法B和C是否声明事务,事务均不会生效

    • 被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制

    • 被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!

      如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:

      • 方案1:子方法中不用 try-catch 处理运行异常
      • 方案2:子方法的catch里面将运行异常抛出【throw new RuntimeException();】
  • 默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚。

    如果是checked异常则不回滚,例如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。

    若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型:@Transactional(rollbackFor = Exception.class

  • 数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务

  • 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚或者在catch里面将异常抛出【throw new RuntimeException();】

    • 方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值)

        try{
              
              
            ....  
        }catch(Exception e){
              
              
            logger.error("",e);
            throw new RuntimeException(e);
        }
      
    • 方案二:手动进行回滚【 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 】

        try{
              
              
            ...
        }catch(Exception e){
              
              
            log.error("fail",e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return false;
        }
      
  • @Transactional可以放在Controller下面直接起作用,看到网上好多同学说要放到@Component下面或者@Service下面,经过试验,可以不用放在这两个下面也起作用。

  • @Transactional引入包问题,它有两个包:

    import javax.transaction.Transactional; 
    // 和
    import org.springframework.transaction.annotation.Transactional; 		// 推荐
    

    这两个都可以用,对比了一下他们两个的方法和属性,发现后面的比前面的强大。建议使用后面的。


使用场景

自动回滚

直接抛出,不try/catch

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder() throws Exception {
    
      
     success();  
     //假如exception这个操作数据库的方法会抛出异常,方法success()对数据库的操作会回滚。 
     exception(); 
     return ApiReturnUtil.success();
}

手动回滚

进行try/catch,回滚并抛出

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){
    
      
    success();  
    try {
    
      
        exception(); 
     } catch (Exception e) {
    
      
        e.printStackTrace();     
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}

回滚部分异常

使用【Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint(); 】设置回滚点。

使用【TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);】回滚到savePoint。

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){
    
      
    success();  
    //只回滚以下异常,
    Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    try {
    
      
        exception(); 
     } catch (Exception e) {
    
      
        e.printStackTrace();     
        // 手工回滚事务
        TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}

手动创建、提交、回滚事务

PlatformTransactionManager 这个接口中定义了三个方法 getTransaction创建事务,commit 提交事务,rollback 回滚事务。它的实现类是 AbstractPlatformTransactionManager。

@Autowired
priDataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;

// 手动创建事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

// 手动提交事务
dataSourceTransactionManager.commit(transactionStatus);

// 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交)
dataSourceTransactionManager.rollback(transactionStatus);

事务失效不回滚的原因及解决方案

异常被捕获导致事务失效

在spring boot 中,使用事务非常简单,直接在方法上面加入@Transactional 就可以实现。

@GetMapping("delete")
@ResponseBody
@Transactional    
public void delete(@RequestParam("id") int id) {
    
           
	try {
    
              
        //delete country
        this.repository.delete(id);         
        if(id == 1){
    
                  
            throw Exception("测试事务");
        }           
         //delete city
        this.repository.deleteByCountryId(id);
    }catch (Exception e){
    
    
        logger.error("delete false:" + e.getMessage());        
    }
}

发现事务不回滚,即 this.repository.delete(id);成功把数据删除了。

原因:

默认spring事务只在发生未被捕获的 RuntimeException 时才回滚。

spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获 RuntimeException 的异常,但可以通过配置来捕获特定的异常并回滚。

换句话说在service的方法中不使用 try catch 或者在 catch 中最后加上throw new RuntimeExcetpion()抛出运行异常,这样程序异常时才能被aop捕获进而回滚。

解决方案:

  • 方案1:例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加

    throw new RuntimeException(); 语句,以便让aop捕获异常再去回滚,并且在service的上层要继续捕获这个异常。

  • 方案2:在service层方法的catch语句中进行手动回滚,这样上层就无需去处理异常。

    @GetMapping("delete") 
    @ResponseBody 
    @Transactional 
    public Object delete(@RequestParam("id") int id){
          
           
        if (id < 1){
          
          
             return new MessageBean(101,"parameter wrong: id = " + id) ; 
         } 
        try {
          
           
             //delete country
             this.countryRepository.delete(id);
             //delete city
             this.cityRepository.deleteByCountryId(id);
             return new MessageBean(200,"delete success");
         }catch (Exception e){
          
          
             logger.error("delete false:" + e.getMessage());
             // 手动回滚
             TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
             return new MessageBean(101,"delete false");
         }
    }
    

自调用导致事务失效

问题描述及原因

在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,否则会造成自调用问题。

若同一类中的 没有@Transactional 注解的方法 内部调用 有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。见 示例代码展示。

自调用问题示例:

@Service
public class OrderService {
    
    
    
    private void insert() {
    
    
        insertOrder();
    }

    @Transactional
    public void insertOrder() {
    
    
        //insert log info
        //insertOrder
        //updateAccount
    }
}

// insertOrder() 尽管有@Transactional 注解,但它被内部方法 insert()调用,事务被忽略,出现异常事务不会发生回滚,并且会报错类似于:org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope(翻译:没有Transaction无法回滚事务。自调用导致@Transactional 失效。)

自调用失效原因:

  • spring里事务是用注解配置的,当一个方法没有接口,单单只是一个内部方法时,事务的注解是不起作用的,需要回滚时就会报错。

  • 出现这个问题的根本原因是:

    @Transactional 的实现原理是AOP,AOP的实现原理是动态代理,而自调用时并不存在代理对象的调用,也就不会产生基于AOP 的事务回滚操作

  • 虽然可以直接从容器中获取代理对象,但这样有侵入之嫌,不推荐。


解决方案

方案1、在类上(或者最外层的公共方法)加事务

@Service
@Slf4j
public class MyTransactional {
    
    
    
    // 最外层公共方法。自动回滚事务方式,insertOrder()方法报错后事务回滚,且线程中止,后续逻辑无法执行
    @Transactional
    public void test1() {
    
    
        this.insertOrder();
        System.out.println("11111111111111111");
    }
    
    // 最外层公共方法。手动回滚事务方式,insertOrder()方法报错后事务回滚,可以继续执行后续逻辑
    @Transactional
    public void test2() {
    
    
        try {
    
      
        	insertOrder();
        } catch (Exception e) {
    
      
            log.error("faild to ...", e);
            // 手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            // 其他操作
        }
        // 其他操作
    }

    // 进行数据库操作的方法(private 或 public 均可)
    private void insertOrder() {
    
    
        //insert log info
        //insertOrder
        //updateAccount
    }
}

方案 2、使用AspectJ 取代 Spring AOP 代理

上面的两个问题@Transactional 注解只应用到 public 方法和自调用问题,是由于使用 Spring AOP 代理造成的。为解决这两个问题,可以使用 AspectJ 取代 Spring AOP 代理。

需要将下面的 AspectJ 信息添加到 xml 配置信息中。

AspectJ 的 xml 配置信息

<tx:annotation-driven mode="aspectj" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
</bean class="org.springframework.transaction.aspectj.AnnotationTransactionAspect" factory-method="aspectOf">
	<property name="transactionManager" ref="transactionManager" />
</bean> 

同时在 Maven 的 pom 文件中加入 spring-aspects 和 aspectjrt 的 dependency 以及 aspectj-maven-plugin。

AspectJ 的 pom 配置信息

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.2.RELEASE</version>
    </dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.9</version>
<configuration>
    <showWeaveInfo>true</showWeaveInfo>
    <aspectLibraries>
    <aspectLibrary>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        </aspectLibrary>
    </aspectLibraries>
</configuration>

<executions>
<execution>
    <goals>
        <goal>compile</goal>
        <goal>test-compile</goal>
    </goals>
</execution>
</executions>
</plugin>

其他

事务提交方式

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。

对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring 会将底层连接的【自动提交特性】设置为 false 。也就是在使用 spring 进行事务管理的时候,spring 会将【是否自动提交】设置为false,等价于JDBC中的 connection.setAutoCommit(false); ,在执行完之后在进行提交 connection.commit(); 。


事务回滚规则

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

默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。

可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。


事务并发会产生的问题

术语 含义
脏读 A事务读取到了B事务还未提交的数据,如果B未提交的事务回滚了,那么A事务读取的数据就是无效的,这就是数据脏读
不可重复读 在同一个事务中,多次读取同一数据返回的结果不一致,这是由于读取事务在进行操作的过程中,如果出现更新事务,它必须等待更新事务执行成功提交完成后才能继续读取数据,这就导致读取事务在前后读取的数据不一致的状况出现
幻读 A事务读取了几行记录后,B事务插入了新数据,并且提交了插入操作,在后续操作中A事务就会多出几行原本不存在的数据,就像A事务出现幻觉,这就是幻读

第一类丢失更新

没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出, 导致对数据的两个修改都失效了。

例如:

张三的工资为5000,事务A中获取工资为5000,事务B获取工资为5000,汇入100,并提交数据库,工资变为5100;
随后,事务A发生异常,回滚了,恢复张三的工资为5000,这样就导致事务B的更新丢失了。


脏读

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

例如:

张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。
与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。
随后,事务A发生异常,回滚了事务,张三的工资又回滚为5000。
最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。


不可重复读

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

例如:

在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。
  与此同时,事务B把张三的工资改为8000,并提交了事务。
  随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。


第二类丢失更新

不可重复读的特例。

有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

例如:

在事务A中,读取到张三的存款为5000,操作没有完成,事务还没提交。
  与此同时,事务B存入1000,把张三的存款改为6000,并提交了事务。
  随后,在事务A中,存储500,把张三的存款改为5500,并提交了事务,这样事务A的更新覆盖了事务B的更新。


幻读

是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

例如:

目前工资为5000的员工有10人,事务A读取到所有的工资为5000的人数为10人。
  此时,事务B插入一条工资也为5000的记录。
  这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。


不可重复读和幻读的区别

不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了

幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样

Guess you like

Origin blog.csdn.net/footless_bird/article/details/119530607