在 MySQL 数据库中,事务是确保数据一致性和完整性的重要机制。它允许将一系列数据库操作作为一个不可分割的整体进行处理,要么全部成功执行,要么全部回滚。
特性 (ACID)
原子性(Atomicity)
-
概念:原子性确保事务是一个不可分割的工作单元。事务中的所有操作要么全部成功执行,要么全部回滚。
-
实现:MySQL 通过
undo log
回滚日志来实现原子性。当事务执行过程中出现错误或被回滚时,undo log
记录的信息可用于撤销已经执行的操作,将数据库恢复到事务开始前的状态。
一致性(Consistency)
-
概念:一致性要求事务执行前后,数据库的完整性约束没有被破坏,数据从一个一致状态转换到另一个一致状态。
-
实现:一致性的实现依赖于原子性、隔离性和持久性的共同作用。原子性保证操作的完整性,隔离性防止并发干扰,持久性确保提交后的结果稳定,从而共同保障数据的一致性。
隔离性(Isolation)
-
概念:隔离性指多个事务并发执行时,一个事务的执行不能被其他事务干扰。
-
实现:MySQL 通过多版本并发控制(MVCC)和锁机制来实现隔离性。MVCC 为每个事务构建数据快照,使其在执行期间看到的数据一致;锁机制则通过对数据加锁,限制其他事务的访问。不同的隔离级别对并发事务的隔离程度不同,从而影响数据的一致性和系统的并发性能。
持久性(Durability)
-
概念:持久性表示事务一旦提交,它对数据库所做的修改就会永久保存到数据库中,即使数据库发生故障也不会丢失。
-
实现:MySQL 利用
redo log
重做日志来实现持久性。当事务提交时,相关修改先记录到redo log
中,后续再将数据持久化到磁盘。在数据库重启或发生故障时,可依据redo log
中的记录恢复数据,确保事务的持久性。
并发问题
脏读:脏读是指一个事务读取到了其他事务未提交的数据。
例如,事务 A 对某条数据进行修改但未提交,此时事务 B 读取了这条被修改但未提交的数据,若事务 A 最终回滚,事务 B 读取的数据就是无效的,这就产生了脏读问题。脏读会导致数据的不一致和不可靠。
不可重复读:不可重复读是指在同一个事务中,相同的查询操作在不同时间返回不同的结果。
例如,事务 A 在查询某条数据后,事务 B 对该数据进行修改并提交,当事务 A 再次执行相同查询时,得到的结果与第一次不同。不可重复读主要是由于其他事务对数据的修改导致的。
幻读:幻读是指在一个事务中,两次相同的查询条件下,查询到的结果集数量不一致,仿佛出现了 “幻影” 行。
例如,事务 A 查询满足某条件的记录数量,事务 B 在事务 A 两次查询之间插入了满足该条件的新记录并提交,事务 A 再次查询时记录数量增加,就产生了幻读现象。
隔离级别
隔离级别 | 特点 | 能否解决脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 可读取其他事务未提交的数据 | ❌ | ❌ | ❌ |
读已提交(Read Committed) | 只读取已提交事务的数据 | ✅ | ❌ | ❌ |
可重复读(Repeatable Read)(InnoDB默认) | 同一事务多次读取一致 | ✅ | ✅ | ⚠️(使用MVCC避免) |
串行化(Serializable) | 强制串行执行事务 | ✅ | ✅ | ✅ |
InnoDB在"可重复读"级别通过Next-Key Lock解决了幻读问题
MVCC机制解析
Read View: MVCC 的核心概念,用于判断一个事务对数据的可见。
在不同隔离级别下,Read View 的生成时机不同。读提交隔离级别每次查询时生成 Read View,可重复读隔离级别在启动事务时生成 Read View。
什么是 Read View?
- Read View 是事务读取数据时创建的“视图”;
- 用于生成数据的“快照”,实现一致性读取;
- 不同事务生成的 Read View 不同。
struct read_view_t {trx_id_t creator_trx_id; // 创建该视图的事务IDids_t m_ids; // 活跃事务ID列表trx_id_t min_trx_id; // 最小活跃事务IDtrx_id_t max_trx_id; // 预分配的下个事务ID
};
隐藏列
InnoDB 存储引擎中的每个数据行都包含一些隐藏列,其中
-
trx_id
记录了该数据发生改变时事务的id
值。 -
roll_pointer
是指向旧版本数据的指针,用于undo log
回溯。
工作原理:当事务查询数据时,依据 Read View 和隐藏列中的trx_id
判断数据的可见性。
- 若
trx_id < min_trx_id
,说明事务已提交,当前事务该数据可见。 - 若
trx_id >= min_trx_id
,表示事务未提交,当前事务该数据不可见。 - 当
min_trx_id < trx_id < max_trx_id
时,若trx_id
在m_ids
中,说明事务未提交,该数据不可见;若trx_id
不在m_ids
中,表明事务已提交,该数据可见。 - 在不同隔离级别下,Read View 的生成时机不同。读提交隔离级别每次查询时生成 Read View,可重复读隔离级别在启动事务时生成 Read View。
快照读 vs 当前读
类型 | 描述 | 使用场景 |
---|---|---|
快照读 | 使用 Read View 查看历史版本数据,不加锁 | SELECT 查询 |
当前读 | 直接读取最新记录,使用加锁机制 | SELECT ... FOR UPDATE 、UPDATE 、DELETE 等 |
长事务问题
问题:
- 锁竞争阻塞资源:长事务长时间持有锁,会阻塞其他事务对相关资源的访问,降低系统的并发性能。
- 死锁风险:多个长事务可能相互等待对方释放锁,从而导致死锁。
- 主从延迟:在主从复制架构中,长事务执行时间长,从库需要花费更多时间重放主库的事务操作,导致主从之间长时间数据不同步,影响数据的实时性。
- 回滚导致时间浪费:长事务执行时间长,若因某些原因需要回滚,回滚操作也会耗费大量时间,造成资源浪费。
解决:
- 拆分事务:尽量将 SQL 拆分为多个短事务。
- 控制事务粒度:减少单个事务处理的数据量。
- 及时提交事务:防止持有太久的 undo log。
- 使用合适的隔离级别:避免不必要的加锁操作。