一、事务的特性(ACID)
事务的四个基本特性通常称为ACID特性,确保了事务的正确性和一致性。
-
原子性(Atomicity)
- 定义:事务中的所有操作要么全部成功,要么全部失败。即一个事务作为一个整体执行,不可分割。
- 举例:
假设一个银行转账操作涉及从账户A中扣款并将钱存入账户B中,如果在存款操作之前发生了错误,那么就不会扣款,保证转账操作的原子性。
-
一致性(Consistency)
- 定义:事务的执行会使数据库从一个一致的状态转变到另一个一致的状态。事务执行前后,数据库应该遵守所有的约束、规则和数据完整性。
- 举例:
假设转账操作前,账户A和账户B的余额分别是1000和2000,执行转账操作后,账户A的余额应该减少相应金额,账户B的余额增加相应金额,操作完成后系统的数据状态依然是合法和一致的。
-
隔离性(Isolation)
- 定义:一个事务的执行不应受到其他事务的干扰,事务的中间状态对其他事务是不可见的。
- 举例:
两个事务同时访问同一数据时,每个事务看到的数据是独立的。例如,在银行转账时,如果有两个事务同时操作同一个账户,事务A的操作不能看到事务B的中间结果,反之亦然。
-
持久性(Durability)
- 定义:一旦事务提交,它对数据库的更改是永久性的,即使系统发生崩溃,已提交的事务修改的数据也不会丢失。
- 举例:
当银行转账成功提交后,即使发生系统故障,账户A和账户B的余额也不会丢失或恢复到之前的状态。
二、Spring事务与MySQL事务的关系
Spring的事务管理是基于MySQL等数据库的原生事务进行的。Spring事务是对底层数据库事务的一层封装,它简化了事务的管理,提供了更高级的功能,如事务传播机制、回滚规则等。
-
Spring事务管理:
- Spring提供了声明式事务管理和编程式事务管理两种方式。声明式事务管理主要通过
@Transactional注解实现。 - Spring使用
DataSourceTransactionManager或JpaTransactionManager等事务管理器来与底层数据库的事务管理进行交互。
- Spring提供了声明式事务管理和编程式事务管理两种方式。声明式事务管理主要通过
-
MySQL事务管理:
- MySQL的事务基于其数据库引擎(如InnoDB)。MySQL事务的隔离级别、回滚、提交等操作是通过
START TRANSACTION、COMMIT、ROLLBACK等SQL语句来控制的。 - MySQL本身不提供事务传播机制,事务管理是由应用程序(如Spring)进行控制的。
- MySQL的事务基于其数据库引擎(如InnoDB)。MySQL事务的隔离级别、回滚、提交等操作是通过
-
Spring与MySQL事务结合:
- Spring事务管理通过与MySQL的连接池(如
HikariCP)结合来管理事务。当Spring开启一个事务时,实际是在底层数据库(如MySQL)上开启一个事务。 - Spring的事务管理可以通过
@Transactional注解控制事务的回滚规则、传播行为等,而MySQL则负责执行具体的数据库操作。
- Spring事务管理通过与MySQL的连接池(如
三、事务与线程以及上下文的关系
-
事务与线程:
- 在Spring中,事务是绑定到当前线程的。每个线程在执行事务操作时,会为其创建一个事务上下文。
- 一个线程内的多个方法调用,如果传播机制是
REQUIRED或SUPPORTS,那么它们共享同一个事务上下文。
-
事务上下文:
- 事务上下文包含了事务的状态(如开始、提交、回滚)、数据库连接等信息。Spring会通过
TransactionManager来管理这个上下文。 - Spring事务是基于线程的,每个线程有自己的事务上下文,因此不会跨线程传播。
- 事务上下文包含了事务的状态(如开始、提交、回滚)、数据库连接等信息。Spring会通过
四、事务与非事务
-
事务方法与非事务方法:
- 事务方法:方法上加了
@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.若 trx_id == creator_trx_id:当前事务修改的版本,可见
- 2.若 trx_id < min_trx_id:该版本由已提交的事务生成(因为其 ID 小于所有活跃事务的最小 ID),可见
- 3.若 trx_id >= max_trx_id:该版本由生成 Read View 后才启动的事务生成,不可见
- 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实现,每个线程都有自己独立的事务上下文。当你在一个线程中开启事务,然后在另一个线程中执行数据库操作时,新线程无法访问原线程的事务上下文,导致事务失效
1047

被折叠的 条评论
为什么被折叠?



