事务特性、隔离级别、MVCC
事务特性(ACID)
- A(Atomicity):原子性。事务是不可分割的最小操作单元,要么全部成功,要么全部失败。(通过undo log实现)
- C(consistency):一致性。事务完成时,必须使所有的数据都保持一致状态。(通过undo log实现)
undolog:
记录的是逻辑日志,当事务回滚时,通过逆操作来恢复数据。逻辑日志记录的是数据库事务的逻辑操作。它描述了事务对数据库所做的更改,例如INSERT、UPDATE或DELETE操作。它的作用包括两个:提供回滚与MVCC
- D(Durability):持久性。事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。(通过redo log实现)
redo log
记录的是数据页的物理变化,可以理解为物理日志,一旦服务宕机,可以用来同步数据。物理日志记录的是数据库存储空间的物理修改。它通常记录数据页(或数据块)的物理地址和这些页上的具体更改。
- I(Isolation): 隔离性。数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下进行。(MVCC多版本并发控制实现)
MVCC
因为MVCC被问得多,也更加复杂,因此放在第三节MVCC单独讲。
事务并发问题
那么,如何解决并发事务的问题呢?答案是对事务进行隔离解决。隔离级别越高,数据越安全,但是性能越低。
事务的隔离级别定义了事务之间的隔离程度,即一个事务内部的操作对其他并发事务的可见性程度。SQL标准定义了四个事务隔离级别,从低到高依次是:
- 读未提交(Read Uncommitted):一个事务可以读取另一个未提交事务的数据。
- 读已提交(Read Committed):一个事务只能读取已经提交事务所做的修改。这是大多数数据库系统的默认隔离级别(但MySQL的默认隔离级别是REPEATABLE READ)。
- 可重复读(Repeatable Read):对同一字段的多次读取结果都是一致的。在这个级别下,InnoDB使用MVCC来实现。
- 串行化(Serializable):最高的隔离级别,所有的事务依次逐个执行,这样事务之间就不可能产生干扰。
事务的隔离级别可以通过MVCC来实现。下一节,我们详细介绍MVCC。
MVCC(多版本并发控制)
MVCC是一种并发控制的方法,它允许非锁定读操作,即读操作不会阻塞写操作,写操作也不会阻塞读操作。MVCC是通过保存数据在某个时间点的快照来实现的,这样读操作就可以读取该时间点之前的数据版本,而写操作可以修改当前的数据版本。
在InnoDB存储引擎中,MVCC是通过Undo日志来实现的。当一个事务修改数据时,它不会直接修改原始数据,而是生成一个新的数据版本,并将旧的数据版本保存在Undo日志中。这样,其他事务在读取数据时,就可以通过Undo日志找到它所需的数据版本。
简而言之,MVCC
- 定义是:MVCC是一种并发控制的方法,它通过对数据库中的每个数据行保存多个版本的信息,来允许多个事务同时读取和修改数据,而不会相互干扰。
- 目标是:提高数据库的并发性能,减少数据冲突和不一致性的发生。
MVCC的实现原理
MVCC的实现包括以下部分:
-
版本链(Versioning Chain)
- 在InnoDB引擎表中,聚簇索引记录的每行数据都包含两个隐藏列:
trx_id
和roll_pointer
。 trx_id
:用于存储每次对某条聚簇索引记录进行修改的事务ID。roll_pointer
:是一个指针,指向这条聚簇索引记录的上一个版本在Undo日志中的位置。当记录被修改时,旧版本会被写入Undo日志,并通过roll_pointer
与新版本相连,形成版本链。
- 在InnoDB引擎表中,聚簇索引记录的每行数据都包含两个隐藏列:
-
Undo日志(Undo Log)
- Undo日志用于存储数据的旧版本,以便在需要时进行回滚操作或支持MVCC的非锁定读。
- 当记录被修改时,旧的数据版本会被写入Undo日志,并且新版本的记录通过
roll_pointer
指向旧版本。
-
Read-View
- Read-View是一个数据结构,它包含了当前系统中活跃的事务列表(即已经开始但尚未提交的事务)。
- 当一个事务需要读取数据时,它会使用Read-View来判断哪些版本的数据对当前事务是可见的。
- Read-View通过比较事务ID和活跃事务列表来决定数据版本的可见性。
那么它具体是怎么决定版本的可见性的呢?
首先我们要了解,Read-View的组成如下:
creator_trx_id
:创建这个Read-View的事务ID。trx_ids
:在生成Read-View时当前系统中活跃的读写事务的事务ID列表。up_limit_id
:活跃的事务中最小的事务ID。low_limit_id
:表示生成Read-View时系统中应该分配给下一个事务的ID值。注意,它并不是trx_ids
中的最大值,而是系统中事务ID的下一个可能值。
接下来,我们就可以进行版本可见性的判断了
-
当一个事务需要读取数据时,它会检查被访问数据记录的
trx_id
(即创建该数据版本的事务ID)。 -
如果被访问版本的
trx_id
小于up_limit_id
(即trx_ids
中的最小值),说明生成该版本的事务在Read-View生成前就已经提交了,因此该版本对当前事务可见。 -
如果被访问版本的
trx_id
大于low_limit_id
,说明生成该版本的事务在Read-View生成后才生成,因此该版本对当前事务不可见。 -
如果被访问版本的
trx_id
在up_limit_id
和low_limit_id
之间(包括这两个值),则需要进一步检查:如果
trx_id
在trx_ids
列表中,说明在生成Read-View时,生成该版本的事务仍然是活跃的,因此该版本不可见。如果
trx_id
不在trx_ids
列表中,说明在生成Read-View时,生成该版本的事务已经提交,因此该版本可见。
-
事务ID和版本比较
- 当一个事务尝试读取一条记录时,它会检查该记录的
trx_id
与Read-View中的事务ID列表。 - 如果
trx_id
小于列表中最小的事务ID,则这个数据版本对当前事务是可见的(因为修改它的事务在当前事务开始前已经提交了)。 - 如果
trx_id
在列表中的最小和最大事务ID之间,则需要进一步检查该事务ID是否在活跃事务列表中。如果在,说明修改该记录的事务尚未提交,因此这个数据版本对当前事务不可见;如果不在,说明修改该记录的事务已经提交,因此这个数据版本对当前事务是可见的。 - 如果
trx_id
大于列表中最大的事务ID,则这个数据版本是在当前事务开始后产生的,因此不可见。
- 当一个事务尝试读取一条记录时,它会检查该记录的
-
非锁定读
- MVCC允许非锁定读操作,即读操作不会阻塞写操作,写操作也不会阻塞读操作。
- 这是因为读操作只是简单地读取数据的一个历史版本(通过版本链和Undo日志),而不是直接读取最新版本的数据。
-
写操作
- 当一个事务执行写操作时,它不会直接覆盖原始数据,而是创建一个新的数据版本,并将旧版本保留在Undo日志中。
- 新版本的数据通过
trx_id
和roll_pointer
与旧版本相连,形成版本链。
这么说其实是非常晦涩难懂的,接下来我会举个例子,帮助我们理解
假设我们有一个简单的银行转账系统,其中有一个账户表(Account),包含账户ID(ID)、账户余额(Balance)等字段。现在有两个事务T1和T2几乎同时发生,T1要查询账户余额,而T2要修改账户余额(即转账)。
初始状态
账户表(Account)中有一条记录,ID为1,Balance为1000。
事务T1(查询账户余额)
- 开始事务:T1启动,并获得一个唯一的事务ID,例如T1_ID = 100。
- 执行查询:T1执行
SELECT Balance FROM Account WHERE ID = 1;
此时,由于没有其他事务修改过该记录,T1读取到的是当前最新的版本,即Balance = 1000。 - 快照读:T1的这次读操作是快照读,因为它读取的是数据的一个历史版本(在这个例子中,因为是最新版本,所以看起来和当前版本一样)。但重要的是,T1读取的这个版本是在T1事务开始时的一个数据快照。
事务T2(修改账户余额)
- 开始事务:T2启动,并获得一个唯一的事务ID,例如T2_ID = 101。
- 执行修改:T2执行
UPDATE Account SET Balance = Balance - 200 WHERE ID = 1;
这将账户余额从1000减少到800。 - 版本管理
- 数据库系统不会直接修改原始数据行的Balance字段,而是为该行创建一个新的版本。新版本包含新的Balance值(800)和T2的事务ID(T2_ID = 101)。
- 原始版本的数据(Balance = 1000)被写入Undo日志,并通过回滚指针(roll_pointer)与新版本相连,形成版本链。
事务T1(继续执行)
- 再次查询:假设T1在T2提交之后再次执行相同的查询操作。
- 版本可见性判断
- 数据库系统检查T1的Read-View(包含当前活跃事务列表和最小、最大事务ID)。
- 由于T2已经提交(假设T2_ID = 101小于Read-View中的最大事务ID,且不在活跃事务列表中),因此T2所做的修改对T1是可见的。
- 但是,因为T1是基于其开始时的数据快照进行读取的,所以T1仍然看到Balance = 1000(这是T1开始时的数据版本)。
- 结果:T1的第二次查询结果仍然是Balance = 1000,尽管T2已经修改了该值。
写在最后
这一篇内容比较多,也比较难懂,大家下去可以多消化一下。根据博主的经验,特别是MVCC这一块被问得很多,如果说不清,说得没有逻辑,在面试中肯定是会扣分。所以我们要形成自己的理解链条,能够保证在面试的高压中能够用清晰的条理去回答问题,而不是死记硬背。
----本文图片与文字部分参考黑马程序员