一、架构
Mysql分为服务层和引擎层
服务层(Service):连接器、查询缓存、分析器(词法分析、语法分析)、查询优化器、执行器;所有的内置函数,存储过程,视图等都在这里实现。
引擎层:InnoDB,MyISAM,Memory;负责数据的读取和存储;其架构模式是插件式的。
连接器:
- 这个阶段会等待TCP监听链接,读取用户名、密码,然后读取用户的权限,后期的操作均在这个权限上约束;
- 如果后期没有操作,连接会处于空闲状态,空闲过长会被断开,由参数wait_timeout决定;默认8小时;
- 连接过程比较复杂,所以建议采用连接池;
- 连接会占用资源,包括后期的查询缓存等,所以需要定期清理;
缓存器:(8.0版本后被删除)
- 拿到语句后会先查询缓存是否有该语句,有即返回;
- 基本是以key-value的形式存储的;
- 少用缓存器,弊大于利,除非你的数据很少更新,因为更新了会使得缓存失效;
分析器:
词法分析和语法分析;注意这里会包含包括表字是否存在,列名是否存在等判断;
优化器:
对于语句
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
先查t1的数据,和先查t2的数据,逻辑结果是一样的,但是效率可能差别很大,例如t1有索引而t2没有的情况下,优化器就是要做这个选择;
执行器:
先判断一下你对这个表 T 有没有执行查询的权限,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。
mysql> select * from T where ID=10;
- 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给
整个服务层架构如下图所示:
二、Redo Log && Bin Log
例子:孔乙己,酒柜老板记账两种方式,一是每一次都翻开账本,二是每次先记在黑板上,打烊的时候记录到账本;在忙的时候第二种方式销量最高,所以Mysql也是有类似机制,叫WSL,write ahead log
Redo Log: 例子中对应的黑板就是redo log;redo log 属于引擎层日志,黑板(redo log)是有固定大小的,如下图所示,可以配置一组为4个文件,每个文件的大小是1GB,那么总可以记录4GB操作,写到末位又从头写,如果写完了,那么就不得不停下来先持久化了;redo log其实是用两个指针维护的,一个记录当前写位置,一个记录当前擦除位置;
- redo log主要作用是提供写速度的;
- redo log还可以保证数据库拥有crash-safe能力的(主要通过双写保证),只要追加日志写完,系统崩溃也不会导致数据丢失;
Bin Log:而另外Service层也有日志,叫binlog (归档日志);binlog的作用主要是做数据恢复,以及数据库主从节点数据备份的。
这两个区别是:
- redo log是innoDB持有的,binlog是MySql Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑;
- redo log是循环写的,空间固定会用完,binlog是追加写入,并不会覆盖以前日志;
- redo log是用来提供写速度和保证crash-safe能力,而bin log作用恢复备份数据的作用多;
两阶段提交:我们做的误删备份恢复一般用Binlog,另外因为redo log和binlog是在不同的地方写,所以可能存在数据不一致的情况,而解决这个不一致的方法是 “两阶段提交”;先写redo log,记为prepare状态,然后写binlog,最后再提交事物,让redo log成为commit状态;
这个两阶段提交是否可靠,在于程序在哪里crash,如果在写完binlog之后crash,那么下次程序启动必然先检查最早的事务,发现一个binlog对应的redolog还在prepare,就会把redolog状态改为commit,所以有了规则和状态的结合。下面看一下,这个的流程图;
事务机制:
事务特征:A 原子性,C 一致性,I 隔离性,D 持久性
隔离性:在一个数据库中有多个事务的时候,就有可能会出现脏读、不可重复读、幻读问题,为了解决这些问题,就有了隔离级别的概念;
隔离级别:
在谈隔离级别之前,你隔离的越严实,效率就会越低。很多时候,我们需要在两者之中寻找一个平衡点。
- Read Uncommited 读未提交
- 一个事务还没提交时,它做的变更就能被别的事物看到;
- Read Commited 读提交;
- 一个事务提交之后,才能被其他事务看到
- Repeated Read 可重复读;
- 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据时一致的
- 当然在可重复读的隔离级别下,未提交变更对其他事务也是不可见的
- Serializable 串行化;
我们来看个例子,图中假设数据表只有一列,一个值得为1,在不同隔离级别下,查询到的V1、V2、V3的值分别时多少
如果是Read UnCommited,那么V1 = 2,V2 =2, V3 = 2;
如果是Read Commited,那么V1 = 1,V2 =2, V3 = 2;
如果是Repeated Read,那么V1 = 1,V2 =1, V3 = 2;
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个试图是在事务启动时创建的,整个事务存在期间都会用这个视图。在读提交的隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的;“读未提交”则不存在视图的概念,直接返回最新值;“串行化”就直接加锁避免并发访问。
多版本并发控制(MVVC):
那么视图是怎么实现的呢,实际上,每条记录在更新的时候都会同时记录一条回滚操作;记录上的最新值,通过回滚操作,都可以得到前一个甚至几个变化前状态的值,有了这些回滚操作,我们就可以在每个时刻都记录当前的read-view,于是一个数据便可以对不同时间点有多个不同版本,每个版本可以通过执行回滚操作回到对应的read-view的值。所以要实现“Repeated Read”的每个事务的视图,只需记录事务创建当前时刻的所有read-view即可
假设一个值从1被按顺序改成了2、3、4在回滚日志里面就会有类似下面的记录
系统的当前值是4,但在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中所看到的,视图A,B,C里面这一个记录的值分别是1、2、4;一条记录在一个系统有多个版本,就是数据库的多版本并发控制(MVVC);
当什么时候会删除那些不必要的回滚日志呢,那就是当没有任何事务再需要这些回滚日志的时候。也就是当前readView版本,没有比这个版本更早的readView在被事务使用;(所以这里我们不建议使用长事务,这样会导致MySql资源一直不释放);
当前读和快照读:
当前读是读真正的记录,快照读是读快照的记录(例如MVVC中的视图),一般如select for update会强制用当前读,而且会给记录加gap锁;
幻读和不可重复读:
幻读的定义:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
一个例子:
事务A需要找到没有小明的班级的所有人员名单。
1.查找班里所有记录是否有小明
2.不存在小明则查询班级里所有人的名单。
事务B 把小明放进这个班级里去
当两个事务执行时,事务A查询了班级里没有小明,这时候B事务把小明插入到班级中,A事务执行查询全班的操作 事务A查询到的结果是:返回了全班人员名单,包含有小明。违反了事务A的要求。
RR隔离级别下的幻读
那么RR隔离级别下是否存在幻读?因为mysql的RR通过MVVC视图实现是避免了幻读的,但如果语句触发了当前读,那么会感觉依旧出现了幻读现象;要解决这个问题,那么就要通过MVCC+间隙锁来彻底解决幻读问题
结论:
- mysql的RR隔离级别下,可以解决幻读问题,但如果事务中触发当前读,也有可能幻读
- 不可重复读和幻读就说的都是读-读,只不过不可重复读是因为别的事务update而影响了两次select结果,幻读是因为别的事务insert而影响了两次select结果;
索引:
索引就是目录,方便你快速找到记录,实现索引的方式有很多种,常见的有有序数组,哈希表,二叉树;下面我列出各自的优缺点
- 有序数组(二分法查找)适合范围查询,等值查询性能也不错,但比较麻烦的是,如果要插入的时候,就需要移动数据,代价比较大,所以适合静态存储,不会怎么变的数据;
- 哈希表支持常量级别的等值查找,但不支持有序查找;
- 搜索二叉树,高度比较高,不太适合磁盘存储,而且顺序查找也不太适合;
- 跳表,适合二分查找,也适合顺序查找,但需要额外的索引空间,并且索引空间的维护不态适合存储在磁盘,所以比较适合redis等内存数据库;
- N叉树,这个就比较适合了,适配顺序查找,那么B+树就是一个很好的选择;
不管是哈希还是有序数组,或者N叉树,它们都是不断迭代、不断优化的产物或者解决方案。数据库发展到今天,跳表、LSM树等数据结构也被用于引擎设计中;InnoDB就是B+树;
B+树特点,可以多很好支持索引的N叉存储,而且让叶子结点链接起来,使得可以支持范围查询;
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k)
)
engine=InnoDB;
根据以上表格,索引和存储如下所示:(什么时候你不记得B+树的结构,那么请记住跳表就好了)
可以看出,这是两种不同的索引,带记录R的的称之为主键索引,也称聚簇cu索引,其他的均是非聚簇索引;
聚簇索引:按照数据记录的主键id顺序存储记录,并以id为索引;
二级索引:主键索引外的索引都是二级索引,索引的data是主键,一次查询至少走两次索引,第二次是走主键索引,我们称之为回表操作;
联合索引:多个列组合的索引,一个索引只建一个树所以联合索引也是一棵树,索引节点存储列组合字符串,使用时根据最左匹配原则;所以走不走联合索引基本看第一个字段存不存在where中;
覆盖索引:通常有些查询需要的字段在联合索引中就有了,不需要回表,这些统称为覆盖索引,是特殊的联合索引;
索引维护
在上面B+树中,如果插入的是700,那很好办,再新加个节点就好了,但如果是400,那就麻烦了,需要移动后面的数据,空出位置,但数据页以及满了,根据B+树的算法,需要申请一个新的数据页。然后挪动部分数据过去这个过程称之为分页。这种情况下,性能自然会受影响,而且会降低数据页的利用率(因为都只存了一半,但也有可以合并两个稀疏数据页的过程);
自增主键
自增主键的特点,正好符合了B+树每次插入都是追加记录的场景,不需要挪动其他记录,也不会触发叶子的分裂,而业务逻辑做主键往往无法做到顺序插入,成本比较高,所以我们建议用自增主键,那么什么情况下不需要自增主键呢?就是如果你的数据是只有一个id索引,而不需要其他索引的情况下,用业务主键会好一点,因为不需要回表查询了
优化点:
- 尽量使用自增主键,因为这样可以减少插入的页分裂,因为基本是插入树的最后一页,自增主键基本不需要过多页移动操作;而业务逻辑字段做主键写数据成本较高。
- 自增主键长度较小,使得页可以装更多的索引,而业务主键数据长度较大,索引也大,占用空间;
- 可以建立一个联合索引,该索引可以减少一个二级索引(原理是最左前缀),具体见mysql 45讲,索引篇下;
内存管理:
BufferPool
Page
FreeList
FlushList
PageHash
LRU
小知识:
为了快速查找记录,Innodb在每个Page的行记录后定义了一个称之为目录槽(directory slot)的结构,用于快速定位页面中行记录。
directory slot中记录了行记录在页面内偏移。需要注意的是,目录槽和行记录并非一一对应的关系,每个dir slot最多可管理8个物理行记录。定位时首先是二分查找dir slot,再通过其中记录的偏移找到行记录,进而再进行顺序查找定位特定行。
参考资料:
https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html
https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_phantom
https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_non_repeatable_read