MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
读操作(SELECT):
当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下:
- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。
- 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。
- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。
写操作(INSERT、UPDATE、DELETE):
当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下:
- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。
- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。
- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。
事务提交和回滚:
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。
事务提交和回滚:
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。
版本的回收:
为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。
MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。
事务 ID 与 Undo 日志
在 InnoDB 中,每个事务在启动时都会被分配一个唯一的事务 ID。这个事务 ID 在 MVCC 的实现中起着关键作用。当对数据进行修改时,InnoDB 会将旧版本的数据记录到 Undo 日志中。
Undo 日志是一种用于存储数据修改前版本的机制。例如,当一个事务执行 UPDATE 操作时,它会先将原数据写入 Undo 日志,这个操作伴随着一个新的数据版本的创建。对于每个数据行,InnoDB 存储引擎通过一个隐藏的列来记录指向 Undo 日志中旧版本数据的指针。这样,在执行快照读时,可以根据事务开始的时间以及这些指针找到合适的旧版本数据。
一致性视图(Read View)的构建
当一个事务开始执行读操作时,InnoDB 会为该事务创建一个一致性视图。这个一致性视图决定了事务能够看到哪些版本的数据。
一致性视图中包含了当前系统中活跃事务的列表(尚未提交的事务)。通过比较数据行的版本信息(包含创建版本号和删除版本号)和一致性视图中的活跃事务列表,可以确定某个数据版本对于当前读事务是否可见。如果数据行的创建版本号小于当前事务的 ID,并且删除版本号大于当前事务的 ID 或者未定义(表示数据未被删除),那么这个数据版本对于当前事务是可见的。这种机制确保了事务能够基于一个稳定的、符合隔离级别的数据视图进行读取操作。
MVCC 与不同隔离级别
InnoDB 的 MVCC 在不同的隔离级别下有不同的表现:
-
读已提交(Read Committed)隔离级别
在这个隔离级别下,每个 SELECT 语句都会重新创建一个一致性视图。这意味着一个事务在执行多次相同的 SELECT 语句时,可能会看到不同的数据版本。例如,在事务执行期间,如果另一个并发事务提交了对某些数据的修改,后续的 SELECT 操作可能会看到这些新提交的数据,因为新的一致性视图会反映最新的已提交事务的情况。 -
可重复读(Repeatable Read)隔离级别
在可重复读隔离级别下,事务在启动时创建的一致性视图在整个事务期间保持不变。这保证了事务在多次执行相同的 SELECT 语句时,看到的数据始终是一致的。即使其他并发事务对数据进行了修改并提交,当前事务也不会看到这些变化,直到它自己重新开始一个新的事务。这种特性使得可重复读隔离级别在需要稳定数据视图的场景中非常有用,例如在一些对数据一致性要求较高的业务逻辑中。
版本回收的时机与策略
版本回收是 MVCC 实现中重要的一环,它涉及到数据库空间的有效利用。InnoDB 存储引擎采用了多种策略来确定何时回收版本。
一种常见的策略是基于 Undo 日志的大小和使用情况。当 Undo 日志占用的空间达到一定阈值时,系统会触发版本回收操作。在回收过程中,会根据数据行的版本信息以及当前活跃事务的情况来判断哪些旧版本数据可以被安全地删除。例如,如果某个数据行的旧版本所关联的事务都已经提交并且不再有其他事务需要读取该旧版本,那么这个旧版本数据就可以被回收。此外,InnoDB 还会考虑系统的负载情况,在系统负载较低的时间段更积极地进行版本回收,以减少对正常业务操作的影响。
通过对这些底层细节的理解,我们可以更好地把握 InnoDB 存储引擎中 MVCC 的工作原理,从而在数据库设计、优化以及故障排查等方面做出更合理的决策,提高数据库系统的整体性能和可靠性。同时,MVCC 的这种实现方式也为处理高并发数据库操作提供了一种优雅且高效的解决方案,平衡了数据一致性和并发性能之间的关系。
一致性非锁定读
对于 一致性非锁定读(Consistent Nonlocking Reads)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见
在 InnoDB
存储引擎中,多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行 DELETE
或 UPDATE
操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB
存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)
在 Repeatable Read
和 Read Committed
两个隔离级别下,如果是执行普通的 select
语句(不包括 select ... lock in share mode
,select ... for update
)则会使用 一致性非锁定读(MVCC)
。并且在 Repeatable Read
下 MVCC
实现了可重复读和防止部分幻读
锁定读
如果执行的是下列语句,就是 锁定读(Locking Reads)
select ... lock in share mode
select ... for update
-
insert
、update
、delete
操作
在锁定读下,读取的是数据的最新版本,这种读也被称为 当前读(current read)
。锁定读会对读取到的记录加锁:
-
select ... lock in share mode
:对记录加S
锁,其它事务也可以加S
锁,如果加x
锁则会被阻塞 -
select ... for update
、insert
、update
、delete
:对记录加X
锁,且其它事务不能加任何锁
在一致性非锁定读下,即使读取的记录已被其它事务加上 X
锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在 Repeatable Read
下 MVCC
防止了部分幻读,这边的 “部分” 是指在 一致性非锁定读
情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是!如果是 当前读
,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, InnoDB
在实现Repeatable Read
时,如果执行的是当前读,则会对读取的记录使用 Next-key Lock
,来防止其它事务在间隙间插入数据
Read View
- Read View 是事务在执行查询操作时生成的一个视图,用于确定哪些版本的数据对该事务可见。Read View 中包含了以下几个重要信息:
-
m_ids
:表示当前系统中活跃的事务 ID 列表。 -
min_trx_id
:表示当前系统中活跃的最小事务 ID。 -
max_trx_id
:表示下一个将要分配的事务 ID。 -
creator_trx_id
:表示创建该 Read View 的事务 ID。
-
- 当事务执行查询操作时,InnoDB 会根据 Read View 来判断数据的可见性。具体规则如下:
- 如果数据的
DB_TRX_ID
小于min_trx_id
,说明该数据在当前事务开始之前就已经提交,因此对当前事务可见。 - 如果数据的
DB_TRX_ID
大于max_trx_id
,说明该数据是在当前事务开始之后才生成的,因此对当前事务不可见。 - 如果数据的
DB_TRX_ID
在min_trx_id
和max_trx_id
之间,并且不在m_ids
列表中,说明该数据已经提交,对当前事务可见。 - 如果数据的
DB_TRX_ID
在m_ids
列表中,说明该数据是由当前活跃的事务生成的,对当前事务不可见
- 如果数据的
InnoDB 存储引擎对 MVCC 的实现主要包括以下几个关键部分:
-
隐藏字段:InnoDB 表中的每行数据包含
DB_TRX_ID
和DB_ROLL_PTR
等隐藏字段,DB_TRX_ID
记录插入或更新该行数据的事务 ID,DB_ROLL_PTR
指向该行数据的旧版本,形成版本链。 - Undo 日志:用于记录数据修改前的状态,事务修改数据时先将原始数据写入 Undo 日志,其记录按事务提交顺序组织成链,可用于回滚事务和查询数据旧版本。
-
Read View:事务执行查询时生成的视图,包含
m_ids
、min_trx_id
、max_trx_id
和creator_trx_id
等信息,用于确定数据的可见性,依据特定规则判断数据对当前事务是否可见。 - 版本链与数据查询:查询时从当前行数据出发,沿版本链查找符合 Read View 可见性规则的版本,找到则返回对应数据,找不到则返回空结果。
- 事务提交与清理:事务提交时标记其 Undo 日志记录为已提交并释放资源,同时按策略清理不再需要的旧版本数据,以节省存储空间。通过这些机制,InnoDB 实现了 MVCC,提高了数据库的并发性能。