一起来分析MySQL事务工作流程原理
本篇文章给大家带来了关于mysql的相关知识,其中主要介绍了事务工作流程原理的相关问题,包括了事务的原子性是通过undo log来实现的、事务的持久性是通过redo log来实现的等等内容,下面一起来看一下,希望对大家有帮助。
推荐学习:mysql视频教程
- 事务的原子性是通过 undo log 来实现的
- 事务的持久性是通过 redo log 来实现的
- 事务的隔离性是通过 (读写锁+MVCC)来实现的
- 而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!
1、redo log 实现持久性
问题1: 为什么需要redo log?
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中 。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
问题2:redo log如何保证事务的持久性?
Redo log可以简单分为以下两个部分:
一是内存中重做日志缓冲 (redo log buffer),是易失的,在内存中
二是重做日志文件 (redo log file),是持久的,保存在磁盘中
这里再细说下写入Redo的时机:
在数据页修改完成之后,在脏页刷出磁盘之前,写入redo日志。注意的是先修改数据,后写日志
redo日志比数据页先写回磁盘
聚集索引、二级索引、undo页面的修改,均需要记录Redo日志
在 MySQL中,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就采用了日志(redo log)来提升更新效率。
当事务提交时,先将 redo log buffer 写入到 redo log file 进行持久化,待事务的commit操作完成时才算完成。这种做法也被称为 Write-Ahead Log(预先日志持久化),在持久化一个数据页之前,先将内存中相应的日志页持久化。
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(redo log buffer)里面,并更新内存(buffer pool),这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候(如系统空闲时),将这个操作记录更新到磁盘里面(刷脏页)。
在一个事务中可以修改多个页,Write-Ahead Log 可以保证单个数据页的一致性,但是无法保证事务的持久性,Force-log-at-commit 要求当一个事务提交时,其产生所有的mini-transaction 日志必须刷新到磁盘中,若日志刷新完成后,在缓冲池中的页刷新到持久化存储设备前数据库发生了宕机,那么数据库重启时,可以通过日志来保证数据的完整性。
问题3:重写日志的流程?
上图表示了重做日志的写入流程,每个mini-transaction对应每一条DML操作,比如一条update语句,其由一个mini-transaction来保证,对数据修改后,产生redo1,首先将其写入mini-transaction私有的Buffer中,update语句结束后,将redo1从私有Buffer拷贝到公有的Log Buffer中。当整个外部事务提交时,将redo log buffer再刷入到redo log file中。( redo log是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写)
问题4:数据写入后的最终落盘,是从 redo log 更新过来的还是从 buffer pool 更新过来的呢?
实际上,redo log 并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在由 redo log 更新过去数据最终落盘的情况。
① 数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程与 redo log 毫无关系。
② 在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态
问题5:redo log buffer 是什么?是先修改内存,还是先写 redo log 文件?
在一个事务的更新过程中,日志是要写多次的。比如下面这个事务:
Copybegin;
INSERT INTO T1 VALUES ('1', '1');
INSERT INTO T2 VALUES ('1', '1');
commit;
这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没 commit 的时候就直接写到 redo log 文件里。
因此就需要 redo log buffer 出场了,它就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个 insert 的时候,数据的内存被修改了,redo log buffer 也写入了日志。
但是,真正把日志写到 redo log 文件,是在执行 commit 语句的时候做的。
redo log buffer 本质上只是一个 byte 数组,但是为了维护这个 buffer 还需要设置很多其他的 meta data,这些 meta data 全部封装在 log_t 结构体中。
问题6:redo log顺序写入磁盘?
redo log以顺序的方式写入文件,当全部文件写满的时候则回到第一个文件相应的起始位置进行覆盖写,每次提交事务之后,都先将相关的操作日志写入redo日志文件中,并且都追加到文件末尾,这是一个顺序I/O
图中展示了一组 4 个文件的 redo log 日志,checkpoint 是当前要擦除的位置,擦除记录前需要先把对应的数据落盘(更新内存页,等待刷脏页)。write pos 到 checkpoint 之间的部分可以用来记录新的操作,如果 write pos 和 checkpoint 相遇,说明 redolog 已满,这个时候数据库停止进行数据库更新语句的执行,转而进行 redo log 日志同步到磁盘中。checkpoint 到 write pos 之间的部分等待落盘(先更新内存页,然后等待刷脏页)。
有了 redo log 日志,那么在数据库进行异常重启的时候,可以根据 redo log 日志进行恢复,也就达到了 crash-safe。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失
2、bin log
MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)
为什么会有两份日志呢?
因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
这两种日志有以下三点不同。
① redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
② redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
③ redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
有了对这两个日志的概念性理解后,再来看执行器和 InnoDB 引擎在执行这个 update 语句时的内部流程。
① 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
② 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
③ 引擎将这行新数据更新到内存(InnoDB Buffer Pool)中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
④ 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
⑤ 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成
其中将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是两阶段提交(2PC)
问题1: 两阶段提交原理?
MySQL 使用两阶段提交主要解决 binlog 和 redo log 的数据一致性的问题。
两阶段提交原理描述:
① redo log 写盘,InnoDB 事务进入 prepare 状态。
② 如果前面 prepare 成功,binlog 写盘,那么再继续将事务日志持久化到 binlog,如果持久化成功,那么 InnoDB 事务则进入 commit 状态 。
redo log 和 binlog 有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:
① 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
② 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
binlog无记录,回滚事务
binlog有记录,提交事务
问题2:为什么必须有“两阶段提交”呢?
如果不使用两阶段提交,假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
**先写 redo log 后写 binlog。**假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。
但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
**先写 binlog 后写 redo log。**如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
3、undo log 实现原子性
undo log有两个作用:提供回滚和多版本控制(MVCC)
在数据修改的时候,不仅记录了redo,还记录了相对应的undo,undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
undo日志,只将数据库逻辑地恢复到原来的样子,在回滚的时候,它实际上是做的相反的工作,比如一条INSERT ,对应一条 DELETE,对于每个UPDATE,对应一条相反的 UPDATE,将修改前的行放回去。undo日志用于事务的回滚操作进而保障了事务的原子性。
实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。 InnoDB 实现回滚,靠的是undo log :当事务对数据库进行修改时,InnoDB 会生成对应的undo log 如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
在InnoDB存储引擎中,undo log分为:
insert undo log
update undo log
insert undo log是指在insert 操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
而update undo log记录的是对delete 和update操作产生的undo log,该undo log可能需要提供MVCC机制,因此不能再事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
补充:purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除",只是做了个标记,真正的删除工作需要后台purge线程去完成。
innodb中通过B+树作为索引的数据结构,并且主键所在的索引为ClusterIndex(聚簇索引), ClusterIndex中的叶子节点中保存了对应的数据内容。一个表只能有一个主键,所以只能有一个聚簇索引,如果表没有定义主键,则选择第一个非NULL唯一索引作为聚簇索引,如果还没有则生成一个隐藏id列作为聚簇索引。
除了Cluster Index外的索引是Secondary Index(辅助索引)。辅助索引中的叶子节点保存的是聚簇索引的叶子节点的值。
InnoDB行记录中除了刚才提到的rowid外,还有trx_id和db_roll_ptr, trx_id表示最近修改的事务的id,db_roll_ptr指向undo segment中的undo log。
新增一个事务时事务id会增加,trx_id能够表示事务开始的先后顺序。
Undo log分为Insert和Update两种,delete可以看做是一种特殊的update,即在记录上修改删除标记。
update undo log记录了数据之前的数据信息,通过这些信息可以还原到之前版本的状态。
当进行插入操作时,生成的Insert undo log在事务提交后即可删除,因为其他事务不需要这个undo log。
进行删除修改操作时,会生成对应的undo log,并将当前数据记录中的db_roll_ptr指向新的undo log
4、MVCC实现隔离性
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。
InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号。
主要实现思想是通过数据多版本来做到读写分离。从而实现不加锁读进而做到读写并行。
MVCC在mysql中的实现依赖的是undo log与read view
- undo log :undo log 中记录某行数据的多个版本的数据。
- read view :用来判断当前版本数据的可见性
InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。
MySQL的MVCC快照并不是每一个事务进来就copy一份数据库信息,而是基于数据表每行信息后面保存的系统版本号去实现的。如下图所示,一行信息会有多个版本并存,每个事务可能读取到的版本不一样
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的row trx_id 。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它 。
数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。 就是一个记录被多个事务连续更新后的状态。
图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢?
实际上,图 2 中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。
5、MySQL 锁技术
当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。 读写锁 解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,
这两种锁被称为:
共享锁(shared lock),又叫做"读锁" 读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。
排他锁(exclusive lock),又叫做"写锁" 写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
总结: 通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行 事务的隔离性就是根据读写锁来实现的!!!
推荐学习:mysql视频教程
以上就是一起来分析MySQL事务工作流程原理的详细内容,更多请关注其它相关文章!