SpringBoot 例外処理のロールバック トランザクションの詳細 (自動ロールバック、手動ロールバック、部分ロールバック)

参考:https://blog.csdn.net/zzhongcy/article/details/102893309

コンセプト

トランザクション定義

トランザクションとは、データベースを操作するための一連のアクションです。トランザクションは、現代のデータベース理論の中核となる概念の 1 つです。一連の処理ステップがすべて実行されるか、まったく実行されない場合、その処理ステップはトランザクションと呼ばれます。すべてのステップが 1 つの操作として完全に実行されると、トランザクションがコミットされたと言います。一部または複数のステップが失敗し、ステップがコミットされなかったため、トランザクションを元のシステム状態にロールバックする必要があります。


トランザクションの特性

  • 原子性: トランザクション内のデータベースに対するすべての操作は、すべてまたはまったく行われない、分割できない一連の操作です。

  • 一貫性: トランザクションの実行によってデータが破損することはありません

  • 分離: トランザクションの実行は他のトランザクションによって干渉されません。つまり、同時に実行されるトランザクションは互いに干渉しません。

  • 永続性: トランザクションがコミットされると、データベースへの変更は永続的になります。


トランザクション実装メカニズム

Spring はトランザクション管理のための豊富な機能サポートを提供します。Spring トランザクション管理は、コード化と宣言の 2 つの方法に分かれています。

  • プログラムによるトランザクション管理: プログラムによるトランザクション管理は、TransactionTemplate を使用するか、基盤となる PlatformTransactionManager を直接使用します。プログラムによるトランザクション管理の場合、Spring では TransactionTemplate の使用を推奨しています。
  • 宣言型トランザクション管理: AOP の上に構築されます。その本質は、前後のメソッドをインターセプトし、対象のメソッドが開始される前にトランザクションを作成または参加し、対象のメソッドの実行後に実行ステータスに応じてトランザクションをサブミットまたはロールバックすることです。
  • 宣言型トランザクション管理は煩雑なコードを必要とせず、より高速でシンプルなので、推奨されます。

宣言的トランザクションには 2 つのタイプがあります。

  • 1 つは、関連するビジネス ルールを設定ファイル (xml) で宣言することです。

  • もう 1 つは@Transactionalアノテーションに基づいています。アノテーション構成は現在一般的な使用方法であり、推奨されています。


アプリケーション システムが @Transactional を宣言したターゲット メソッドを呼び出すと、Spring Framework はデフォルトで AOP プロキシを使用し、コードの実行時にプロキシ オブジェクトを生成します。@Transactional の属性構成情報に従って、このプロキシ オブジェクトは、@Transactional のターゲット メソッドがインターセプタ TransactionInterceptor によってインターセプトされるかどうかを決定します。抽象トランザクション マネージャー AbstractPlatformTransactionManager は、データ ソース DataSource を操作してトランザクションをコミットまたはロールバックします。

Spring AOPプロキシにはCglibAopProxyとJdkDynamicAopProxyの2種類があり、CglibAopProxyを例にとると、CglibAopProxyの場合は内部クラスのDynamicAdvisedInterceptorのインターセプトメソッドを呼び出す必要があります。JdkDynamicAopProxy の場合は、その invoke メソッドを呼び出す必要があります。


オープントランザクション

Spirng Boot ではデフォルトでトランザクションが有効になっており、何もする必要はなく、@Transactional を直接使用するだけです。


Spring がトランザクションを開始する方法:

方法 1: Spring での純粋な XML 構成トランザクション

<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>

方法 2: Spring での XML+アノテーション構成トランザクション

一般に、粗粒度の制御は XML で構成され、その後アノテーションが使用されます。

<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"/>

方法 3: Spring の純粋なアノテーション構成トランザクション。**@EnableTransactionManagement** アノテーションを使用してトランザクション管理機能も有効にすることができます

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

@Transactional の使用に注釈を付ける

アノテーション @Transactional 共通設定

パラメータ名 機能説明
読み取り専用 現在のトランザクションが読み取り専用トランザクションかどうかを設定するために使用されます。true に設定すると読み取り専用を意味し、false に設定すると読み取り/書き込みを意味し、デフォルト値は false です。例: @Transactional(readOnly=true)
ロールバック用 ロールバックする必要がある例外クラスの配列を設定するために使用され、指定された例外配列内の例外がメソッド内でスローされた場合、トランザクションはロールバックされます。例: 単一の例外クラスを指定する場合: @Transactional(rollbackFor=RuntimeException.class)、複数の例外クラスを指定する場合: @Transactional(rollbackFor={RuntimeException.class, Exception.class})
トランザクションマネージャー / 値 Springコンテナで複数のトランザクションマネージャをホストする場合、トランザクションマネージャのBean名を指定する
クラス名のロールバック ロールバックする必要がある例外クラス名の配列を設定するために使用され、指定された例外名の配列内の例外がメソッド内でスローされた場合、トランザクションはロールバックされます。例: 単一の例外クラス名を指定します @Transactional(rollbackForClassName="RuntimeException") 複数の例外クラス名を指定します: @Transactional(rollbackForClassName={"RuntimeException","Exception"})
noRollbackFor ロールバックする必要のない例外クラスの配列を設定するために使用され、指定した例外配列内の例外がメソッド内でスローされた場合、トランザクションはロールバックされません。例: 単一の例外クラスを指定する: @Transactional(noRollbackFor=RuntimeException.class) 複数の例外クラスを指定する: @Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName ロールバックする必要のない例外クラス名の配列を設定するために使用され、指定した例外名配列の例外がメソッド内でスローされた場合、トランザクションはロールバックされません。例: 単一の例外クラス名を指定します: @Transactional(noRollbackForClassName="RuntimeException") 複数の例外クラス名を指定します: @Transactional(noRollbackForClassName={"RuntimeException", "Exception"})
伝搬 トランザクションの伝播動作を設定するために使用されます。例: @Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)
隔離 基礎となるデータベースのトランザクション分離レベルを設定するために使用されます。トランザクション分離レベルは、複数トランザクションの同時実行に対処するために使用されます。通常、データベースのデフォルトの分離レベルを使用でき、基本的に設定する必要はありません
タイムアウト このプロパティは、トランザクションのタイムアウト秒数を設定するために使用されます。デフォルト値は -1 で、タイムアウトしないことを意味します。
タイムアウト設定: @Transactional(timeout=30)、30 秒に設定されます。

伝播のプロパティ (トランザクションの伝播動作)

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

Propagation属性 意味
必要 デフォルト値はトランザクションが存在する場合に実行され、現在のトランザクションが存在しない場合は新しいトランザクションが作成されます。
サポート 現在トランザクションがある場合はトランザクション状態で実行され、現在のトランザクションがない場合は非トランザクション状態で実行されます。
必須 トランザクション状態で実行する必要があります。現在トランザクションがない場合は、例外 IllegalTransactionStateException がスローされます。
REQUIRES_NEW 新しいトランザクションを作成して実行します。現在のトランザクションがある場合は、現在のトランザクションを一時停止します。
サポートされていません トランザクションがない状態で実行し、現在のトランザクションがある場合は現在のトランザクションを一時停止します。
一度もない トランザクションがない状態で実行します。現在のトランザクションがある場合は、例外 IllegalTransactionStateException がスローされます。

トランザクションの 5 つの分離レベル

例:@Transactional(isolation = Isolation.READ_COMMITTED)

分離レベル 意味
デフォルト これは、データベースのデフォルトのトランザクション分離レベルを使用する PlatfromTransactionManager のデフォルトの分離レベルであり、他の 4 つは JDBC の分離レベルに対応します。
READ_UNCOMMITTED 最も低い分離レベル。実際、トランザクションが完了するまで、他のトランザクションはトランザクションによって変更されたデータを見ることができるため、これを分離レベルと呼ぶべきではありません。他のトランザクションがコミットされる前に、トランザクションは他のトランザクションによって行われた変更を確認することもできます。ダーティ、ファントム、反復不可能な読み取りが発生する可能性があります
READ_COMMITTED ほとんどのデータベースのデフォルト レベル。トランザクションが完了するまで、他のトランザクションはトランザクションによって変更されたデータを参照できません。残念ながら、そのトランザクションがコミットされた後は、他のトランザクションによって挿入または更新されたデータを表示できます。これは、トランザクションのさまざまな時点で、他のトランザクションがデータを変更した場合、異なるデータが表示されることを意味します。ダーティ リードは防止されますが、ファントム リードや反復不可能なリードが発生する可能性があります。
REPEATABLE_READ ISOLATION_READ_COMMITTED よりも厳密なこの分離レベルでは、トランザクション内で特定のデータセットをクエリする場合、たとえ他のトランザクションがクエリされたデータを変更したとしても、少なくとも同じデータセットを再度クエリできることが保証されます。ただし、他のトランザクションが新しいデータを挿入した場合は、新しく挿入されたデータをクエリできます。ダーティ リードとノンリピータブル リードは防止できますが、ファントム リードが発生する可能性があります。
シリアル化可能 完全服从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 次读出来的记录数不一样

おすすめ

転載: blog.csdn.net/footless_bird/article/details/119530607