事务
事务的特性
原子性,一致性,隔离性,持久性
ACID
Atom:全做或者全都不做
Consistency:从一个一致状态到另一个一致状态
Isolation:并发无影响,事务之间无影响
Durability:持久性确保事务一旦提交,它对数据所做的更改就是永久性的,即使系统发生崩溃,数据也能恢复到最近一次提交的状态。
ACID 靠什么保证的呢?
ACID 中的原子性主要通过 Undo Log 来实现,持久性通过 Redo Log 来实现,隔离性由 MVCC 和锁机制来实现,一致性则由其他三大特性共同保证。
MVCC:读旧版本,写新版本
详细说说如何保证原子性?
事务对数据进行修改前,会记录一份快照到 Undo Log,如果事务中有任何一步执行失败,系统会读取 Undo Log 将所有操作回滚,恢复到事务开始前的状态,从而保证事务要么全部成功,要么全部失败。
详细说说如何保证持久性?
MySQL 的持久性主要由预写 Redo Log、双写机制、两阶段提交以及 Checkpoint 刷盘机制共同保证。
当事务提交时,MySQL 会先将事务的修改操作写入 Redo Log,并强制刷盘,然后再将内存中的数据页刷入磁盘。这样即使系统崩溃,重启后也能通过 Redo Log 重放恢复数据。
在将数据页写入到磁盘时,如果发生崩溃,可能会导致数据页不完整。InnoDB 的数据页大小为16KB,通常大于操作系统的 4KB页大小。
为了解决只写入部分的问题,MySQL 采用了双写机制,脏盘刷页时,先将数据页写入到一个双写缓冲区中,2M 的连续空间,然后再将其写入到磁盘的实际位置。
崩溃恢复时,如果发现数据页不完整,会从双写缓冲区中恢复副本,确保数据页的完整性。
在涉及主从复制时,MySQL 通过两阶段提交保证 Redo Log 和 Binlog 的一致性:第一阶段,写入 Redo Log 并标记为 prepare 状态;第二阶段,写入 Binlog 再提交 Redo Log 为 commit 状态。
崩溃恢复时,如果发现 Redo Log 是 prepare 但 Binlog 完整,则会提交事务;反之会回滚,避免主从不一致。
另外,由于 Redo Log 的容量有限,Checkpoint 机制会定期将内存中的脏页刷到磁盘,这样能减少崩溃恢复时需要处理的 Redo Log 数量
详细说说如何保证隔离性?
隔离性主要通过锁机制和 MVCC 来实现。
比如说一个事务正在修改某条数据时,MySQL 会通过临键锁来防止其他事务同时进行修改,避免数据冲突。
同时,临键锁可以防止幻读现象的发生。比如事务 A 查询 id > 10 的记录,那么临键锁不仅会锁住 id=10 的行,还会锁住 10 后面的“间隙”,防止其他事务插入 id=15 的数据。
MVCC 主要用来优化读操作,通过保存数据的历史版本,让读操作不需要加锁就能直接读取快照,提高读的并发性能。
如何保证一致性呢?
MySQL 的一致性并不是靠某一个机制单独保证的,而是原子性、隔离性和持久性协同作用的结果。
事务会不会自动提交?
是的,MySQL 默认开启了事务自动提交模式。
每条单独的 SQL 语句都会被视为一个独立的事务处理单元;SQL 语句执行成功后会自动执行 COMMIT;执行失败时会自动 ROLLBACK。
可通过 SELECT @@autocommit; 查看当前会话的自动提交状态。
如果需要执行多条 SQL 语句,可以将它们放在一个事务中,使用 START TRANSACTION 开启事务,执行完所有 SQL 语句后手动提交。
事务的隔离级别有哪些?
隔离级别定义了一个事务可能受其他事务影响的程度,MySQL 支持四种隔离级别,分别是:读未提交、读已提交、可重复读和串行化。
读未提交会出现脏读,读已提交会出现不可重复读,可重复读是 InnoDB 默认的隔离级别,可以避免脏读和不可重复读,但会出现幻读。不过通过 MVCC 和临键锁,能够防止大多数并发问题。
串行化最安全,但性能较差,通常不推荐使用。
详细说说读未提交?
事务可以读取其他未提交事务修改的数据。也就是说,如果未提交的事务一旦回滚,读取到的数据就会变成了“脏数据”,通常不会使用。
什么是读已提交?
读已提交避免了脏读,但可能会出现不可重复读,即同一事务内多次读取同一数据结果会不同,因为其他事务提交的修改,对当前事务是可见的。
什么是可重复读?
可重复读能确保同一事务内多次读取相同数据的结果一致,即使其他事务已提交修改。
什么是串行化?
串行化是最高的隔离级别,通过强制事务串行执行来解决“幻读”问题。
事务的隔离级别是如何实现的?
读未提交通过行锁共享锁确保一个事务在更新行数据但没有提交的情况下,其他事务不能更新该行数据,但不会阻止脏读,意味着事务2 可以在事务1 提交之前读取到事务1 修改的数据。
读已提交会在更新数据前加行级排他锁,不允许其他事务写入或者读取未提交的数据,也就意味着事务2 不能在事务 1 提交之前读取到事务1 修改的数据,从而解决脏读的问题。
另外,读已提交会在每次读取数据前都生成一个新的 ReadView,所以会出现不可重复读的问题。
可重复读只在第一次读操作时生成 ReadView,后续读操作都会使用这个 ReadView,从而避免不可重复读的问题。
另外,对于当前读操作,可重复读会通过临键锁来锁住当前行和前间隙,防止其他事务在这个范围内插入数据,从而避免幻读的问题。
串行化级别下,事务在读操作时,会先加表级共享锁;在写操作时,会先加表级排他锁。
请详细说说幻读呢?
幻读是指在同一个事务中,多次执行相同的范围查询,结果却不同。这种现象通常发生在其他事务在两次查询之间插入或删除了符合当前查询条件的数据。
如何避免幻读?
MySQL 在可重复读隔离级别下,通过 MVCC 和临键锁可以在一定程度上避免幻读。
比如说在查询时显示加锁,利用临键锁锁定查询范围,防止其他事务插入新的数据。
什么是当前读呢?
当前读是指读取记录的最新已提交版本,并且在读取时对记录加锁,确保其他并发事务不能修改当前记录。
比如 SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE,以及 UPDATE、DELETE,都属于当前读。
为什么 UPDATE 和 DELETE 也属于当前读?
因为更新、删除这些操作,本质上不仅是写操作,还需要在写之前读取数据,然后才能修改或删除。为了保证修改的是最新的数据,并防止并发冲突,InnoDB 必须读取最新版本的数据并加锁,因此 UPDATE 和 DELETE 也属于当前读。
什么是快照读呢?
快照读是 InnoDB 通过 MVCC 实现的一种非阻塞读方式。当事务执行 SELECT 查询时,InnoDB 并不会直接读当前最新的数据,而是根据事务开始时生成的 Read View 去判断每条记录的可见性,从而读取符合条件的历史版本。
MVCC 了解吗?
MVCC 指的是多版本并发控制,每次修改数据时,都会生成一个新的版本,而不是直接在原有数据上进行修改。并且每个事务只能看到在它开始之前已经提交的数据版本。
这样的话,读操作就不会阻塞写操作,写操作也不会阻塞读操作,从而避免加锁带来的性能损耗。
其底层实现主要依赖于 Undo Log 和 Read View。
每次修改数据前,先将记录拷贝到Undo Log,并且每条记录会包含三个隐藏列,DB_TRX_ID 用来记录修改该行的事务 ID,DB_ROLL_PTR 用来指向 Undo Log 中的前一个版本,DB_ROW_ID 用来唯一标识该行数据(仅无主键时生成)。
每次读取数据时,都会生成一个 ReadView,其中记录了当前活跃事务的 ID 集合、最小事务 ID、最大事务 ID 等信息,通过与 DB_TRX_ID 进行对比,判断当前事务是否可以看到该数据版本。
请详细说说什么是版本链?
版本链是指 InnoDB 中同一条记录的多个历史版本,通过 DB_ROLL_PTR 字段将它们像链表一样串起来,用来支持 MVCC 的快照读。
也就是说,当更新一行数据时,InnoDB 不会直接覆盖原有数据,而是创建一个新的数据版本,并更新 DB_TRX_ID 和 DB_ROLL_PTR,使它们指向前一个版本和相关的 undo 日志。
这样,老版本的数据就不会丢失,可以通过版本链找到。
由于 undo 日志会记录每一次的 update,并且新插入的行数据会记录上一条 undo 日志的指针,所以DB_ROLL_PTR 这个指针找到上一条记录,这样就形成了一个版本链。
请详细说说什么是ReadView?
ReadView 是 InnoDB 为每个事务创建的一份“可见性视图”,用于判断在执行快照读时,哪些数据版本是当前这个事务可以看到的,哪些不能看到。
当事务开始执行时,InnoDB 会为该事务创建一个 ReadView,这个 ReadView 会记录 4 个重要的信息:
creator_trx_id:创建该 ReadView 的事务 ID。
m_ids:所有活跃事务的 ID 列表,活跃事务是指那些已经开始但尚未提交的事务。
min_trx_id:所有活跃事务中最小的事务 ID。它是 m_ids 数组中最小的事务 ID。
max_trx_id :事务 ID 的最大值加一。换句话说,它是下一个将要生成的事务 ID。
ReadView 是如何判断记录的某个版本是否可见的?
会通过三个步骤来判断:
1、如果某个数据版本的 DB_TRX_ID 小于 min_trx_id,则该数据版本在生成 ReadView 之前就已经提交,因此对当前事务是可见的。
2、如果 DB_TRX_ID 大于 max_trx_id,则表示创建该数据版本的事务在生成 ReadView 之后开始,因此对当前事务不可见。
3.如果 DB_TRX_ID 在 min_trx_id 和 max_trx_id 之间,需要判断 DB_TRX_ID 是否在 m_ids 列表中:
不在,表示创建该数据版本的事务在生成 ReadView 之后已经提交,因此对当前事务也是可见的。
在,表示事务仍然活跃,或者在当前事务生成 ReadView 之后才开始,因此是不可见的。
可重复读和读已提交在 ReadView 上的区别是什么?
可重复读:在第一次读取数据时生成一个 ReadView,这个 ReadView 会一直保持到事务结束,这样可以保证在事务中多次读取同一行数据时,读取到的数据是一致的。
读已提交:每次读取数据前都生成一个 ReadView,这样就能保证每次读取的数据都是最新的。
