实际上,我们研究事务的原理,就是研究
MySQL
的
InnoDB
引擎是如何保证事务的四大特性的。
而对于这四大特性,实际上分为两个部分。 其中的
原子性、一致性、持久性
,实际上是由
InnoDB
中的两份日志来保证的,一份是
redo log
日志
,一份是
undo log
日志
。 而
隔离性
是通过
数据库的锁
, 加上
MVCC
来保证的。
一、
redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:
重做日志缓冲(
redo log buffer
)
以及
重做日志文件(
redo log file)
,
前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中
,
用于在刷新脏页到磁盘,
发生错误时
,
进行数据恢复使用。
如果没有
redolog
,可能会存在什么问题的?
我们知道,在InnoDB
引擎中的内存结构中,主要的内存区域就是
缓冲池
,在缓冲池中缓存了很多的
数据页
。 当我们在一个事务中,执行多个增删改的操作时,InnoDB
引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为
脏页
。 而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。
那么,如何解决上述的问题呢?
有了redo log
之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在
redo log buffer中。在事务提交时,会将
redo log buffer
中的数据刷新到
redo log
磁盘文件中。过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log
进行数据恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘或者涉及到的数据已经落盘,此 时redolog
就没有作用了,就可以删除了,所以存在的两个
redolog
文件是循环写的。
那为什么每一次提交事务,要刷新
redo log
到磁盘中呢,而不是直接将
buffer pool
中的脏页刷新到磁盘呢 ?
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log
在 往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这种先写日志的方式,称之为 WAL(
Write-Ahead Logging
)。
二、undo log
回滚日志,用于记录数据被修改前的信息
,
作用包含两个
:
提供回滚
(
保证事务的原子性
)
和
MVCC
(多版本并发控制
)
。
undo log
和
redo log
记录物理日志不一样,它是
逻辑日志
。可以认为当
delete
一条记录时,
undo log中会记录一条对应的
insert
记录,反之亦然,当
update
一条记录时,它记录一条对应相反的update记录。当执行
rollback
时,就可以从
undo log
中的逻辑记录读取到相应的内容并进行回滚。
Undo log
销毁
:
undo log
在事务执行时产生,事务提交时,并不会立即删除
undo log
,因为这些日志可能还用于MVCC
。
Undo log
存储
:
undo log
采用段的方式进行管理和记录,存放在
rollback segment回滚段中,内部包含1024
个
undo log segment
。
三、MVCC
1. 基本概念
(1)当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select … lock in share mode(
共享锁
)
,
select … for update、
update
、
insert
、
delete(
排他锁
)
都是一种当前读。
(2)快照读
简单的
select
(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据, 不加锁,是非阻塞读。
-
Read Committed
:每次
select
,都生成一个快照读。
-
Repeatable Read
:开启事务后第一个
select
语句才是快照读的地方。
-
Serializable
:快照读会退化为当前读。
(3)MVCC
全称
Multi-Version Concurrency Control
,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL
实现
MVCC
提供了一个非阻塞读功能。
MVCC
的具体实现,还需要依赖于数据库记录中的
三个隐式字段
、
undo log
日志
、
readView
。
2. 实现原理
(1)隐藏字段
实际上除了自己创建的字段以外,InnoDB
还会自动的给我们添加三个隐藏字段及其含义分别是:
隐藏字段 |
含义 |
DB_TRX_ID |
最近修改事务 ID ,记录插入这条记录或最后一次修改该记录的事务 ID 。 |
DB_ROLL_PTR |
回滚指针,指向这条记录的上一个版本,用于配合 undo log ,指向上一个版本。 |
DB_ROW_ID |
隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 |
而上述的前两个字段是肯定会添加的, 是否添加最后一个字段
DB_ROW_ID
,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段
(2)undo log
回滚日志
,在
insert
、
update
、
delete
的时候产生的便于数据回滚的日志。
当
insert
的时候,产生的
undo log
日志只在回滚时需要,在事务提交后,可被立即删除。
而
update
、
delete
的时候,产生的
undo log
日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。
① undo log版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
(3)ReadView
ReadView
(读视图)是 快照读
SQL
执行时
MVCC
提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id
ReadView
中包含了四个
核心字段
:
字段 |
含义 |
m_ids |
当前活跃的事务 ID 集合 |
min_trx_id |
最小活跃事务 ID |
max_trx_id |
预分配事务 ID ,当前最大事务 ID+1 (因为事务 ID 是自增的) |
creator_trx_id |
ReadView 创建者的事务 ID |
而在
readview
中就规定了版本链数据的
访问规则
:
trx_id
代表当前
undolog
版本链对应事务
ID
。
条件 |
是否可以访问 |
说明 |
trx_id ==creator_trx_id |
可以访问该版本 |
成立,说明数据是当前这个事务更改的。 |
trx_id < min_trx_id |
可以访问该版本 |
成立,说明数据已经提交了。 |
trx_id > max_trx_id |
不可以访问该版本 |
成立,说明该事务是在ReadView生成后才开启。 |
min_trx_id <= trx_id<= max_trx_id |
如果 trx_id 不在 m_ids 中,是可以访问该版本的 |
成立,说明数据已经提交。 |
不同的隔离级别,生成
ReadView
的时机不同:
-
READ COMMITTED
:在事务中每一次执行快照读时生成
ReadView
。
-
REPEATABLE READ
:仅在事务中第一次执行快照读时生成
ReadView
,后续复用该
ReadView
所以呢,
MVCC
的实现原理就是通过
InnoDB
表的隐藏字段、
UndoLog
版本链、
ReadView
来实现的。而MVCC +
锁,则实现了事务的
隔离性
。 而
一致性
则是由
redolog
与
undolog
保证。