事务、事务传播机制、事务隔离级别

一、事务的特性(ACID)

事务的四个基本特性通常称为ACID特性,确保了事务的正确性和一致性。

  1. 原子性(Atomicity)

    • 定义:事务中的所有操作要么全部成功,要么全部失败。即一个事务作为一个整体执行,不可分割。
    • 举例
      假设一个银行转账操作涉及从账户A中扣款并将钱存入账户B中,如果在存款操作之前发生了错误,那么就不会扣款,保证转账操作的原子性。
  2. 一致性(Consistency)

    • 定义:事务的执行会使数据库从一个一致的状态转变到另一个一致的状态。事务执行前后,数据库应该遵守所有的约束、规则和数据完整性。
    • 举例
      假设转账操作前,账户A和账户B的余额分别是1000和2000,执行转账操作后,账户A的余额应该减少相应金额,账户B的余额增加相应金额,操作完成后系统的数据状态依然是合法和一致的。
  3. 隔离性(Isolation)

    • 定义:一个事务的执行不应受到其他事务的干扰,事务的中间状态对其他事务是不可见的。
    • 举例
      两个事务同时访问同一数据时,每个事务看到的数据是独立的。例如,在银行转账时,如果有两个事务同时操作同一个账户,事务A的操作不能看到事务B的中间结果,反之亦然。
  4. 持久性(Durability)

    • 定义:一旦事务提交,它对数据库的更改是永久性的,即使系统发生崩溃,已提交的事务修改的数据也不会丢失。
    • 举例
      当银行转账成功提交后,即使发生系统故障,账户A和账户B的余额也不会丢失或恢复到之前的状态。

二、Spring事务与MySQL事务的关系

Spring的事务管理是基于MySQL等数据库的原生事务进行的。Spring事务是对底层数据库事务的一层封装,它简化了事务的管理,提供了更高级的功能,如事务传播机制、回滚规则等。

  1. Spring事务管理

    • Spring提供了声明式事务管理和编程式事务管理两种方式。声明式事务管理主要通过@Transactional注解实现。
    • Spring使用DataSourceTransactionManagerJpaTransactionManager等事务管理器来与底层数据库的事务管理进行交互。
  2. MySQL事务管理

    • MySQL的事务基于其数据库引擎(如InnoDB)。MySQL事务的隔离级别、回滚、提交等操作是通过START TRANSACTIONCOMMITROLLBACK等SQL语句来控制的。
    • MySQL本身不提供事务传播机制,事务管理是由应用程序(如Spring)进行控制的。
  3. Spring与MySQL事务结合

    • Spring事务管理通过与MySQL的连接池(如HikariCP)结合来管理事务。当Spring开启一个事务时,实际是在底层数据库(如MySQL)上开启一个事务。
    • Spring的事务管理可以通过@Transactional注解控制事务的回滚规则、传播行为等,而MySQL则负责执行具体的数据库操作。

三、事务与线程以及上下文的关系

  1. 事务与线程

    • 在Spring中,事务是绑定到当前线程的。每个线程在执行事务操作时,会为其创建一个事务上下文。
    • 一个线程内的多个方法调用,如果传播机制是REQUIREDSUPPORTS,那么它们共享同一个事务上下文。
  2. 事务上下文

    • 事务上下文包含了事务的状态(如开始、提交、回滚)、数据库连接等信息。Spring会通过TransactionManager来管理这个上下文。
    • Spring事务是基于线程的,每个线程有自己的事务上下文,因此不会跨线程传播。

四、事务与非事务

  1. 事务方法与非事务方法

    • 事务方法:方法上加了@Transactional注解,Spring会为其创建一个事务上下文,在方法执行期间管理事务的提交和回滚。
    • 非事务方法:没有加@Transactional注解的方法,执行时不会涉及事务管理。即使在事务方法中调用非事务方法,非事务方法也不会受到事务控制。

五、事务的调用关系、传播机制及与非事务交互的完整代码示例

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionExampleService {

    // 事务方法:事务方法调用非事务方法
    // 1. 在 Spring 中,如果一个事务方法调用非事务方法,非事务方法不会参与事务管理
    // 2. 例如:`methodB` 中的操作不会在 `methodA` 的事务上下文内运行, 
    //    即使 `methodA` 是事务方法,`methodB` 的数据库操作也不会受到事务控制。
    @Transactional
    public void methodA() {
        System.out.println("Method A started (Transaction)");

        // 调用非事务方法methodB
        methodB();

        System.out.println("Method A completed (Transaction)");
    }

    // 非事务方法:没有事务控制
    public void methodB() {
        System.out.println("Method B started (No Transaction)");
        // methodB 中没有事务管理
        // 这里如果有数据库操作,它不会受到事务管理,即使methodA是事务方法
        System.out.println("Method B completed (No Transaction)");
    }
}

@Service
public class TransactionPropagationExample {

    // 事务传播机制示例
    // 1. REQUIRED(默认传播机制):
    //  - 如果当前没有事务,创建一个新事务;如果当前已有事务,加入该事务。
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        System.out.println("Method A started (REQUIRED)");

        // 方法B会加入methodA的事务
        methodB();  

        System.out.println("Method A completed (REQUIRED)");
    }

    // 2. REQUIRES_NEW:
    //  - 无论当前是否已有事务,都会启动一个新事务,并挂起当前事务。
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        System.out.println("Method B started (REQUIRES_NEW)");
        // methodB 会开启一个新的事务,挂起 methodA 的事务,如果此时事务A回滚了,B不会收到影响,因为二者不是一个事务上下文
        // 即使 methodA 已经在事务中,methodB 会在新的事务上下文中执行
        System.out.println("Method B completed (REQUIRES_NEW)");
    }
}

@Service
public class AdvancedTransactionPropagation {

    // 事务传播机制的更多示例
    // 1. Propagation.SUPPORTS:如果当前存在事务,方法加入该事务;如果没有事务,方法以非事务
// 2. Propagation.NOT_SUPPORTS:如果当前存在事务,挂起该事务,以非事务运行
// 3. Propagation.NEVER:必须在非事务环境下执行,如果当前存在事务,则抛出异常
// 4. Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常
    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodA() {
        System.out.println("Method A started (SUPPORTS)");

        // methodB 如果有事务,加入事务;如果没有事务,则以非事务方式执行
        methodB();  

        System.out.println("Method A completed (SUPPORTS)");
    }

    // 2. Propagation.MANDATORY:方法必须在一个已经存在的事务中执行;如果没有事务则抛出异常
    @Transactional(propagation = Propagation.MANDATORY)
    public void methodB() {
        System.out.println("Method B started (MANDATORY)");
        // 如果调用此方法时没有事务,Spring会抛出异常
        System.out.println("Method B completed (MANDATORY)");
    }
}

@Service
public class TransactionAndNonTransactionInteraction {

    // 事务方法调用非事务方法
    // 1. 事务方法调用非事务方法时,非事务方法不会参与事务管理。
    @Transactional
    public void methodA() {
        System.out.println("Method A started (Transaction)");

        // 调用非事务方法methodB
        methodB();

        System.out.println("Method A completed (Transaction)");
    }

    // 非事务方法
    public void methodB() {
        System.out.println("Method B started (No Transaction)");
        // 由于methodB没有@Transactional注解,methodB的操作不会受到事务管理
        // 例如:methodB中的数据库操作不会在methodA的事务上下文内执行
        System.out.println("Method B completed (No Transaction)");
    }
}

@Service
public class NestedTransactionExample {

    // 事务传播机制示例:NESTED(嵌套事务)
    // 1. Propagation.NESTED:如果当前有事务,方法会开启一个嵌套事务, 
    //    如果没有事务,则像REQUIRED一样开启新事务
    @Transactional(propagation = Propagation.NESTED)
    public void methodA() {
        System.out.println("Method A started (NESTED)");

        // 调用methodB,methodB会在methodA的嵌套事务中执行
        methodB();  

        System.out.println("Method A completed (NESTED)");
    }

    // 2. methodB 也使用事务传播机制 NESTED
    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        System.out.println("Method B started (NESTED)");
        // methodB会在methodA的事务上下文中执行一个嵌套事务, 
        // 如果methodB的事务发生回滚,不会影响到外部的methodA事务
        //父事务和子事务的回滚与提交:
            //子事务回滚:
                //如果子事务发生异常并回滚,那么子事务会回滚到它开始时设置的保存点。子事务回滚不会直接影响父事务。父事务仍然可以决定是否提交或回滚。
            //父事务回滚:
                //如果父事务回滚,所有嵌套的子事务都会回滚。即使某个子事务已经提交了,它也会回滚到父事务的回滚点。
    //父事务提交:
        //如果父事务提交,且子事务都没有回滚,所有操作都会提交。
        System.out.println("Method B completed (NESTED)");
    }
}


@Transactional(propagation = Propagation.REQUIRED)
public void a() {
    // a方法开始,假设这里有一个事务
    b();  // 调用b方法
    // 执行其他操作
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
    // b方法开始,挂起a的事务并创建新的事务
    c();  // 调用c方法
    // 执行其他操作
}

@Transactional(propagation = Propagation.REQUIRED)
public void c() {
    // c方法开始,加入b创建的新的事务
    // 执行操作
}




@Transactional(propagation = Propagation.REQUIRED)
public void a() {
    // a方法开始,假设这里有一个事务
    b();  // 调用b方法
    c();  // 调用c方法
    // 执行其他操作
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
    // b方法开始,挂起a的事务并创建新的事务
    // 执行操作
}

@Transactional(propagation = Propagation.REQUIRED)
public void c() {
    // c方法开始,加入到a的方法的事务
    // 执行操作
}



六。事务隔离级别

1. READ UNCOMMITTED(读取未提交)

场景:假设有两个事务,事务A和事务B,同时操作相同的数据。

  • 事务A执行一条更新语句,将用户表中的某个用户的余额从100改为150,但事务A还没有提交。
  • 事务B在同一时刻,读取了用户余额,发现余额为150,尽管事务A还没有提交这个更改。
  • 此时事务B读取的数据是脏数据,因为它读取到了未提交的事务A的数据。如果事务A在之后回滚,事务B读取到的150就变得无效。

问题:这种情况会发生“脏读”,即事务B读取到了未提交的、可能会回滚的数据。

2. READ COMMITTED(读取已提交)

场景:同样是两个事务A和B,事务A执行更新,事务B读取数据。

  • 事务A将用户的余额从100改为150并提交。
  • 事务B读取该余额,这时它读取到的是150。
  • 如果事务A在接下来再次更新余额,将余额从150改为200,并提交。
  • 事务B再次读取余额时,发现余额变成了200。

问题READ COMMITTED避免了脏读,但可能发生不可重复读。在事务B执行的过程中,事务A提交了更新操作,导致事务B的读取结果发生变化。

3. REPEATABLE READ(可重复读)(MySQL默认)

场景:依然是两个事务A和B,但这次事务B在读取数据时会受到锁的保护。

  • 事务A将用户余额从100改为150,并提交。
  • 事务B读取余额时,获得了150,但此时事务B对余额进行了锁定(这由数据库实现自动进行),因此事务B后续再读取余额时,仍然看到的是150。
  • 然后,事务A再次将余额改为200并提交。
  • 事务B再次读取余额时,仍然是150,而不是200。

问题REPEATABLE READ避免了脏读和不可重复读,但仍然可能出现幻读。如果事务B查询的是某个特定条件下的数据(例如,查询某个特定用户的所有交易记录),并且其他事务在这个过程中插入或删除了符合条件的数据,事务B就可能读取到不同的结果集。

由MVCC机制实现,组件undolog,ReadView,读已提交是一个事务每次查询都会生成快照,可重复读只有第一次加快照。

3.1MVCC机制原理:

a. Read View 基本概念

Read View(读视图)是一个"快照",用于判断当前事务能看到哪些版本的数据。它包含四个核心参数:​

  • m_ids​:生成 Read View 时,当前活跃(未提交)的事务 ID 列表
  • min_trx_id​:m_ids 中的最小事务 ID(活跃事务的最小 ID)
  • max_trx_id​:InnoDB 下一个要分配的事务 ID(大于当前所有已分配的 ID)
  • creator_trx_id​:当前生成 Read View 的事务 ID
b. 版本可见性判断规则

对于版本链中的某一行版本(其事务 ID 为 trx_id),Read View 会按以下逻辑判断是否可见:

  1. 1.若 trx_id == creator_trx_id​:当前事务修改的版本,可见
  2. 2.若 trx_id < min_trx_id​:该版本由已提交的事务生成(因为其 ID 小于所有活跃事务的最小 ID),可见
  3. 3.若 trx_id >= max_trx_id​:该版本由生成 Read View 后才启动的事务生成,不可见
  4. 4.若 min_trx_id <= trx_id < max_trx_id​:
    • 若 trx_id 在 m_ids 中(属于活跃事务):该事务未提交,版本不可见
    • 若 trx_id 不在 m_ids 中(活跃事务已提交):版本可见
c. 对判断条件的解读

首先,为什么 trx_id 还能大于 max_trx_id?​

原因在于线程的并发性。当你生成快照之后可能不会立马执行 select 操作,可能有其他线程抢占。假设恰好 max_trx_id(或者更大的)事务所在的线程抢到了时间片,并完整的执行完了修改数据的事务(修改了该数据的最新版本)。这时候 select 操作所在的事务抢回了时间片,因为数据的最新版本变了,所以就出现了会大于 max_trx_id 的情况。

再者,trx_id 可能不在 m_ids 中,为啥?​

还是线程的并发性。事务并不一定会按事务的 id 顺序执行完,所以 m_ids 不是连续的数据。如果不在 m_ids 证明事务已经提交了,虽然还在最小值和最大值之间。虽然不按顺序执行,但 InnoDB 总会分配 ID 更大的事务到线程等待队列,这个要注意。

d. MVCC 解决的问题

MVCC 解决了并发控制的什么问题?​

其实就是解决读写冲突的问题,对于写写冲突老老实实加行锁。

e. 补充说明
  • Read View 是快照,在可重复读的隔离级别下,同一事务多次 select 操作只会生成第一次 select 操作的快照

4. SERIALIZABLE(可串行化)

场景:两个事务A和B依然操作相同的数据,但此时两者会完全串行执行,数据库通过锁定表或行来确保两者不发生交叉操作。

  • 事务A将用户余额从100改为150,并提交。
  • 事务B在事务A提交之前,无法读取到余额150的数据,直到事务A提交完成。
  • 如果事务B尝试在事务A未提交时读取余额,数据库会在事务B上进行锁定,直到事务A完成。
  • 这样,事务B会被完全阻塞,直到事务A提交或者回滚,确保事务A和事务B的数据操作是完全串行的。

问题SERIALIZABLE通过严格的锁定机制避免了脏读、不可重复读和幻读,但性能最低。事务的执行是完全串行化的,其他事务必须等待当前事务完成,这会大大影响并发性和性能。

与可重复读的区别:读已提交是a事务提交之后b事务立马可见,串行化是必须等a释放锁,例如a有共享锁,锁没释放,b只能读不能写。

原理: 查加共享锁  增删改加排它锁

七。事务上下文/临时保存区

1. 事务的临时保存区域与隔离性

数据库中的事务不仅是针对数据的操作,而且事务会在执行过程中有一个临时保存区域,称为事务上下文事务工作区。每个事务在执行时,会在这个临时区域内进行数据的修改、插入或删除,这些变更在事务提交之前对其他事务是不可见的,直到提交之后才会正式反映到数据库中。

在并发环境中,多个事务可能会同时对数据库中的数据进行操作。为了确保事务的隔离性,数据库管理系统(DBMS)通过控制事务对数据的访问,确保每个事务的操作不会互相干扰。这里的“隔离性”指的是每个事务应该像是独立执行的,彼此之间没有直接的干扰。不同的事务隔离级别提供了不同程度的并发性和一致性保障。

2.临时保存区域与隔离级别的关系

  • 脏读(Dirty Read):在最低隔离级别READ UNCOMMITTED下,事务A的临时更改可以被事务B读取,即使事务A尚未提交。由于事务A的更改可能会回滚,事务B可能会读取到无效数据。
  • 不可重复读(Non-repeatable Read):在READ COMMITTED下,事务A的临时数据提交前是不可见的,但一旦提交,事务B可能会读取到不同的数据版本。也就是说,事务B多次读取相同数据时,可能会看到不同的值。
  • 幻读(Phantom Read):在REPEATABLE READ下,事务会保证每次读取相同的结果,但若有其他事务插入或删除了数据行,事务B在重复查询时可能会遇到不同的数据集。
  • 完全隔离:在SERIALIZABLE级别下,事务之间完全串行执行,所有的修改会在事务提交时才对其他事务可见,从而避免了脏读、不可重复读和幻读。

这些隔离级别的设置,决定了事务在操作其临时保存区域的数据时,是否会对其他事务产生影响。高隔离级别会确保一个事务的数据修改不被其他事务看到,直到它被提交,避免了不一致的读操作,但同时也会牺牲并发性。


通过这个临时保存区域,事务在执行过程中会先在自己的工作区内处理数据,只有在提交时,数据的修改才会对其他事务可见。如果有任何事务在提交前回滚,那么所有的变更都会被撤销,数据库恢复到事务开始之前的状态。这种机制有效地保证了数据的一致性和隔离性。

八。spring事务失效各种原因

1.方法不是public的,或者加了final

2.方法发生了this()自调用

3.自己new了个对象,没用代理对象

4.没被spring管理,比如没有注解

5.捕获异常没有抛出,事务不会回滚,也就算失效了

6.spring事务配置类,没有加@Configuration(spring中事务要显式配置才会生效)

// 1. 配置类:启用事务 + 配置事务管理器
@Configuration
@EnableTransactionManagement // ✅ 开关:启用事务功能
@ComponentScan("com.example")
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource(); // 数据源
    }
    
    @Bean
    public PlatformTransactionManager transactionManager() { // ✅ 事务控制器
        return new DataSourceTransactionManager(dataSource());
    }
}

7.或者不在一个线程中进行业务,Spring事务管理基于ThreadLocal实现,每个线程都有自己独立的事务上下文。当你在一个线程中开启事务,然后在另一个线程中执行数据库操作时,新线程无法访问原线程的事务上下文,导致事务失效

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值