关系型数据库是指采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,一系列的行和列被称为表,一组表组成了数据库,关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 关系模型可以简单理解为二维表格模型,而一个关系型数据库就是由二维表及其之间的关系组成的一个数据组织。
关系型数据库(二维表格+关系)---关系模型---理解为二维表格模型---一行---一列---多行多列构成表---多张表构成库---此外关系模型还表明数据库中所存储的数据之间的联系(一对一、一对多、多对多)
关系型数据库是指采用了关系模型来组织数据的数据库,关系模型可以理解为二维表格模型,表中的一行称为一条记录或者元组,表中的一列称为字段或者属性,多行多列构成一张二维表,多张表构成数据库,此外关系模型还表明数据库中所存储的数据之间的联系(一对一、一对多、多对多),所以关系型数据库就是由二维表及其之间的关系组成了一个数据组织。
MySQL内部架构 MySQL的架构可以分成3层:连接层、Server层和存储引擎层
数据库长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。(尽量使用长连接)
如果没有命中缓存,就需要真正执行SQL语句了。
最终会生成一个语法树,方便后序模块读取表名、字段、语句类型。
SELECT * FROM T WHERE ID = 10;
执行器的执行流程是这样的:
- 调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中
- 调用引擎接口取"下一行",重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录的结果集返回给客户端。
略
MySQL的架构可以分为3层:连接层、服务层、存储引擎层
客户端执行一条SQL,首先需要通过TCP和服务端建立连接,验证用户名密码,读取用户权限。然后会查询缓存(前提是SELECT语句),如果缓存命中,则直接返回查询结果给客户端。如果未命中,则进入解析器中,进行SQL的词法分析(分析要干什么)、语法分析(分析语法是否正确)。之后会进入优化器,将SQL查询语句的执行方案确定下来(优化器考虑查询成本,确定执行方案)。最后进行执行器,执行前查看用户是否有权限,有的话就会根据表的引擎定义,调用存储引擎API对表进行读写。
Compressed和Dynamic行格式和Compact非常类似,主要区别在于处理行溢出数据时有些区别。
varchar是变长的,在存储数据的时候,也要把数据占用的大小存起来,存到[变长字段长度列表]里面,读取数据的时候才能根据这个变长字段长度列表去读取相应长度的数据。其他的TEXT、BLOB等字段也是这么实现的。 [变长字段长度列表]只出现在数据表中有变长字段的时候。
表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。 如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0。 当数据表的字段都定义成NOT NULL的时候,这时候表里的行格式就不会有NULL值列表了。
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。 NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。
在MySQL中,VARCHAR(n) 中的 n 表示的是字符数(即最大字符数),而不是字节数。这意味着,如果在 VARCHAR(n) 中存储的是多字节字符集(例如UTF-8),则可以存储的字节数可能会大于 n,因为一个字符可能由多个字节组成。 例如,如果使用 VARCHAR(10) 来存储UTF-8编码的字符串,那么可以存储的最大字符数为10,但实际上可以存储的最大字节数可能会大于10。
要注意的是,如果使用的是单字节字符集(例如Latin1),则一个字符就是一个字节,因此 VARCHAR(n) 中的 n 表示的也就是最大字节数。
一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。 如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。 计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532。 如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
变长字段类型,需要再加2字节(用于存储字段实际数据的长度值),不管三七二十一都使用2字节表示可变字段的长度
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。 Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。 Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
Compact:部分数据+溢出页地址,把溢出数据保存在溢出页中
Compressed和Dynamic:溢出页地址,把溢出数据全部保存在溢出页中
MySQL支持多种存储引擎,常见的有MyISAM、InnoDB、Memory。可以通过show engines()来查看Mysql支持的所有存储引擎以及默认的存储引擎。 MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
MySQL存储引擎采用的是插件式架构,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。 可以根据MySQL定义的存储引擎实现标准接口来编写一个属于自己的存储引擎,也可以使用官方提供的存储引擎。
索引:是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。(索引是以空间换时间的设计思想) 存储引擎:就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。
1. 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
2. 二级索引的 B+Tree 的叶子节点存放的是主键值+二级索引的索引列的值,而不是实际数据。
1.主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
2. 唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
3. 普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
4. 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
单列索引:建立在单列上的索引称为单列索引,比如主键索引
联合索引:建立在多列上的索引成为联合索引(通过将多个字段组合成一个索引)
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵守[最左匹配原则],联合索引会失效,这样就无法利用到索引快速查询的特性了。
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的SQL使用到
- 比如性别的区分度就很小,不适合建立索引或不适合排在联合索引靠前的位置,而UUID这类字段就比较适合做索引或排在联合索引列的靠前的位置
MySQL有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界限时30%)很高的适合,它一般就会忽略索引,进行全表扫描
B+Tree是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的,每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都会有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。 B+Tree相比于B树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况下,查询一个数据的磁盘IO依然维持在3-4次。 先会检查二级索引中的B+Tree的索引值,找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的B+Tree树查找到对应的叶子节点,然后获取整行数据。这个过程叫做【回表】,也就是说要查找两个B+Tree才能找到数据 在二级索引的B+Tree就能查询到结果的过程就叫做【索引覆盖】,也就是只需要查找一个B+Tree就能找到数据。
在查询时使用了二级索引,如果查询的数据能在二级索引里查询得到,那么就不需要进行回表操作,这个过程就是覆盖索引。
1. B树因为非叶子节点也存储数据,所以相比B+树存储的数据比较少
2. 二叉树的每个父节点的儿子节点个数只能是2个,这就比B+Tree高出不少,检索到目标数据所经历的IO次数较多
3. Hash适合做等值查询,不适合做范围查询,这也是B+Tree索引要比Hash表索引有着更为广泛的使用场景的原因
索引最大的好处是提高查询速度,但是索引也有缺点,比如:
MySQL会在包含GROUP BY子句的查询中默认添加上ORDER BY子句。
索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的
索引的价值是帮助快速定位。如果想要定位的数据有很多,那么索引就失去了它的实用价值。当数据重复度大时,比如高于10%的时候,也不需要对这个字段建立索引。
表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
例如身份证、UUID(在索引比较时需要转为 ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将他们删除,从而减少索引对更新操作的影响。
有时候有意或者无意的就对同一个列创建了多个索引,比如:index(a,b,c)相当于index(a)、index(a,b)、index(a、b、c)
下面列举一下几种常见优化索引的方法:
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。 使用前缀索引是为了减少索引字段大小,可以增加一个索引页中存储的索引值数量,有效提高索引的查询速度。 不过,前缀索引有一定的局限性:
InnoDB创建主键索引默认为聚簇索引,数据被存放在B+Tree的叶子节点上。也就是说,同一个叶子节点内的各个数据是按照主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。 另外,主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值和二级索引列值),这样二级索引占用的空间也就越小。
简单列举几条,发生索引失效的情况:
MySQL支持多种存储引擎,不同的存储引擎,存储数据的方式是不同的,我们最常使用的是InnoDB 存储引擎。 记录是按照行来存储的,但是数据库的读取并不是以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率非常低。 因此,InnoDB的数据是按[数据页]为单位来读写的。也就是说,当需要读取一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。 数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。 在File Header中有两个指针,分别指向上一个数据页和下一个数据页,连接起来相当于一个双向链表。采用链表的结构是让数据页之间不需要是物理上的连续,而是逻辑上的连续。
InnoDB里的B+数中的每个节点都是一个数据页,每个数据页都有对应的一个页目录,可以看到,在定位记录在哪一个页时,是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。
InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。 数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找的方式进行检索从而提高效率。
为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。 如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值+二级索引列值则就是二级索引,一个表中可以有多个二级索引。 在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。
MySQL的数据是持久化的,意味着数据(索引+记录)是保存在磁盘上的,因为这样即使设备断电了,数据也不会丢失。 内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的,也就是读取同样大小的数据,磁盘中读取的速度比从内存中读取的速度要慢上万倍,甚至几十万倍。 由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,如此往复,然后将用户数据读入到内存,也就是说查询过程中会发生多次磁盘I/O,而磁盘I/O次数越多,所消耗的时间也就越大。
MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。 要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。 二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。 为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。 而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。 B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。 但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:
B树是一种多路平衡搜索树,在存储大量数据的时候,它的整体高度相比二叉树来说会矮很多,对于数据库来说,所有的数据必然是存储在磁盘上的,而磁盘IO的效率很低,特别是在随机磁盘IO的情况下,效率更低,所以树的高度就会决定磁盘IO的一个次数,树的高度越低,磁盘IO次数越少,对性能提升越大,这就是为什么采用B树作为存储结构的原因,MySql使用B+树(说B+树与B树的区别)
1. B+树非叶子几点不存储数据,所以每一层能够存储的索引会增加,意味着B+树在层高相同的情况下存储的数据量比B树多,使得IO次数更少
2. MySQL范围查询,B树需要遍历所有节点
3. B+树IO次数比B树稳定
4. B+树全局扫描能力强一些,B+树需要遍历整个树
虽然InnoDB和MyISAM都支持B+树索引,但是它们数据的存储结构实现方式不同。不同之处在于:
当我们使用左或者左右模糊匹配的时候,也就是like %xx或者like %xx%这两种方式都会造成索引失效。
如果使用name like '%林'方式来查询,因为查询的结果可能是[陈林、张林、周林]等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
如果查询条件中对索引字段使用函数,就会导致索引失效。 为什么索引会失效呢? 因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。 不过,从MySQL8.0开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
在查询条件中对索引进行表达式计算,也是无法走索引的。 为什么索引会失效呢?
SELECT * FROM t_user WHERE id + 1 = 10; // 索引失效
SELECT * FROM t_user WHERE id = 10 - 1; // 索引不失效
因为索引保存的是索引字段的原始值,而不是id+1表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。
如果索引字段是字符串类型,但是在查询条件中,输入的参数是整型的话,执行计划的结果发现这条语句会走全表扫描。 MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后进行比较。
SELECT * FROM t_user WHERE phone = 130000001; // 索引失效
SELECT * FROM t_user WHERE id = "1"; // 索引不会失效
创建联合索引时,我们需要注意创建时的顺序问题,因为联合索引(a,b,c)和(c,b,a)在使用的时候会存在差别。 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
索引下推(tui)的大概原理是:截断的字段不会在Server层进行条件判断,而是会被下推到[存储引擎层]进行条件判断(因为c字段的值是在(a,b,c)联合索引里的),然后过滤出符合条件的数据后再返回给Server层。由于在存储引擎层就过滤掉大量数据,无需再回表读取数据来进行判断,减少回表次数,从而提升了性能。
索引下推(Index Condition Pushdown):它能减少回表次数,提高查询效率。"下推"是将部分服务层(上层)的事情,交给下层(存储引擎层)处理。
在没有开启索引下推之前,MySQL的查询方式是这样的:
这种方式我们会发现会涉及两次回表操作。索引下推就是针对这个场景的优化。
索引下推就是把过滤的场景下推给存储引擎层。 优化之后:当根据name和age进行查询的时候,直接在存储引擎中根据name和age进行过滤,得到匹配后的数据行,再回表查询(只涉及一次回表查询)
为什么会失效? 原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。 也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
在WHERE子句中,如果在OR前的条件是索引列,而在OR后的条件列不是索引列,那么索引会失效。 这是因为OR的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会走全表扫描。
今天给大家介绍了 6 种会发生索引失效的情况:
从这个思考题我们知道了,使用左模糊匹配(like "%xx")并不一定会走全表扫描,关键还是看数据表中的字段。 如果数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index)。
count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数的作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少个。
COUNT(查询条件)
SELECT COUNT(name) FROM t_user; 从t_user表中,统计name字段不会NULL的记录有多少个 SELECT COUNT() FROM t_user; 从t_user表中,字段不为NULL的记录有多少个,由于*永远不为NULL,所以实际上是统计表中有多少个字段.
在通过count函数统计有多少个记录时,MySQL的server层会维护一个名叫count的变量。 server层会循环向InnoDB读取一条记录,如果count函数指定的参数不为NULL,那么会将变量count加1,直到符合查询的全部记录被读取完,就退出循环。最后将count变量的值发送给客户端。
所以,count(*)执行过程跟count(1)执行过程基本一样的,性能没有什么差异。
count(字段)的执行效率相比前面count(1)、count(*)、count(主键字段)执行效率是最差的。
最坏的情况就是遍历全表
count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。 所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。 再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。
如果业务对于统计个数不需要很精确的值,这时,我们就可以使用show table status或者explain命令对表进行估算。 执行explain命令效率是很高的,因为它并不会真正的去查询,下图中的rows字段就是expalin命令对表t_order记录的估算值。
如果想精确的获取表的记录数,我们可以将这个计数值保存到单独的一张计数表中。 当我们在数据表中插入一条记录时,将计数表中的计数字段+1.也就是说,在新增和删除操作时,我们需要额外维护这个计数表。
事务就是一组SQL操作,是一个原子操作单元。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障,也就是A、I、D是手段,C是目的!
InnoDB引擎通过什么技术来保证事务的这四个特性的呢?
MySQL服务端是允许多个客户端连接的,这意味着MySQL会出现同时处理多个事务的情况。 那么多个并发事务会出现脏读、不可重复读、幻读的问题。
如果一个事务[读到]了另一个[未提交事务修改过的数据],就意味着发生了[脏读]现象。
事务A读到了事务B未提交修改过的数据,则事务A发生了脏读现象。
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了[不可重复读]现象。
事务A两次读取同一数据,出现前后两次读到的数据不一样的情况,就意味着事务A发生了不可重复读现象。
在一个事务内多次查询某个符合查询条件的[记录数量],如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了[幻读]现象。
事务A多次查询某个符合查询条件的记录数量,出现前后两次查询到的记录数量不一样的情况,就意味着事务A发生了幻读现象。
幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
脏读、不可重复读、幻读都是发生在读-写或写-读的情况下。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
这四种隔离界别具体是怎么实现的呢?
MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。
我们需要了解两个知识:
Read View是什么东西? Read View有四个重要的字段:
对于使用InnoDB存储引擎的数据库表,它的聚簇索引记录都包含下面两个隐藏列:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
如果记录的trx_id值小于Read View中的min_trx_id值,表示这个版本的记录是在创建Read View前已经提交的事务生成的,所以该版本的记录对当前事务可见。
如果记录的trx_id值大于等于Read View中的max_trx_id值,表示这个版本的记录是在创建Read View后才启动该的事务生成的,所以该版本的记录对当前事务不可见。
如果记录的trx_id值在Read View的min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中:
这种通过[版本链]来控制并发事务访问同一个记录时的行为就叫做MVCC(多版本并发控制)。
MVCC的实现依赖于:隐藏字段、Undo Log(undo 日志)、Read View
对于读已提交和可重复读隔离级别来说,它是通过MVCC来实现的。 在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的一个非常大的区别是它们生成ReadView的时机不同。
MVCC是通过数据行的多个版本管理来实现数据库的并发控制。
MVCC整体操作流程:
背:所谓的MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用READ COMMITTED、REPEATABLE READ这两种隔离级别在执行普通的SELECT(快照读)操作时访问记录的版本链的过程,这样可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTED、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTED在每一次普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
MVCC机制实现了快照读,普通SELECT查询就是快照读,快照读到的数据有可能不是最新的数据,它主要是为了实现可重复度的事务隔离级别。 当前读的是最新版本的数据,当前读就是加了锁的增删改查语句。 在快照读的情况下,InnoDB通过MVCC机制解决了幻读现象; 在当前读的情况下,InnoDB是无法通过MVCC解决幻读的现象,因为它每次读取的都是最新的数据;
读已提交隔离级别是在每次读取数据时,都会生成一个新的Read View。
可重复读隔离级别是启动事务时生成一个Read View,然后整个事务期间都在用这个Read View。
事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性,我们这次主要讲的是隔离性。 当多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,那为了避免这些问题,SQL 提出了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读。 要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别,要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。 而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,解决的方案有两种:
对于「读已提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:
这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。(每个事务都有自己的Read View) 在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
- 采用MVCC方式的话,读(MVCC)-写(加锁)操作彼此并不冲突,性能更高 - 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能
一般情况下,我们愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些情况下,必须采用加锁的方式执行。
在MySQL的范围里,根据加锁的范围,可以分为全局锁、表锁和行锁三类。
使用全局锁,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
// 添加全局锁 flush tables with read lock
// 释放全局锁
unlock tables
会话断开,全局锁会被自动释放。
全局锁的应用场景是什么? 全局锁的应用场景:做全库逻辑备份,这样在备份数据库期间,不会因为数据库或表结构的更新,而出现备份文件的数据与预期的不一样。
全局锁会带来什么缺点呢? 加上全局锁,意味着整个数据库都是只读状态。 那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。
既然备份数据库数据的时候,使用全局锁会影响业务,那么有什么其他方式可以避免?
MySQL表级锁有哪些?具体怎么用的 MySQL里面的表级锁有这几种:
// 表级别的共享锁,也就是读锁 lock tables t_student read;
// 表级别的独占锁,也就是写锁
lock tables t_student write;
// 释放表锁
unlock tables;
当会话退出后,也会释放所有表锁
我们不需要显式的使用MDL,因为当我们对数据库表进行操作时,会自动给这个表加上MDL;
MDL是为了保证用户对表执行CRUD操作时,防止其他线程对这个表的结构做了变更。 当有线程在执行select语句(加MDL读锁)的期间,如果有其他线程要更改表的结构(申请MDL写锁),那么会被阻塞,直达执行完select语句(释放MDL读锁) 当有线程对表结构进行变更(加MDL写锁)的期间,如果有其他线程执行了CRUD操作(申请MDL读锁),那么会被阻塞,直到表结构变更完成(释放MDL写锁)
MDL不需要显示调用,那它是在什么时候释放的? MDL是在事务提交后才会释放,这意味着事务执行期间,MDL是一直持有的。
所以为了能安全的对表结构进行变更,在对表结构进行变更前,先要看看数据库中的长事务,是否有事务已经对表加上了MDL读锁,如果可以考虑kill掉这个长事务,然后再做表结构的变更。
也就是在执行插入、更新、删除操作,需要先对表加上意向独占锁,然后对该记录加独占锁。
普通的select是不会加行级锁的,普通的select语句时利用MVCC实现一致性读,是无锁的 不过,select也是可以对记录加共享锁和独占锁的:
//先在表上加上意向共享锁,然后对读取的记录加共享锁 select ... lock in share mode;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。 如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。 那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。 所以,意向锁的目的是为了快速判断表里是否有记录被加锁。
表里的主键通常都会设置成自增的,这是通过对主键字段声明AUTO-INCREMENT属性实现的。 之后可以在插入数据时,可以不指定主键,数据库会自动给主键赋值递增的值,这主要是通过ANTO-INC锁实现的。 AUTO-INC锁是特殊的表锁,锁不再是一个事务提交后才释放,而是执行完插入语句后就会立即释放。 在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。 那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。
InnoDB是支持行级锁的,而MyISAM引擎是不支持行级锁的; 普通的select语句是不会对记录加锁的,因为它属于快照读。 如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
//先在表上加上意向共享锁,然后对读取的记录加共享锁 select ... lock in share mode;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
行级锁主要有三种类型:
Record Lock称为记录锁,锁住的是一条记录。而且记录锁是有S锁和X锁之分:
// 对主键id为1的这条记录加上X型的记录锁,这样其他事务就无法对这条记录进行修改
SELECT * FROM t_test WHERE id = 1 for update;
Gap Lock称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 间隙锁虽然存在X型间隙锁和S型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的就是为了防止插入幻影记录而提出的。
Next-Key Lock称为临键锁,是Record Lock+Gap Lock的组合,锁定一个范围,并且锁定记录本身。 next-key lock是包含间隙锁+记录锁的,如果一个事务获取了X型的next-key lock,那么另外一个事务在获取相同范围的X型的next-key lock时,是会被阻塞的。
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock也包含间隙锁)。 如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明事务想在某个区间插入新纪录,但是现在处于等待状态。
插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。
下面介绍的是MySQL加行级锁的一些规则。
什么SQL语句会加行级锁? InnoDB引擎是支持行级锁的,而MyISAM引擎是不支持行级锁的。 普通的select语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,是通过MVCC实现的。 如果要在查询时对记录加行级锁,可以使用下面两个方式,这种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁(S型锁) select ... lock in share mode;
//对读取的记录加独占锁(X型锁)
select ... for update;
除了上面这两条锁定读语句会加行级锁之外,update和delete操作都会加行级锁,且锁的类型都是独占锁(X型锁)。
//对操作的记录加独占锁(X型锁) update table .... where id = 1;
//对操作的记录加独占锁(X型锁)
delete from table where id = 1;
共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。
行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。 加锁的对象是索引,加锁的基本单位是next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock是前开后闭区间,而间隙锁是前开后开区间。 但是,在能使用记录锁或间隙锁就能避免幻读现象的场景下,next-key lock就会退化成记录锁或间隙锁。
用命令select * from performance_schema.data_locks\G;这条语句,查看事务执行SQL过程加了什么锁。
为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成记录锁?
为什么唯一索引等值查询并且查询记录「不存在」的场景下,在索引树找到第一条大于该查询记录的记录后,要将该记录的索引中的 next-key lock 会退化成「间隙锁」?
当对唯一索引进行范围查询时,会对每一个扫描到的索引加next-key锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁:
InnoDB存储引擎的默认事务隔离级别是可重复读,但是在这个隔离级别下,在多个事务并发的时候,会出现幻读的问题,所谓的幻读是指在同一事务下,前后执行两次同样的查询语句,出现了结果集数量不同的情况。 因此InnoDB存储引擎实现了自己的行锁,通过next-key锁来锁住记录本身和记录之间的"间隙",防止其他事务在这个记录之间插入新的记录,从而避免了幻读现象。 在InnoDB事务中,对记录加锁的基本单位是next-key锁,但是会因为一些条件退化成间隙锁,或者记录锁。加锁的位置准确的说,锁是加在索引上而非行上。
当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 那么锁就会持续很长一段时间,直到事务结束,而这期间除了 select ... from语句,其他语句都会被锁住不能执行,业务会因此停滞,接下来等着你的,就是老板的挨骂。 那 update 语句的 where 带上索引就能避免全表记录加锁了吗? 并不是。 关键还得看这条语句在执行过程种,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了。
MySQL的记录锁+间隙锁可以防止删除操作而导致的幻读问题。 接下来,来验证「 MySQL 记录锁+间隙锁可以防止删除操作而导致的幻读问题」的结论。 实验环境:MySQL 8.0 版本,可重复读隔离级。 现在有一张用户表(t_user),表里只有一个主键索引,表里有以下行数据: 现在有一个 A 事务执行了一条查询语句,查询到年龄大于 20 岁的用户共有 6 条行记录。 然后, B 事务执行了一条删除 id = 2 的语句: 此时,B 事务的删除语句就陷入了等待状态,说明是无法进行删除的。 因此,MySQL 记录锁+间隙锁可以防止删除操作而导致的幻读问题。
事务A在主键索引上加了10个next-key锁,如下:
这相当于把整个表都锁住了,其他事务进行增、删、改操作的时候都会被阻塞。 只有在事务A提交了事务,事务A执行过程中产生的锁才会被释放。
参考MySQL是如何加行锁的:非唯一索引等值查询和非唯一索引范围查询。
尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
’Insert语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的trx_id隐藏列来作为隐式锁来保护记录的。 什么是隐式锁? 当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。 隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。
事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
我们在执行一条"增删改"语句的时候,虽然没有输入begin开启事务和commit提交事务,但是MySQL会隐式开启事务来执行"增删改"语句,执行完就自动提交事务的,这样就保证了执行完"增删改"语句后,我们可以及时看到"增删改"的结果。
执行一条语句是否自动提交事务,是由 autocommit 参数决定的,默认是开启。所以,执行一条 update 语句也是会使用事务的。
一个事务在执行过程中,在还没有提交事务之前,如果MySQL发生了崩溃,要怎么回滚到事务之前的数据呢? 如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中发生了MySQL崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过undo log回滚到事务之前的数据。 实现这一机制就是undo log(回滚日志),它保证了事务的ACID特性中的原子性。 undo log是一种用于撤销回退的日志。在事务没提交之前,MySQL会先记录更新前的数据到undo log日志文件里面,当事务回滚时,可以利用undo log来进行回滚。 每当InnoDB引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到undo log里,比如:
在发生回滚时,就读取undo log里的数据,然后做原先相反操作。 不同操作,需要记录的内容是不同的,所以不同类型的操作(修改、删除、新增)产生的undo log的格式也是不同的。
一条记录的每一次更新操作产生的undo log格式都有一个roll_pointer指针和一个trx_id事务id:
另外,undo log还有一个作用,通过行记录隐藏字段 + Read View + Undo Log实现MVCC(多版本并发控制)来实现[读已提交]和[可重复读]隔离级别
因此,undo log两大作用:
InnoDB设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。 Buffer Pool本质上是InnoDB向操作系统申请的一段连续的内存空间,用来减少磁盘的IO操作,提高数据库的读写性能。
Undo页是记录什么? 答:开启事务后,InnoDB层更新记录前,首先要记录相应的undo log,如果是更新操作,需要把被更新前的旧值记录下来,也就是生成一条undo log,undo log会写入Buffer Pool中的Undo页面。
Buffer Pool是提高了读写效率,但是Buffer Pool是基于内存的,而内存总是不可靠的,万一断电重启,还没来得及落盘的脏页数据就会丢失。 为了防止断电导致数据丢失问题,InnoDB引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以redo log的形式记录下来,这个时候更新就算完成了。后续,InnoDB引擎会在适当的时候,由后台线程将缓存在Buffer Pool的脏页刷新到磁盘里,这里就是WAL技术(指MySQL的写操作并不是立即写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上)
redo log是物理日志,记录了某个数据页做了什么修改(比如对XXX表空间中的YYY数据页ZZZ偏移量的地方做了AAA更新),每当执行一个事务就会产生这样的一条或者多条物理日志。 在事务提交时,只要将redo log持久化到磁盘即可,可以不需要等到将缓存在Buffer Pool里的脏页数据持久化到磁盘。 当系统崩溃时,虽然脏页数据没有持久化,但是redo log已经持久化,接着MySQL重启后,可以根据redo log的内容,将所有数据恢复到最新的状态。
这两种日志属于InnoDB存储引擎的日志,主要区别:
事务提交之前发生了崩溃,重启后会通过undo log回滚事务,事务提交之后发生了崩溃,重启后会通过redo log恢复事务。 所以有了redo log,再通过WAL技术,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe(崩溃恢复)。可以看出来,redo log保证了事务四大特征中的持久性。
写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。 针对「顺序写」为什么比「随机写」更快这个问题,可以比喻为你有一个本子,按照顺序一页一页写肯定比写一个字都要找到对应页写快得多。 可以说这是 WAL 技术的另外一个优点:MySQL 的写操作从磁盘的「随机写」变成了「顺序写」,提升语句的执行性能。这是因为 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上 。 至此, 针对为什么需要 redo log 这个问题我们有两个答案:
实际上,执行一个事务的过程,产生的的redo log也不是直接写入磁盘的,因为这样会产生大量的IO操作,而磁盘的运行速度远远慢于内存。 所以,redo log也有自己的缓存——redo log buffer,每当产生一条redo log时,会先写到redo log buffer,后续再持久化到磁盘。 主要有下面几个时机:
默认情况下,InnoDB存储引擎有1个重做日志文件组,它由两个redo log文件组成,这两个redo日志的文件名叫:ib_logfile0和ib_logfile1。 redo log是为了防止Buffer Pool中的脏页丢失而设计的(实现事务的持久性)。那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。
binlog(二进制日志):记录所有对MySQL数据库的修改操作,包括插入、更新和删除等。binlog主要用于数据恢复到指定时间点或者指定事务。 MySQL在完成一条更新操作后,Server层还会生成一条binlog,等之后事务提交的时候,会将该事务执行过程中产生的所有binlog统一写入binlog文件。 binlog文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如SELECT和SHOW操作。
如果不小心整个数据库的数据被删除了,能使用redo log文件恢复数据吗?
MySQL的主从复制依赖于binlog,binlog记录了MySQL上的所有变化并以二进制形式保存在磁盘上。 主从复制的过程就是将binlog中的数据从主库传输到从库上。、 这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制binlog的线程同步完成。
从库是不是越多越好? 不是的,随着从库数量的增加,从库连接上的IO线程也比较多,主库也要创建同样多的log dump线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。 所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
binlog是mysql的二进制日志。binlog只记录变更相关的操作信息,如语句的执行时间、时长、操作数据等额外信息。不包括select、show操作。
概述:记录数据库执行的原生sql语句
优点:不需要记录每一行的变化,日志量相对较小,节省IO,提高性能,主从复制网络带宽小
缺点:由于记录的是SQL执行语句,为了保证这些语句能在slave端执行,必须记录上下文信息来保证slave上执行能得到与master相同的结果。某些sql函数无法使用,比如sysdate(),会出现主从数据不一致的情况。
概述:记录一行数据在更改前和更改后的变化
优点:详细记录了数据的修改情况,主从复制模式下可靠性高
缺点:row模式下二进制文件最大,占用硬盘空间,网络带宽高,对性能有一定的影响
概述:statement模式与row模式的结合,更加sql判断使用哪种模式,默认采用statement模式,特殊情况会转换为row模式
使用了类似uuid()、user()、current_user()等不确定的函数
使用了UDF
使用了临时表
使用了insert delay函数
事务提交后,redo log和bin log都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致的情况。 可以看到,在持久化redo log和bin log这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致。这是因为redo log影响主库的数据,bin log影响从库的数据,而这两个是独立的逻辑,所以redo log和bin log必须保持一致才能保证主从数据一致。 MySQL为了避免出现两份日志之间的逻辑不一致的问题,使用了两阶段提交来解决。两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。 两阶段提交是把单个事务的提交拆分成了2个阶段,分别是【准备(Prepare)阶段】和【提交(Commit)阶段】,每个阶段都由协调者和参与者共同完成。
留
留
留
留
在执行更新语句前要先连接数据库,这是连接器的工作。 接下来,分析器会通过词法分析和语法解析知道这是一条更新语句。优化器决定要使用ID索引,然后,执行器负责具体执行,找到这一行,然后更新。 与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它就是今天要讨论的redo log(重做日志)和binlog(归档日志)。如果接触MySQL,那这两个词肯定是绕不过的。
如果MySQL每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,那么整个IO成本、查找成本都很高,为了解决这个问题,MySQL使用WAL技术,WAL(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。 具体来说,当有一条记录需要更新的时候,InnoDB引擎就会把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面。 与此类似,InnoDB的redo log是固定大小的,比如可以配置一组4个文件,每个文件的大小是1GB,也就是总共4GB。从头开始写,写到末尾又回到开头循环写。 有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
这两种日志有以下三点不同
深色表示在执行器中执行,浅色表示在INNDB内部执行。
将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交"。
两阶段提交就是为了让两份日志之间的逻辑一致。 由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。
MySQL的数据是存储在磁盘里的,如果每次都从磁盘里面读取数据,这样性能是极差的。为了提高查询性能,InnoDB在存储引擎层设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存(buffer pool)中读取。
有了缓冲池后:
InnoDB会把存储的数据划分成若干个页,以页为磁盘和内存交互的基本单位,一个页的默认大小为16KB。因此,Buffer Pool同样需要按页来划分。 在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。 Buffer Pool 除了缓存「索引页」和「数据页」,还包括了「undo 页」,「插入缓存」、「自适应哈希索引」、「锁信息」等等。
不是的。当我们查询一条记录时,InnoDB会把整个页的数据加载到Buffer Pool中,因为,首先通过索引定位到磁盘中的页,再通过页里的页目录去定位到某条具体的记录。
为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一个控制块,控制块信息包括[缓存页的表空间、页号、缓存页地址、链表节点]等等。
为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的控制块作为链表的节点,这个链表称为Free链表(空闲链表)。
空闲页:每当需要从磁盘中加载一个页到Buffer Pool中时,就从Free链表中取一个空闲的缓存页,然后把缓存页对应的控制块的信息填上,最后把该缓存页对应的控制块从Free链表中移除。
设计Buffer Pool除了能提高读性能,还能提高写性能,也就是更新数据的时候,不需要每次都写入磁盘,而是将Buffer Pool对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。 为了快速知道哪些缓存页是脏页,就设计出Flush链表。它与Free链表相似,区别在于Flush链表的元素都是脏页。
LRU链表管理的初心:尽量高效的提高Buffer Pool的缓存命中率 简单的LRU算法的实现思路是:
简单的LRU算法并没有被MySQL使用,因为简单的LRU算法无法避免下面这两个问题:
小林coding
小林coding
引入了Buffer Pool后,当修改数据时,首先是修改Buffer Pool中的数据所在页,然后将其页设置为脏页,但是磁盘中还是原数据。 因此,脏页需要被刷入磁盘,保证缓存和磁盘数据一致,但若每次修改数据都刷入磁盘,则性能会很差,因此一般都会在一定时机进行批量刷盘。
可能大家担心,如果在脏页还没有来得及刷入到磁盘时,MySQL 宕机了,不就丢失数据了吗? 这个不用担心 InnoDB的更新操作采用的是Write Ahead Log策略,即先写日志,再写入磁盘。通过redo log日志让MySQL拥有了崩溃恢复的能力。
MySQL的数据是存储在磁盘里的,如果每次都从磁盘里面读取数据,这样性能是极差的。 为了提高查询性能,InnoDB在存储引擎层设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
有了缓冲池后:
● 当读取数据时,如果数据存在于Buffer Pool中,客户端就会直接读取Buffer Pool中的数据,否则再去磁盘中读取数据
● 当修改数据时,首先是修改Buffer Pool中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘InnoDB通过三种链表来管理缓存页:
- Free链表,来管理空闲页
- Flush链表,来管理脏页
- LRU链表,来提高缓存的命中率
它设计了一种高效的LRU链表,将链表分为young区和old两个区域,加入缓冲池的页,优先插入old区域;当页被访问的时候,才加入young区,目的是为了解决预读失效的问题。