标签:实现 计算 间隔 一句话 sse 字符 如何 效果 冗余
[TOC]
MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。
大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。
两者的对比:
READ COMMITTED
和 REPEATABLE READ
两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制《MySQL高性能》上面有一句话这样写到:
不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。
本质:客户端进程向服务器进程发送一段文本(MySQL语句),服务器进程处理后再向客户端进程发送一段文本(处理结果)。
组件的简单介绍:
简单来说 MySQL 主要分为 Server 层和存储引擎层:
连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。
主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。
查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。
连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 sql 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。
MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。
所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。
MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。
MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步:
第一步,词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。
第二步,语法分析,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。
完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。
优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。
可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。
当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
说了以上这么多,那么究竟一条 sql 语句是如何执行的呢?其实我们的 sql 可以分为两种,一种是查询,一种是更新(增加,更新,删除)。我们先分析下查询语句,语句如下:
select * from tb_student A where A.age=‘18‘ and A.name=‘ 张三 ‘;
结合上面的说明,我们分析下这个语句的执行流程:
先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。
通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id=‘1‘。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。
接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案:
a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。
那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
以上就是一条查询 sql 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?sql 语句如下:
update tb_student A set A.age=‘19‘ where A.name=‘ 张三 ‘;
我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 binlog(归档日志) ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 redo log(重做日志),我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:
为什么要用两个日志模块,用一个日志模块不行吗?
这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档(存档)。
并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做?
如果采用 redo log 两阶段提交的方式就不一样了,写完 binglog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:
这样就解决了数据一致性的问题。
参考:《MySQL实战45讲》
页是MySQL
中磁盘和内存交互的基本单位,也是MySQL
是管理存储空间的基本单位。
InnoDB
采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
指定和修改行格式的语法如下:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
InnoDB
目前定义了4种行格式
COMPACT行格式
具体组成如图:
Redundant行格式
具体组成如图:
Dynamic和Compressed行格式
这两种行格式类似于COMPACT行格式
,只不过在处理行溢出数据时有点儿分歧,它们不会在记录的真实数据处存储字符串的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
另外,Compressed
行格式会采用压缩算法对页面进行压缩。
16KB
,当记录中的数据太多,当前页放不下的时候,会把多余的数据存储到其他页中,这种现象称为行溢出
。所以说,如果我们写 select * from user where indexname = ‘xxx‘
这样没有进行任何优化的sql语句,默认会这样做:
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。
记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:
SELECT * FROM page_demo WHERE c1 = 3;
最笨的办法:从 Infimum
记录(最小记录)开始,沿着链表一直往后找,总有一天会找到(或者找不到[摊手]),在找的时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值时,你就可以停止查找了,因为该节点后边的节点的主键值依次递增。
为了加快搜索速度,引入了页目录,页目录的创建如下:
n_owned
属性表示该记录拥有多少条记录,也就是该组内共有几条记录。页
的尾部的地方,这个地方就是所谓的Page Directory
,也就是页目录
(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽
(英文名:Slot
),所以这个页面目录就是由槽
组成的。对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。(下面图有 16 条记录)
比方说找主键值为 6 的记录:
(0+4)/2=2
,所以查看槽2
对应记录的主键值为8
,又因为8 > 6
,所以设置high=2
,low
保持不变。(0+2)/2=1
,所以查看槽1
对应的主键值为4
,又因为4 < 6
,所以设置low=1
,high
保持不变。high - low
的值为1,所以确定主键值为6
的记录在槽2
对应的组中。此刻我们需要找到槽2
中主键值最小的那条记录,然后沿着单向链表遍历槽2
中的记录。但是我们前边又说过,每个槽对应的记录都是该组中主键值最大的记录,这里槽2
对应的记录是主键值为8
的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿到槽1
对应的记录(主键值为4
),该条记录的下一条记录就是槽2
中主键值最小的记录,该记录的主键值为5
。所以我们可以从这条主键值为5
的记录出发,遍历槽2
中的各条记录,直到找到主键值为6
的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。总结:
next_record
属性遍历该槽所在的组中的各个记录。数据页
。File Header
,表示页的一些通用信息,占固定的38字节。Page Header
,表示数据页专有的一些信息,占固定的56个字节。Infimum + Supremum
,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26
个字节。User Records
:真实存储我们插入的记录的部分,大小不固定。Free Space
:页中尚未使用的部分,大小不确定。Page Directory
:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。File Trailer
:用于检验页是否完整的部分,占用固定的8个字节。next_record
属性,从而使页中的所有记录串联成一个单链表
。InnoDB
会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽
,存放在Page Directory
中,所以在一个页中根据主键查找记录是非常快的,分为两步:
File Header
部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表
。LSN
值,如果首部和尾部的校验和和LSN
值校验不成功的话,就说明同步过程出现了问题。InnoDB
存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存
起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO
的开销了。
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表
(或者说空闲链表)。
我们怎么知道该页在不在Buffer Pool
中呢?难不成需要依次遍历Buffer Pool
中各个缓存页么?一个Buffer Pool
中的缓存页这么多都遍历完岂不是要累死?
我们其实是根据表空间号 + 页号
来定位一个页的,也就相当于表空间号 + 页号
是一个key
,缓存页
就是对应的value
,怎么通过一个key
来快速找着一个value
呢?哈哈,那肯定是哈希表喽~
如果我们修改了Buffer Pool
中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页
(英文名:dirty page
)。如果每次产生脏页就立即同步到磁盘上的话会严重影响程序性能
凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表
。
挺多的,详情看掘金的 mysql 小册 https://juejin.im/book/5bffcbc9f265da614b11b731/section/5c238f0851882521eb44c51f
磁盘太慢,用内存作为缓存很有必要。
Buffer Pool
本质上是InnoDB
向操作系统申请的一段连续的内存空间,可以通过innodb_buffer_pool_size
来调整它的大小。
Buffer Pool
向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后,Buffer Pool
剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为碎片
。
InnoDB
使用了许多链表
来管理Buffer Pool
。
free链表
中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到Buffer Pool
时,会从free链表
中寻找空闲的缓存页。
为了快速定位某个页是否被加载到Buffer Pool
,使用表空间号 + 页号
作为key
,缓存页作为value
,建立哈希表。
在Buffer Pool
中被修改的页称为脏页
,脏页并不是立即刷新,而是被加入到flush链表
中,待之后的某个时刻同步到磁盘上。
LRU链表
分为young
和old
两个区域,可以通过innodb_old_blocks_pct
来调节old
区域所占的比例。首次从磁盘上加载到Buffer Pool
的页会被放到old
区域的头部,在innodb_old_blocks_time
间隔时间内访问该页不会把它移动到young
区域头部。在Buffer Pool
没有可用的空闲缓存页时,会首先淘汰掉old
区域的一些页。
我们可以通过指定innodb_buffer_pool_instances
来控制Buffer Pool
实例的个数,每个Buffer Pool
实例中都有各自独立的链表,互不干扰。
自MySQL 5.7.5
版本之后,可以在服务器运行过程中调整Buffer Pool
大小。每个Buffer Pool
实例由若干个chunk
组成,每个chunk
的大小可以在服务器启动时通过启动参数调整。
可以用下边的命令查看Buffer Pool
的状态信息:
SHOW ENGINE INNODB STATUS\G
假设目前表中的记录比较少,所有的记录都可以被存放到一个页中,在查找记录的时候可以根据搜索条件的不同分为两种情况:
以主键为搜索条件
这个查找过程我们已经很熟悉了,可以在页目录
中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
以其他列作为搜索条件
对非主键列的查找的过程可就不这么幸运了,因为在数据页中并没有对非主键列建立所谓的页目录
,所以我们无法通过二分法快速定位相应的槽
。这种情况下只能从最小记录
开始依次遍历单链表中的每条记录,然后对比每条记录是不是符合搜索条件。很显然,这种查找的效率是非常低的。
大部分情况下我们表中存放的记录都是非常多的,需要好多的数据页来存储这些记录。在很多页中查找记录的话可以分为两个步骤:
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中根据我们刚刚唠叨过的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是超级耗时的,如果一个表有一亿条记录,使用这种方式去查找记录那要等到猴年马月才能等到查找结果。所以祖国和人民都在期盼一种能高效完成搜索的方法,索引
同志就要亮相登台了。
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
Query OK, 0 rows affected (0.03 sec)
假设一个页只能放 3 条记录,当我们要插入第四条记录时,就要进行页分裂了(页分裂之后也要满足下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值)
由于数据页的编号可能并不是连续的,所以在向index_demo
表中插入许多条记录后,可能是这样的效果:
因为这些16KB
的页在物理存储上可能并不挨着,所以如果想从这么多页中根据主键值快速定位某些记录所在的页,我们需要给它们做个目录,每个页对应一个目录项,每个目录项包括下边两个部分:
key
来表示。page_no
表示。以页28
为例,它对应目录项2
,这个目录项中包含着该页的页号28
以及该页中用户记录的最小主键值5
。我们只需要把几个目录项在物理存储器上连续存储,比如把他们放到一个数组里,就可以实现根据主键值快速查找某条记录的功能了。比方说我们想找主键值为20
的记录,具体查找过程分两步:
20
的记录在目录项3
中(因为 12 < 20 < 209
),它对应的页是页9
。页9
中定位具体的记录。这个简易的索引方案存在的缺点:
InnoDB
是使用页来作为管理存储空间的基本单位,也就是最多能保证16KB
的连续存储空间,而随着表中记录数量的增多,需要非常大的连续的存储空间才能把所有的目录项都放下,这对记录数量非常多的表是不现实的。页28
中的记录都删除了,页28
也就没有存在的必要了,那意味着目录项2
也就没有存在的必要了,这就需要把目录项2
后的目录项都向前移动一下,这种牵一发而动全身的设计不是什么好主意~复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,我们把这些用来表示目录项的记录称为目录项记录。用 record_type
区分用户记录和目录项记录记录头信息里的 record_type
属性
0
:普通的用户记录1
:目录项记录2
:最小记录3
:最大记录目录项记录
的record_type
值是1,而普通用户记录的record_type
值是0。目录项记录
只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB
自己添加的隐藏列。min_rec_mask
的属性么,只有在存储目录项记录
的页中的主键值最小的目录项记录
的min_rec_mask
值为1
,其他别的记录的min_rec_mask
值都是0
。很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 “目录” 就可以很快地定位到对应的页上了!(二分查找,时间复杂度近似为O(logn))
其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
B+
树的叶子节点存储的是完整的用户记录。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
我们把具有这两种特性的B+
树称为聚簇索引
,所有完整的用户记录都存放在这个聚簇索引
的叶子节点处。这种聚簇索引
并不需要我们在MySQL
语句中显式的使用INDEX
语句去创建(后边会介绍索引相关的语句),InnoDB
存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB
存储引擎中,聚簇索引
就是数据的存储方式(所有的用户记录都存储在了叶子节点
),也就是所谓的索引即数据,数据即索引。
聚簇索引只适用于主键查询,当想要使用非主键列作为查询条件时就需要重新构建 B+ 树了(二级索引)
c2
列的大小进行记录和页的排序,这包括三个方面的含义:
c2
列的大小顺序排成一个单向链表。c2
列大小顺序排成一个双向链表。c2
列大小顺序排成一个双向链表。B+
树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键
这两个列的值。主键+页号
的搭配,而变成了c2列+主键+页号
的搭配。以查找c2
列的值为4
的记录为例,查找过程如下:
确定目录项记录
页
根据根页面
,也就是页44
,可以快速定位到目录项记录
所在的页为页42
(因为2 < 4 < 9
)。
通过目录项记录
页确定用户记录真实所在的页。
在页42
中可以快速定位到实际存储用户记录的页,但是由于c2
列并没有唯一性约束,所以c2
列值为4
的记录可能分布在多个数据页中,又因为2 < 4 ≤ 4
,所以确定实际存储用户记录的页在页34
和页35
中。
在真实存储用户记录的页中定位到具体的记录。
到页34
和页35
中定位到具体的记录。
但是这个B+
树的叶子节点中的记录只存储了c2
和c1
(也就是主键
)两个列,所以我们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录(回表)。
也就是根据c2
列的值查询一条完整的用户记录需要使用到2
棵B+
树!!!这种B+
树也被称为二级索引
(英文名secondary index
),或者辅助索引
。
让B+
树按照c2
和c3
列的大小进行排序,这个包含两层含义:
c2
列进行排序。c2
列相同的情况下,采用c3
列进行排序目录项记录
都由c2
、c3
、页号
这三个部分组成,各条记录先按照c2
列的值进行排序,如果记录的c2
列相同,则按照c3
列的值进行排序。B+
树叶子节点处的用户记录由c2
、c3
和主键c1
列组成。以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
联合索引
只会建立如上图一样的1棵B+
树。c2
和c3
列的大小为排序规则建立2棵B+
树。InnoDB
中索引即数据,也就是聚簇索引的那棵B+
树的叶子节点中已经把所有完整的用户记录都包含了,而MyISAM
的索引方案虽然也使用树形结构,但是却将索引和数据分开存储:
将表中的记录按照记录的插入顺序单独存储在一个文件中,称之为数据文件
。这个文件并不划分为若干个数据页,有多少记录就往这个文件中塞多少记录就成了。我们可以通过行号而快速访问到一条记录。
使用MyISAM
存储引擎的表会把索引信息另外存储到一个称为索引文件
的另一个文件中。MyISAM
会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值 + 行号
的组合。也就是先通过索引找到对应的行号,再通过行号去找对应的记录!
这一点和InnoDB
是完全不相同的,在InnoDB
存储引擎中,我们只需要根据主键值对聚簇索引
进行一次查找就能找到对应的记录,而在MyISAM
中却需要进行一次回表
操作,意味着MyISAM
中建立的索引相当于全部都是二级索引
!
如果有需要的话,我们也可以对其它的列分别建立索引或者建立联合索引,原理和InnoDB
中的索引差不多,不过在叶子节点处存储的是相应的列 + 行号
。这些索引也全部都是二级索引
。
InnoDB
和MyISAM
会自动为主键或者声明为UNIQUE
的列去自动建立B+
树索引
每建立一个索引都会建立一棵B+
树,每插入一条记录都要维护各个记录、数据页的排序关系,这是很费性能和存储空间的。
1.添加PRIMARY KEY(主键索引)
ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
2.添加UNIQUE(唯一索引)
ALTER TABLE `table_name` ADD UNIQUE ( `column` )
3.添加INDEX(普通索引)
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
4.添加FULLTEXT(全文索引)
ALTER TABLE `table_name` ADD FULLTEXT ( `column`)
5.添加多列索引
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间,一棵很大的B+树由许多数据页组成,那可是很大的一片存储空间呢。
每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收啥的操作来维护好节点和记录的排序。、
CREATE TABLE person_info(
id INT NOT NULL auto_increment,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name, birthday, phone_number)
);
SELECT * FROM person_info WHERE name = ‘Ashburn‘ AND birthday = ‘1990-09-27‘;
SELECT * FROM person_info WHERE birthday = ‘1990-09-27‘;
SELECT * FROM person_info WHERE name = ‘Ashburn‘ AND phone_number = ‘15123983239‘;
SELECT * FROM person_info WHERE name LIKE ‘As%‘;
SELECT * FROM person_info WHERE name LIKE ‘%As%‘;
SELECT * FROM person_info WHERE name > ‘Asa‘ AND name < ‘Barlow‘;
SELECT * FROM person_info WHERE name > ‘Asa‘ AND name < ‘Barlow‘ AND birthday > ‘1980-01-01‘;
name > ‘Asa‘ AND name < ‘Barlow‘
来对name进行范围,查找的结果可能有多条name值不同的记录,birthday > ‘1980-01-01‘
条件继续过滤。SELECT * FROM person_info WHERE name = ‘Ashburn‘ AND birthday > ‘1980-01-01‘ AND birthday < ‘2000-12-31‘ AND phone_number > ‘15100000000‘;
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
这个查询的结果集需要先按照name
值排序,如果记录的name
值相同,则需要按照birthday
来排序,如果birthday
的值相同,则需要按照phone_number
排序。大家可以回过头去看我们建立的idx_name_birthday_phone_number
索引的示意图,因为这个B+
树索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表
操作取出该索引中不包含的列就好了。简单吧?是的,索引就是这么牛逼。
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number
这个查询语句相当于做了3次分组操作:
和使用B+树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组,吧啦吧啦的~
idx_name_birthday_phone_number
索引为例,看下边这个查询:
SELECT * FROM person_info WHERE name > ‘Asa‘ AND name < ‘Barlow‘;
在使用idx_name_birthday_phone_number
索引进行查询时大致可以分为这两个步骤:
idx_name_birthday_phone_number
对应的B+
树中取出name
值在Asa
~Barlow
之间的用户记录。idx_name_birthday_phone_number
对应的B+
树用户记录中只包含name
、birthday
、phone_number
、id
这4个字段,而查询列表是*
,意味着要查询表中所有字段,也就是还要包括country
字段。这时需要把从上一步中获取到的每一条记录的id
字段都到聚簇索引对应的B+
树中找到完整的用户记录,也就是我们通常所说的回表
,然后把完整的用户记录返回给查询用户。由于索引idx_name_birthday_phone_number
对应的B+
树中的记录首先会按照name
列的值进行排序,所以值在Asa
~Barlow
之间的记录在磁盘中的存储是相连的,集中分布在一个或几个数据页中,我们可以很快的把这些连着的记录从磁盘中读出来。
根据第1步中获取到的记录的id
字段的值可能并不相连,而在聚簇索引中记录是根据id
(也就是主键)的顺序排列的,所以根据这些并不连续的id
值到聚簇索引中访问完整的用户记录可能分布在不同的数据页中,这样读取完整的用户记录可能要访问更多的数据页。
所以这个使用索引idx_name_birthday_phone_number
的查询有这么两个特点:
B+
树索引,一个二级索引,一个聚簇索引。顺序I/O
,访问聚簇索引使用随机I/O
。顺序I/O比随机I/O的性能高很多
需要回表的记录越多,使用二级索引的性能就越低
查询优化器会事先对表中的记录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表
的方式。
可以限制查询获取较少的记录数让优化器更倾向于选择使用二级索引 + 回表
的方式进行查询,因为回表的记录越少,性能提升就越高
SELECT * FROM person_info WHERE name > ‘Asa‘ AND name < ‘Barlow‘ LIMIT 10;
为了彻底告别回表
操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列,比如这样:
SELECT name, birthday, phone_number FROM person_info WHERE name > ‘Asa‘ AND name < ‘Barlow‘
因为我们只查询name
, birthday
, phone_number
这三个索引列的值,所以在通过idx_name_birthday_phone_number
索引得到结果后就不必到聚簇索引
中再查找记录的剩余列,也就是country
列的值了,这样就省去了回表
操作带来的性能损耗。
只为出现在WHERE子句中的列、连接子句中的连接列,或者出现在ORDER BY或GROUP BY子句中的列创建索引。而出现在查询列表中的列就没必要建立索引了:
在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT~
只对字符串的前几个字符进行索引——也就是说在二级索引的记录中只保留字符串前几个字符。
CREATE TABLE person_info(
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
name(10)
就表示在建立的B+
树索引中只保留记录的前10
个字符的编码,这种只索引字符串值的前缀的策略是非常鼓励的,尤其是在字符串类型能存储的字符比较多的时候。
如果使用了索引列前缀,比方说前边只把name
列的前10个字符放到了二级索引中,下边这个查询可能就有点儿尴尬了:
SELECT * FROM person_info ORDER BY name LIMIT 10;
因为二级索引中不包含完整的name
列信息,所以无法对前十个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用索引排序,只好乖乖的用文件排序喽。
如果索引列在比较表达式中不是以单独列的形式出现,而是以某个表达式,或者函数调用形式出现的话,是用不到索引的。
WHERE my_col * 2 < 4
不行WHERE my_col < 4/2
行如果数据页满了,会导致页分裂和记录位移,也就意味着性能损耗。
**建议:**让主键具有AUTO_INCREMENT
,让存储引擎自己为表生成主键,而不是我们手动插入
CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
KEY idx_name (name(10))
);
我们知道,通过idx_name_birthday_phone_number
索引就可以对name
列进行快速搜索,再创建一个专门针对name
列的索引就算是一个冗余
索引,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。
B+
树索引在空间和时间上都有代价,所以没事儿别瞎建索引。B+
树索引适用于下边这些情况:
聚簇索引
发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT
属性。覆盖索引
进行查询,避免回表
带来的性能损耗。不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
SQL 标准定义了四个隔离级别:
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。`
与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock
锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化)**隔离级别。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
对于使用InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列
trx_id
:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列。roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。比方说我们的表hero
现在只包含一条记录:
mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name | country |
+--------+--------+---------+
| 1 | 刘备 | 蜀 |
+--------+--------+---------+
1 row in set (0.07 sec)
假设插入该记录的事务id
为80
,那么此刻该条记录的示意图如下所示:
假设之后两个事务id
分别为100
、200
的事务对这条记录进行UPDATE
操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志
,每条undo日志
也都有一个roll_pointer
属性(INSERT
操作对应的undo日志
没有该属性,因为该记录并没有更早的版本),可以将这些undo日志
都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id
,这个信息很重要,我们稍后就会用到。
ReadView 四个比较重要的内容:
m_ids
:表示在生成ReadView
时当前系统中活跃的读写事务的事务id
列表。min_trx_id
:表示在生成ReadView
时当前系统中活跃的读写事务中最小的事务id
,也就是m_ids
中的最小值。max_trx_id
:表示生成ReadView
时系统中应该分配给下一个事务的id
值。creator_trx_id
:表示生成该ReadView
的事务的事务id
。(只读事务默认都为 0)trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。trx_id
属性值小于ReadView
中的min_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
前已经提交,所以该版本可以被当前事务访问。trx_id
属性值大于ReadView
中的max_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
后才开启,所以该版本不可以被当前事务访问。trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。READ COMMITTED
和REPEATABLE READ
隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
以表hero
为例来,假设现在表hero
中只有一条由 事务id
为 80
的事务插入的一条记录:
mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name | country |
+--------+--------+---------+
| 1 | 刘备 | 蜀 |
+--------+--------+---------+
1 row in set (0.07 sec)
比方说现在系统里有两个事务id
分别为100
、200
的事务在执行:
# Transaction 100
BEGIN;
UPDATE hero SET name = ‘关羽‘ WHERE number = 1;
UPDATE hero SET name = ‘张飞‘ WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
小贴士: 再次强调一遍,事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。所以我们才在Transaction 200中更新一些别的表的记录,目的是让它分配事务id。
此刻,表hero
中number
为1
的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITTED
隔离级别的事务开始执行:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为‘刘备‘
这个SELECT1
的执行过程如下:
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[100, 200]
,min_trx_id
为100
,max_trx_id
为201
,creator_trx_id
为0
。name
的内容是‘张飞‘
,该版本的trx_id
值为100
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。name
的内容是‘关羽‘
,该版本的trx_id
值也为100
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。name
的内容是‘刘备‘
,该版本的trx_id
值为80
,小于ReadView
中的min_trx_id
值100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name
为‘刘备‘
的记录。之后,我们把事务id
为100
的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE hero SET name = ‘关羽‘ WHERE number = 1;
UPDATE hero SET name = ‘张飞‘ WHERE number = 1;
COMMIT;
然后再到事务id
为200
的事务中更新一下表hero
中number
为1
的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = ‘赵云‘ WHERE number = 1;
UPDATE hero SET name = ‘诸葛亮‘ WHERE number = 1;
此刻,表hero
中number
为1
的记录的版本链就长这样:
然后再到刚才使用READ COMMITTED
隔离级别的事务中继续查找这个number
为1
的记录,如下:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为‘刘备‘
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为‘张飞‘
这个SELECT2
的执行过程如下:
SELECT
语句时会又会单独生成一个ReadView
,该ReadView
的m_ids
列表的内容就是[200]
(事务id
为100
的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id
为200
,max_trx_id
为201
,creator_trx_id
为0
。name
的内容是‘诸葛亮‘
,该版本的trx_id
值为200
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。name
的内容是‘赵云‘
,该版本的trx_id
值为200
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。name
的内容是‘张飞‘
,该版本的trx_id
值为100
,小于ReadView
中的min_trx_id
值200
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name
为‘张飞‘
的记录。以此类推,如果之后事务id
为200
的记录也提交了,再此在使用READ COMMITTED
隔离级别的事务中查询表hero
中number
值为1
的记录时,得到的结果就是‘诸葛亮‘
了,具体流程我们就不分析了。总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。
对于使用REPEATABLE READ
隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView
,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。
比方说现在系统里有两个事务id
分别为100
、200
的事务在执行:
# Transaction 100
BEGIN;
UPDATE hero SET name = ‘关羽‘ WHERE number = 1;
UPDATE hero SET name = ‘张飞‘ WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
此刻,表hero
中number
为1
的记录得到的版本链表如下所示:
假设现在有一个使用REPEATABLE READ
隔离级别的事务开始执行:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为‘刘备‘
这个SELECT1
的执行过程如下:
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[100, 200]
,min_trx_id
为100
,max_trx_id
为201
,creator_trx_id
为0
。name
的内容是‘张飞‘
,该版本的trx_id
值为100
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。name
的内容是‘关羽‘
,该版本的trx_id
值也为100
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。name
的内容是‘刘备‘
,该版本的trx_id
值为80
,小于ReadView
中的min_trx_id
值100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name
为‘刘备‘
的记录。之后,我们把事务id
为100
的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE hero SET name = ‘关羽‘ WHERE number = 1;
UPDATE hero SET name = ‘张飞‘ WHERE number = 1;
COMMIT;
然后再到事务id
为200
的事务中更新一下表hero
中number
为1
的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = ‘赵云‘ WHERE number = 1;
UPDATE hero SET name = ‘诸葛亮‘ WHERE number = 1;
此刻,表hero
中number
为1
的记录的版本链就长这样:
然后再到刚才使用REPEATABLE READ
隔离级别的事务中继续查找这个number
为1
的记录,如下:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为‘刘备‘
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为‘刘备‘
这个SELECT2
的执行过程如下:
REPEATABLE READ
,而之前在执行SELECT1
时已经生成过ReadView
了,所以此时直接复用之前的ReadView
,之前的ReadView
的m_ids
列表的内容就是[100, 200]
,min_trx_id
为100
,max_trx_id
为201
,creator_trx_id
为0
。name
的内容是‘诸葛亮‘
,该版本的trx_id
值为200
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。name
的内容是‘赵云‘
,该版本的trx_id
值为200
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本。name
的内容是‘张飞‘
,该版本的trx_id
值为100
,而m_ids
列表中是包含值为100
的事务id
的,所以该版本也不符合要求,同理下一个列name
的内容是‘关羽‘
的版本也不符合要求。继续跳到下一个版本。name
的内容是‘刘备‘
,该版本的trx_id
值为80
,小于ReadView
中的min_trx_id
值100
,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为‘刘备‘
的记录。也就是说两次SELECT
查询得到的结果是重复的,记录的列c
值都是‘刘备‘
,这就是可重复读
的含义。如果我们之后再把事务id
为200
的记录提交了,然后再到刚才使用REPEATABLE READ
隔离级别的事务中继续查找这个number
为1
的记录,得到的结果还是‘刘备‘
,具体执行过程大家可以自己分析一下。
所谓的MVCC
(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SEELCT
操作时访问记录的版本链的过程,这样子可以使不同事务的读-写
、写-读
操作并发执行,从而提升系统性能。
READ COMMITTD
、REPEATABLE READ
这两个隔离级别的一个很大不同就是:生成ReadView的时机不同
小贴士: 我们之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的,大家可以对比上边举的例子自己试想一下怎么使用。 另外,所谓的MVCC只是在我们进行普通的SEELCT查询时才生效,截止到目前我们所见的所有SELECT语句都算是普通的查询,至于啥是个不普通的查询,我们稍后再说哈~
大家有没有发现两件事儿:
insert undo
在事务提交之后就可以被释放掉了,而update undo
由于还需要支持MVCC
,不能立即删除掉。MVCC
,对于delete mark
操作来说,仅仅是在记录上打一个删除标记,并没有真正将它删除掉。随着系统的运行,在确定系统中包含最早产生的那个ReadView
的事务不会再访问某些update undo日志
以及被打了删除标记的记录后,有一个后台运行的purge线程
会把它们真正的删除掉。
我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1
改成2
我们只需要记录一下:
将第0号表空间的100号页面的偏移量为1000处的值更新为
2
。
系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性
的要求。因为在系统奔溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为重做日志
,英文名为redo log
,我们也可以土洋结合,称之为redo日志
。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo
日志刷新到磁盘的好处如下:
redo
日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的
redo
日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo
日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。
type
:该条redo
日志的类型。
在MySQL 5.7.21
这个版本中,设计InnoDB
的大叔一共为redo
日志设计了53种不同的类型,稍后会详细介绍不同类型的redo
日志。
space ID
:表空间ID。
page number
:页号。
data
:该条redo
日志的具体内容。
redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来。
语句在执行过程中可能修改若干个页面。也就会产生多个 redo 日志,在恢复这些 redo 日志的时候,必须保证这个过程是原子性的,Innodb 的大叔们规定在执行这些需要保证原子性的操作时必须以组
的形式来记录的redo
日志,在进行系统奔溃重启恢复时,针对某个组中的redo
日志,要么把全部的日志都恢复掉,要么一条也不恢复。
如何把这些redo
日志划分到一个组里边儿呢?
设计InnoDB
的大叔做了一个很简单的小把戏,就是在该组中的最后一条redo
日志后边加上一条特殊类型的redo
日志,该类型名称为MLOG_MULTI_REC_END
,这样在系统奔溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_END
的redo
日志,才认为解析到了一组完整的redo
日志,才会进行恢复。否则的话直接放弃前边解析到的redo
日志。
设计InnoDB
的大叔为了更好的进行系统奔溃恢复,他们把通过mtr
生成的redo
日志都放在了大小为512字节
的页
中。为了和我们前边提到的表空间中的页做区别,我们这里把用来存储redo
日志的页称为block
(你心里清楚页和block的意思其实差不多就行了)。
设计InnoDB
的大叔为了解决磁盘速度过慢的问题而引入了Buffer Pool
。同理,写入redo
日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer
的连续内存空间,翻译成中文就是redo日志缓冲区
,我们也可以简称为log buffer
。这片内存空间被划分成若干个连续的redo log block
,就像这样:
向log buffer
中写入redo
日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。
事务
需要保证原子性
,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚
(英文名:rollback
),这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性
要求。
每当我们要对一条记录做改动时(这里的改动
可以指INSERT
、DELETE
、UPDATE
),都需要留一手 —— 把回滚时所需的东西都给记下来。
这些为了回滚而记录的这些东东称之为撤销日志,英文名为undo log
,我们也可以土洋结合,称之为undo日志
。
事务分为只读事务和读写事务。
如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB
存储引擎就会给它分配一个独一无二的事务id
,分配方式如下:
事务id
,否则的话是不分配事务id
的。事务id
,否则的话也是不分配事务id
的。每个表都会被分配一个唯一的table id
,这个后面会用到
CREATE TABLE undo_demo (
id INT NOT NULL,
key1 VARCHAR(100),
col VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1)
)Engine=InnoDB CHARSET=utf8;
mysql> SELECT * FROM information_schema.innodb_sys_tables WHERE name = ‘xiaohaizi/undo_demo‘;
+----------+---------------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+---------------------+------+--------+-------+-------------+------------+---------------+------------+
| 138 | xiaohaizi/undo_demo | 33 | 6 | 482 | Barracuda | Dynamic | 0 | Single |
+----------+---------------------+------+--------+-------+-------------+------------+---------------+------------+
1 row in set (0.01 sec)
常考内容:
数据库的事务,事物的特性,事务的隔离级别分别解决了哪些问题,理解什么是脏读,幻读! 事务的实现原理通过什么要保证的事务的特性?
什么是左连接,什么是右连接,什么是全连接,什么是内连接?
数据库的索引有什么作用?用什么来实现的?好处坏处是啥?
索引的种类,原理,索引存了哪些内容,什么时候索引会失效?唯一索引和主键索引的区别!单列和联合索引,最左匹配原则,什么时候该用联合索引?
怎么看这个表是否加了索引?
B树和B+树有什么区别?为什么索引不用B树?那B+树的叶子结点上存了哪些信息?
数据库的锁?乐观锁悲观锁,共享锁和排它锁。
数据库范式
数据库五大约束?
数据库连接池:工作原理,参数,种类,会出现的问题
数据库的读写分离,数据切分(数据库分库分表,水平切分垂直切分啊)
数据库的主从:实现原理,mysql主从复制的方式,如何配置主从同步,主从同步会出现的问题
Mysql:简单的sql语句至少能手写(分组,连接,子查询等)、sql语句的执行过程、、B/B+树相关问题,索引原理、聚集索引/非聚集索引区别、联合索引、explain、sql优化、数据库事务、乐观锁和悲观锁、脏读、虚读和不可重复读、隔离级别、MVCC、表锁/行锁/间隙锁、慢查询日志等。
标签:实现 计算 间隔 一句话 sse 字符 如何 效果 冗余
原文地址:https://www.cnblogs.com/xiehang/p/11623901.html