记录一下事务的理解
为什么要有事务?
原子性 → 失败可回滚。
一致性 → 数据状态始终正确
隔离性 → 并发读写不相互干扰(通过加锁/MVCC实现)。
持久性 → 提交后不丢数据(通过redo log、双写缓冲等手段实现)。
一致性和其他 ACID 的关系
原子性:保证部分成功、部分失败不会破坏一致性。
隔离性:防止并发事务互相干扰,导致数据不一致。
持久性:保证提交后的结果不会丢失,否则数据可能不一致。
一致性是最终目标,其他三个是保证手段。
隔离性要解决什么问题?
多个事务并发执行时,读写互不干扰。其中:
- 保证读不干扰,主要依赖 根据隔离级别确定使用 行级锁(S 锁)或 MVCC (快照)机制。
- 保证写不干扰,主要依赖 行级锁(X 锁)。
读不干扰,根据不同的适用场景,划分具体的隔离级别,不同的隔离级别体现在:
- 读未提交 → 不加锁,可读脏数据。
- 读已提交 → 不加锁,过滤了未提交的数据,避免脏读。
- 可重复读 → 使用MVCC机制(快照读),避免不可重复读。
- 串行化 → 加 S 锁(当前读)+ 间隙锁,可以避免幻读。
注:
- S 锁:允许事务 读取一行数据,但 不允许修改。其他事务也可以加 S 锁(共享读取)。
- X 锁:允许事务 修改一行数据,同时阻止其他事务对该行加任何类型的锁(S 或 X)。
- 串行化隔离级别下,普通 SELECT 会被隐式转换成 SELECT ... LOCK IN SHARE MODE。
- 在 InnoDB 中,如果一个事务已经对某行加了 S 锁,它 可以尝试升级成 X 锁,比如通过 UPDATE 或 SELECT … FOR UPDATE,这叫 锁升级。但成功升级的前提是 没有其他事务持有 S 锁或 X 锁,否则会阻塞,直到其他事务释放锁(或触发死锁检测机制回滚其中一个事务)。
- SELECT 语句默认是快照读,而 LOCK IN SHARE MODE 与 FOR UPDATE 都是当前读。LOCK IN SHARE MODE 与 FOR UPDATE 两者应用场景区别是?前者用于在当前事务处理过程中(比如余额校验)阻止别的事务修改数据(存取余额)这种场景。后者用于当前事务本身需要修改数据。
写不干扰,体现在当事务里执行插入/更新/删除等操作时,会对匹配的行加上 行级排他锁(X 锁),这个锁会 一直持有到事务提交或回滚,不会提前释放。(跟隔离级别无关,任意隔离级别的写操作都会加 X 锁)
注意隔离性与隔离级别是两个不同的概念。对于复杂事务,需要对当前需要处理的数据主动使用 S 锁、X 锁 或 MVCC 等机制,而不是依赖默认的隔离级别处理。
隔离级别
SQL 标准定义的 4 个隔离级别:
隔离级别 能避免的问题 可能出现的问题 特点
READ UNCOMMITTED 无 脏读、不可重复读、幻读 几乎不用
READ COMMITTED 避免脏读 不可重复读、幻读 Oracle 默认
REPEATABLE READ (MySQL 默认 InnoDB) 避免脏读、不可重复读 幻读(InnoDB 里用 next-key lock 也避免了) MySQL 默认
SERIALIZABLE 避免所有问题 事务完全串行,性能最差 很少用
隔离级别解决的问题是读不一致,比如:
- 脏读 (Dirty Read):读到了别人未提交的数据。
- 不可重复读 (Non-repeatable Read):同一个事务两次查询同一行,结果不同。
- 幻读 (Phantom Read):同一个事务两次查询满足条件的行数不一样(新增/删除了行)。
不同隔离级别的适用场景:
- 金融、电商支付场景 → 更注重一致性 → REPEATABLE READ 或 SERIALIZABLE。
- 报表、日志分析 → 更注重性能、允许读到最新已提交数据 → READ COMMITTED。
Spring中的事务
Spring中四种基本传播行为
REQUIRED 有则同生共死(同一个事务),无则自立门户(默认传播行为)
REQUIRES_NEW 自求多福(将之前的事务挂起,自己新创建一个事务)
NESTED 父死儿必死,儿死父不死(寄生关系、舔狗)
SUPPORTS 有事务就加进去,没有则不管(懒狗)
Spring中什么异常会导致事务回滚?
Spring默认针对抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。