MySQL之事务的实现原理

在上一章节,详细阐述了MySQL的事务的特性,以及存在的并发问题。并且,也详细阐述了MySQL内置的四种不同的隔离级别,分别都解决了对应的并发问题。那么,同学们有没有思考一个问题:MySQL是怎么实现的这些不同的隔离级别?例如,在可重复读的隔离级别下,B事务明明已经提交了事务,表示对数据的修改已经写入了磁盘(至少可以保证能够写入磁盘),但是A事务是通过什么方式仍然获取到的原来的数据呢?
要搞懂这些原理,我们必须首先了解MySQL的底层存储,MVCC机制以及锁的相关特性。下面我们一一进行讲解。

MySQL 体系结构

首先,我们看一下来自MySQL官方的一个MySQL的架构图
在这里插入图片描述
下面我们就结合上图,对MySQL的体系架构做一个整体的讲解:

  1. Connector(连接器):指的就是客户端连接工具,也指不同的语言通过SQL与数据库的交互,这些客户端或者语言(jar包)就是连接器。
  2. Connection Manager(连接管理器):管理缓冲数据库连接、线程处理等需要缓存的需求。

如Java中的数据库连接池DataSource保存的就是这里的连接。因为数据库连接的创建和销毁是一个很重的操作,因此,客户端也会将这些连接进行缓存。
在这里还会进行例如用户账号、密码以及库(表)的访问权限的验证等。

  1. SQL Interface(SQL接口):接收用户执行的SQL语句,并返回查询的结果。
  2. Parser(查询解析器):将一条SQL语句解析成MySQL认识的语法格式:即抽象语法树。因此select * from t_user where name=a and age=bselect * from t_user where age=b and name=a在本质上是一样的,因为会被解析器解析。
  3. Optimizer(查询优化器):在查询之前,SQL语句会使用查询优化器对查询进行优化(生产查询路径树,并选举一条最优的查询路径)。因此我们通过explain查看SQL的执行计划时,有时发现SQL没有走索引,可能就是优化器认为不走索引更快,导致的。
  4. Caches & Buffers(缓存&缓冲):主要包含表缓冲、权限缓冲等。
  5. Pluggable Storage Engines(插件式存储引擎):存储引擎是MySQL中具体与文件系统打交道的子系统。MySQL的存储引擎是插件式的,目前存储引擎有很多,且优势各不相同。MySQL默认InnoDB。

现在最常用的存储引擎也是InnoDB,常用于OLTP(联机事务处理)
而对于OLAP(联机分析处理),常用的是例如Hive等大数据平台。

  1. Files & Logs(磁盘物理文件):包含MySQL的各个引擎的数据、索引的文件,以及undo log, redo log(InnoDB所有),bin log(Server 层所有),error log, query log, slow query log(慢查询日志)等各种日志文件。
  2. File System(文件系统):对存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。

结合上面介绍的MySQL的各个核心组件,下面分析一下一条SQL语句在MySQL的执行流程是怎样的:
在这里插入图片描述

  1. MySQL客户端向MySQL server发起请求,获取到一个连接请求。
  2. 在connector(连接管理器)创建连接,分配线程,并验证用户名、密码和库表权限等。
  3. 判断是否打开了query_cache(查询缓存),如果打开了,则进入第四步,否则进入第五步
  4. 根据当前SQL语句,获取到hash值,通过hash查询cache&buffer中是否有在过期时间范围内的数据。有则直接返回。否则进入第五步。
  5. SQL连接组件接收SQL语句,并将SQL语句分解成MySQL能够识别的数据结构(抽象语法树)。
  6. Optimizer(查询优化器)根据SQL语句的特征,生产查询路径树,并选举一条最优的查询路径。
  7. 调用存储引擎接口,打开表,执行查询。同时检查存储引擎中是否有对应的缓存记录,如果有,则直接返回。如果没有就继续往下执行。
  8. 到数据文件中寻找数据(如果有索引,会先通过索引查找)
  9. 当查询到所需要的数据之后,先写入存储引擎缓存中。如果打开了query_cache,也会同时写进去。
  10. 返回数据给客户端,同时关闭表,线程和连接。

InnoDB存储结构

首先我们知道,MySQL的InnoDB存储引擎在底层是通过B+Tree的数据结构来存储底层数据的(想必大家也都知道B+Tree,不做过多介绍),另外,还需要知道的是,MySQL是一页(Page)为单位存储数据的。一个Page默认为16kb。可以通过show global variables like "%innodb_page_size%";查询当前页大小
在这里插入图片描述

  1. MySQL整体数据结构:
    在这里插入图片描述

通过上图可以比较清晰看到,MySQL中的数据是以页(Page)为单位进行存储的,存储结构是B+Tree。该结构具有如下特征:

  1. 一个Page中可以存储多个节点,Page与Page之间通过指针分别指向左孩子和右孩子节点
  2. 同一层的节点之间通过左右兄弟指针互相关联,可以很好的解决MySQL中的范围查询。
  3. 叶子节点存储实际的数据元素(图上没有展示);
  • 聚簇索引的数据元素即为数据集(即MySQL中的一条实际的记录)
  • 非聚簇索引的数据元素存储的仅仅只是聚餐索引的值(注意,这里是值,而不是地址),查询时,如果需要的话,还需要再次通过聚簇索引回表查询,查询出真正的数据节点。
  1. Page的内部结构:
    在这里插入图片描述

如上图所示,是一个Page内的大致的数据结构。供分为如下几大模块:

  1. 页头:记录页面的控制信息,共占56字节,包括页的左右兄弟页面指针,页面空间使用情况等。例如上图所示的兄弟节点的指针就是在页头中维护的,页与页之间通过左右兄弟指针形成了一个双向链表。
  2. 虚记录:
  • 最小虚记录:比页内最小主键还小的记录。查询时,如果主键比最小虚记录还小,则说明不再这个页内,根据左兄弟指针向左查找。
  • 最大虚记录:比页内最大主键还大的记录。查询时,如果主键比最大虚记录还大,则说明不再这个页内,根据右兄弟指针向右查找。
  1. 记录堆:即已经被使用的Page中的那部分内存空间
  • 有效记录:实际存储的有用的信息,可能被MySQL检索到的数据记录。
  • 已删除记录:MySQL中删除记录时,为了提高性能,减少IO,仅仅只是给该记录做了一个标记,标记该记录已删除,而不会真正的删除该记录,当新记录需要插入时,会优先使用这些已经被标记删除的内存空间。
  1. 自有空间链表(上图没有展示出来):即所有的被标注为已删除的记录,会通过指针进行串联,形成一个链表,即自由空间链表。当新的记录需要插入时,优先遍历自由空间链表:
  • 如果找到足够大的连续内存后,用新纪录覆盖已删除的记录。
  • 如果没有找到,则在未分配空间中进行分配内存的分配。
  1. 未分配空间:页面内未使用的存储空间。
  2. Slot区:Slot存储了一个槽位,一个槽位对应MySQL聚簇索引的一批记录。通过Slot,可以使得一个页面内的所有的记录,形成一个类似HashMap的结构(也有说是跳表结构的,个人感觉和HashMap比较像),主要用于做Page内数据的索引。
  3. 页尾: 页面最后部分。占8个字节,主要存储页面的校验信息.

总结:一个记录的检索过程分为如下两步:

  1. 检索记录所在的Page:Page的检索是通过索引构建的B+Tree进行检索的。
  2. 在Page中检索记录:Page内的记录的检索是通过Slot槽位构建的HashMap(跳表的结构)进行检索的

最后,这一块确实很难,也比较抽象。本人能力有限,安利一个比较好的文章,供参考
http://mysql.taobao.org/monthly/2016/02/01/

MVCC机制

前面介绍了关于MySQL整体架构及存储的知识,但是还没有进入本章的主题:即MySQL通过什么方式实现了不同的隔离级别?
上一章中讲解MySQL的事务特性时提到,MySQL的InnoDB支持四种事务隔离级别,其中最常用的就是RR(可重复读)。
在RR级别下,B事务提交,A事务还是只能读取到B事务提交之前的记录,那么很容易就能够想到,MySQL底层一定维护了一条记录的多个版本。确实是这样的,MySQL通过MVCC机制,实现了事务的多版本控制。

MySQL默认事务隔离级别RR就是通过MVCC+行级锁共同实现的。

  1. MVCC是什么:多版本控制(Multiversion Concurrency Control): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
  2. MVCC通过什么方式实现了多版本并发控制?在MySQL中,主要通过以下三种机制协同工作,实现了MVCC机制:
    2.1 隐藏字段: 在插入一条记录时,除了我们自己指定的数据字段外,MySQL会默认额外添加如下三个隐藏字段:
  • db_row_id(6字节):随着新行插入而单调递增的占6字节的行ID(并不是顺序递增)。其主要作用是:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。

如果先没有指定主键,在表运行一段时间后,创建主键,那么会MySQL会进行聚簇索引的重建,会将新创建的主键或唯一索引作为聚簇索引,构建表。

  • db_trx_id(6字节):事务id,表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除
  • db_roll_ptr(7字节):回滚指针,指向该版本记录的前一个版本。实际指向的是当前记录行的上一版本的undo log信息

2.2. Read View: 主要是用来做可见性分析的, 里面保存了“对本事务不可见的其他活跃事务”。

  1. 当一个事务被创建时,会创建该事务的Read View(读视图),主要保存如下三个信息:
  • 当前活跃事件列表(即当前还未提交的事务id)
  • 列表中最小事务ID:小于当前事务ID的数据都是可见的,因为这些事务肯定已经commit或者rollback。
  • 列表中最大事务ID:大于当前事务ID的数据都是不可见的,需要通过回滚指针找到下一个版本。
  1. 一个事务通过Read View做可见性分析时,遵循如下规则:
    在这里插入图片描述
  • 小于活跃事件列表中最小事务id:该事务已提交,可见
  • 大于活跃事件列表中最大事务id:该事务id对应的事务在当前事务启动之后才开启,不可见.
  • 在事务id处于(min_trx_id, max_trx_id)时,就需要校验是否在当前活跃事件列表中:因为事务的执行时间是不确定的,在min_trx_id后面启动的事务,可能已经提交,这时也是可见的:
  1. 不在活跃事件列表中:说明该事务在当前事务启动时,已经提交。可见
  2. 在活跃事件列表中:说明该事务在当前事务启动时,还没有提交,处于活跃状态,哪怕在当前事务启动之后提交,也是不可见的。不可见

2.3. Undo log:回滚日志。Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
2.3.1 Undo log 分类:在InnoDB里,undo log分为如下两类:

  1. insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
  2. update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

2.3.2 Undo log的作用:Undo log在MySQL中主要有一下两个用处:
一条记录在经过多个事务修改后,在数据库中,大致的一个数据存储结构如下图所示:
在这里插入图片描述

  1. 保证事务原子性:即可以通过undo log做回滚操作。
  2. 实现数据多版本:即实现MVCC机制。结合上面的Read View来看,一个事务会在上述的链表中,最终找到一个在当前事务中可见的记录版本。
  1. 当前读与快照读:讲到MVCC的版本控制,就不得不提到这两个核心概念:

在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。当用户在这个事务中要读取某个记录行的时候,innodb会将该记录行的DB_TRX_ID与该Read View中的一些变量进行比较,判断是否满足可见性条件。

  1. 快照读(snapshot read):即通过回滚指针构成的链表,读取到当前事务对应的事务id可见的数据版本。普通的 select 语句(不包括 select … lock in share mode, select … for update)就是快照读。
  2. 当前读(current read) :即直接读取聚餐索引中存储的最新数据,不会通过记录中的回滚指针向下寻找。select ... lock in share modeselect ... for updateinsertupdatedelete 语句都是属于当前读。

注意:只靠 MVCC 实现RR隔离级别,可以保证可重复读,还能防止部分幻读,但并不是完全防止。

因此,InnoDB在实现RR隔离级别时,不仅使用了MVCC,还会对“当前读语句”读取的记录行加记录锁(record lock)和间隙锁(gap lock),禁止其他事务在间隙间插入记录行,来防止幻读。也就是前文说的"行级锁+MVCC"。

  1. RR和RC的Read View产生区别:
  1. 在innodb中的Repeatable Read级别, 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务之后都是使用的这个快照,不会重新创建,直到事务结束。
    2 在innodb中的Read Committed级别, 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。

前面介绍完了MySQL如何通过MVCC实现事务隔离级别中的RC和RR,其中也提到,在RR级别中,如果仅仅通过MVCC的话,只能防止快照读的幻读发生,而不能防止当前读产生的幻读问题。因此MySQL通过MVCC+行锁实现的RR事务隔离级别。那么下面我们就简单聊一聊MySQL中是锁机制:
安利一个写的不错的文章,避免重复造轮子
https://tonydong.blog.csdn.net/article/details/103324323

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值