注: 极客时间《MySQL实战45讲》–读书笔记
GitHub:https://github.com/ByrsH/Reading-notes/blob/master/Database/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2.md
SQL语句在MySQL各个功能模块中的执行过程:
大体来说,MySQL 可以分为 Server 层和存储引擎层两部分
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。建议大多数情况下不要使用查询缓存。MySQL8.0版本把查询缓存功能已去除。
与查询流程不一样的是,更新流程还涉及两个重要的日志模块:redo log(重做日志)和binlog(归档日志)。
在做更新操作时,如果每一次更新操作都立即写进磁盘,那么整个过程IO成本、查找成本都很高。redo log就是为了解决这一问题,MySQL通过WAL(Write-Ahead Logging)技术,先把更新操作写入redo log日志,并更新内存,这时更新操作就完成了,同时InnoDB引擎会在适当的时候(系统比较空闲时),将一批操作更新到磁盘里面。如果redo log写满时,就不能再执行新的更新,得停下来先擦掉redo log一些记录,写入磁盘,再执行新的更新。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe
redo log和binlog不同点:
update语句执行流程:
将redo log的写入拆成了两个步骤:prepare 和 commit,这就是“两阶段提交”。
“两阶段提交”的目的是为了让两份日志之间的逻辑一致。保证一致性。
数据恢复过程:
事物就是要保证一组数据库操作要么全部成功,要么全部失败。
事物支持是在引擎层实现的,但并不是所有引擎都支持事物,MyISAM引擎就不支持事物。
多事务同时执行的时候,可能会出现的问题:脏读、不可重复读、幻读。
SQL标准的事物隔离级别:
MySQL事物启动方式:
索引的出现是为了提高数据库查询效率,就像书的目录一样。
常见的索引模型:
哈希表这种结构适用于只有等值查询的场景,比如 Memcached及其他一些nosql引擎。由于哈希表存储不是有序的,因此做区间查询的速度是很慢的。
而有序数组在等值查询和范围查询场景中的性能就都非常优秀。有序数组只适用于静态存储引擎,由于它是有序存储,因此在插入数据时,要挪动后面的所有数据,成本太高。
为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。因此使用N叉数,而不是二叉树。
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB使用的是B+树索引模型。
每一个索引在 InnoDB 里面对应一棵 B+ 树。B+树能够很好地配合磁盘的读写特性,减少单词查询时访问磁盘的次数。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引。
普通索引的查询方式是先搜索普通索引树,找到主键值,再去搜索主键索引树。这个过程也被称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应尽量使用主键查询。
数据页的分裂与合并:当数据页已满时,有新的数据插入就会申请新的数据页,并把部分数据移动过去,这个过程称为数据页的分裂,不仅性能会受影响,而且数据页的利用率也会降低。当相邻的两个数据页由于数据删除,导致利用率很低时,就会将数据页合并。
从性能和存储空间方面考量,自增主键往往是更合理的选择。
有普通索引查询主键值,再回到主键索引树搜索的过程称为回表。那如何优化索引避免回表呢:
覆盖索引。指的是普通索引树上,节点已经包含了要查询的信息,也就是普通索引“覆盖了”我们的查询需求,因此就不用再使用主键查询,避免了回表。由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的优化手段。
最左前缀原则。B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符。
联合索引如何安排字段顺序:
而 MySQL 5.6 引入的索引下推优化,在索引遍历过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
数据库锁设计的初衷是处理并发问题。根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。
全局锁就是对整个数据库实例进行加锁。要想使数据库处于只读状态,加全局读锁,使用命令:Flush table with read lock(FTWRL)。使用全局读锁后,其他线程的数据更新语句、数据定义语句和更新类事物的提交语句都将被阻塞。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本。
MyISAM不支持可重复读的事物隔离级别,因数据库备份时要使用FTWRL(只读全局锁)来进行备份,期间只能进行读操作。因此建议使用InnoDB引擎。InnoDB使用mysqldump备份时,建议使用参数–single-transaction
设置全库只读:Flush table with read lock(FTWRL)、 set global readonly=true。建议使用FTWRL,原因是:
表锁一般是引擎不支持行锁是才被用到。
MySQL表级锁有两种:表锁、元数据锁(meta data lock, MDL)
表锁语法:lock tables … read/write。 unlock tables 释放锁。
MDL: 不需要显示使用,在访问一个表时会被自动加上。MDL的作用是,保证读写的正确性。当对一个表做增删改查、结构变更操作的时候,加MDL锁。
如果一个表有频繁的查询语句,而且客户端有重试机制,这时改变表结构可能会导致库的线程爆满,从而整个库挂掉。当有多个查询在执行时,语句结束后并不会马上释放MDL读锁,而是等到整个事物提交后释放,这时更改表结构会被阻塞,它被阻塞后,之后的所有操作都会被阻塞,整个表就不可读写了。
如何安全加字段:
MariaDB 已经合并了AliSQL的这个功能。
行锁是针对数据库表中行记录的锁。存储引擎InnoDB支持行锁,MyISAM不支持行锁。
两阶段锁协议:在InnoDB事物中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。原因是这个锁的时间最少,减少了事务之间的锁等待,提升了并发度。
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
应对死锁有两种策略:
如果死锁检测过多,就会消耗大量的CPU资源。比如1000个并发线程同时更新一行记录,那么死锁检测就是100万量级。
如何解决:
begin/start transaction 命令并不是事物的的起点,在执行的它们之后的第一个操作InnoDB表的语句,事物才真正启动。要想立即启动事物可以使用start transaction with consistent snapshot这个命令。
视图:
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。
InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。
对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
除了 update 语句外,select 语句如果加锁,也是当前读。
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
change buffer:
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存中,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
唯一索引要确定其唯一性,因此必须要将数据页读取到内存中,判断其是否唯一。所以唯一索引用不到change buffer,普通索引可以用到。
change buffer 使用的是buffer pool里的内存,change buffer实际上是可以持久化的数据,将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。系统后台会定期merge,数据库正常关闭也会merge。
对于读多写少的业务来说,页面在写完之后马上读的概率很小,因此很多更新操作会缓存之change buffer 中,这样的话使用效果会很好。但是业务是写入之后会马上读取的话,会触发merge,这样随机访问IO的次数不会减少,同时又增加了change buffer的维护成本,这样反而起到了副作用。
普通索引和唯一索引应该怎么选择,其实,这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,我建议你尽量选择普通索引。
redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
使用哪个索引是由 MySQL 来确定的,确切的说是优化器的工作。
查询扫描行数是优化器重要的依据条件之一。
一个索引上不同的值越多,这个索引的区分度就越好。索引上不同值的个数也称为基数。基数是通过采样统计来得出的值,采样统计时,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
analyze table t 命令,可以用来重新统计索引信息。在实践中,如果发现explain的结果预估的rows值跟实际情况差距比较大,可以使用这个方法处理。
大多数时候优化器都能找到正确的索引,但偶尔会选错索引。这是有以下处理方法:
MySQL支持前缀索引,你可以定义字符串的一部分作为索引。默认地,创建时不指定长度索引就会包含整个字符串。
前缀索引只取字符串前几位,比整个字符串索引占用空间更小。但可能会增加额外的记录扫描次数,因为依据前缀查询后,要去主键索引查找判断是否正确,这时有可能前缀一样后面的字符串不一致,就需要再去字符串索引查找,这就增加了记录扫描次数(回主键查找次数)。
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
如何确定前缀的长度:
1、统计出这个列上有多少个不同的值:
mysql> select count(distinct email) as L from SUser;
2、依次取不同长度的前缀来统计不同值,看哪个值不小于 L * 95%(5%接受区分度损失比例):
mysql> select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
使用前缀索引就用不上覆盖索引对查询性能的优化了。例如在不使用前缀索引情况下,覆盖索引含有要查询的信息,就不用回表查询ID索引了。使用了前缀索引,还需查询ID索引确认是否是要查找的记录。
如果遇到前缀的区分度不够好的情况时,该怎么办:
第一种方式是使用倒序存储。有时字符串倒叙会有很好的区分度。
mysql> select field_list from t where id_card = reverse(‘input_id_card_string’);
第二种方式是使用hash字段。你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
mysql> select field_list from t where id_card_crc=crc32(‘input_id_card_string’) and id_card=‘input_id_card_string’
相同点:都不支持范围查询。倒叙存储是按照倒叙字符串排序的,hash字段只能支持等值查询。
区别:
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据数据页的内容一致,称为“干净页”。不论是脏页还是干净页,都在内存中。
因此不难想象,平时执行很快的更新操作,其实就是在写内存和日志,而MySQL偶尔“抖”一下的那个瞬间,可能就是在刷脏页。
什么情况下回触发数据库的flush过程:
上述四种场景对性能的影响:
首先告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。通过innodb_io_capacity这个参数设置,该值建议设置成磁盘的IOPS(可以通过fio工具测试)。
InnoDB怎么控制引擎按照“全力”的百分比来刷脏页?
刷盘速度要参考两个因素:一个是脏页比例,一个是redo log写盘速度。
无论是你的查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用 IO 资源并可能影响到了你的更新语句,都可能是造成你从业务端感知到 MySQL“抖”了一下的原因。要避免这种情况,就要合理地设置innodb_io_capacity的值,并且平时要多关注脏页的比例,不要让它经常接近75%。
在刷脏页时,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷(该行为在机械硬盘时代很有意义,但在SSD时IOPS往往就不是瓶颈了)。innodb_flush_neighbors参数来控制这个行为。值为1时,会有上述的“连坐”机制;值为0时,表示不找邻居,只刷自己。MySQL8.0,该参数默认值为0。
主要内容:数据库表的空间回收。为什么简单地删除表数据到不到表空间回收的效果,如何正确回收空间?
InnoDB表包含两部分:表结构定义和数据。MySQL8.0之前,表结构是存在 .frm 为后缀的文件里,8.0版本则允许把表结构定义放在系统数据表中了。
参数 innodb_file_per_table 设置为OFF时,表的数据放在共享表空间,也就是跟数据字典放在一起;当设置为ON表示,每个InnoDB表数据存储在一个以.idb为后缀的文件中。从MySQL5.6.6开始,默认为ON。
建议设置该值为ON。因为单独存储为一个文件更容易管理,可以通过drop table命令直接删除这个文件。如果放在共享表空间中,即使表删掉了,空间也是不会回收的。
记录和数据页删除后,会被标记为删除可复用。记录的复用只限于符合范围条件的数据,当有新的记录插入时,如果位置范围是在删除的记录上,则复用。对于数据页则是可复用与任何位置。
如果相邻的两个数据页利用率都很小,系统会把两个数据页上的数据合并到一个页上,另外一个数据页就被标记为可复用。
如果用delete命令把整个表的数据删除,那么所有的数据页都被标记为可复用。但磁盘上,文件不会变小。
delete命令只是把记录的位置,或者数据页标记为“可复用”,但磁盘文件的大小是不会变的。这些可复用,而没有被利用的空间,看起来就像是“空洞”。
不仅删除数据会造成空洞,插入数据,更新索引上的值也会造成空洞。当一个在一个已满的数据页上插入数据,就会申请一个新的页面保存数据,页分裂完成后,老的数据页尾就留下了空洞。更新索引值,可以理解为删除一个旧的值,再插入一个新值。
可以使用 alter table A engine=InnoDB命令来重建表。该命令的执行流程是:新建一个与表A结构相同的表B,然后按照主键ID递增的顺序,把数据一行一行从表A读出插入表B。表B就没有像表A中的空洞了,索引更加紧凑。完成后用表B替换表A。
在MySQL5.5之前,流程和上述描述差不多,区别是临时表B不需要你自己创建,MySQL会自动完成上述操作。在上述往临时表插入数据过程中,如果有新的数据要写入表A的话,就会造成数据丢失。因此在整个DDL过程中,表A不能有更新,也就是DDL不是Online的。
MySQL5.6版本开始引入Online DDL,对操作流程做了优化:
count(*)的实现方式
不同的MySQL引擎,count(*)有不同的实现方式:
InnoDB没有把总行数存起来的原因是:由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”也是不确定的。
在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
小结:
如果要经常统计操作记录总数的话,应该自己把操作记录表的行数存起来。
使用redis缓存来保持计数会存在不精确的问题:
如果把计数存在一个表中,就可以解决存在缓存中出现的问题:
count(*), count(主键 id), count(字段), count(1) 性能分析:
结论是:按照效率排序的话,count(字段)
排序动作的执行,可能在内存中完成,也可能需要使用外部排序,利用磁盘临时文件辅助排序。这取决于排序所需要的内存和MySQL为排序开辟的内存(sort_buffer)的大小。sort_buffer的大小由sort_buffer_size参数决定。
内部排序使用快速排序算法,外部排序一般使用归并排序算法。
上述排序如果单行数据很大,那么排序性能就会很差。
修改参数,当单行长度超过该值时,MySQL就会换一种算法。先取排序的字段和ID,然后排序,最后再回表取出结果集。
SET max_length_for_sort_data = 16;
如果MySQL认为内存足够大,会 优先选择全字段排序,若内存不够用则选择rowid排序。
MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
建立联合索引,避免执行排序:
使用覆盖索引,避免回表查询:
Extra 字段显示 Using temporary,表示的是需要使用临时表; Using filesort,表示的是需要执行排序操作。
对于InnoDB表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。
对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。优化器这时就会优先考虑的是,用于排序的行越小越好(是因为单行数据很大,那么排序性能就会很差),所以,MySQL这时就会选择rowid排序。
如果你创建的表没有主键,或者把一个表的主键删掉了,那么InnoDB会自己生成一个长度为6字节的rowid来作为主键。
order by rand() 使用了内存临时表,内存临时表排序的时候使用的是rowid排序方法。
如果临时表大小超过了tmp_table_size(默认值是16M),那么内存临时表就会转成磁盘临时表。
当使用磁盘临时表的时候,对应的就是一个没有显式索引的InnoDB表的排序过程。
临时文件算法–>归并排序算法
优先队列排序算法:
mysql> select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y1,1; // 在应用代码里面取 Y1、Y2、Y3 值,拼出 SQL 后执行
select * from t limit @Y2,1;
select * from t limit @Y3,1;
在实际应用的过程中,比较规范的用法就是:尽量将业务逻辑写在业务代码中,让数据库只做“读写数据”的事情。
如果对字段做了函数计算,就用不上索引了。原因:对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。 优化器会选择遍历索引(主键索引或字段索引,取决于索引大小)。
要想使用到索引的快速定位能力,就要把字段的函数操作改为范围查询。比如下面两个:
在mysql查询语句中,如果作为条件的值类型与字段类型不一样时,可能会存在类型转换,比如vachar类型和整数。在mysql中,字符串和数字比较的话,是将字符串转换成数字。如果varchar类型的字段转换成数字的话,将会用到函数,从而就用不上索引,将全表扫描。
当两个表的字符集不同时(例如utf8、utf8mb4),做表关联查询的时候用不上关联字段的索引。原因:当两个字符集不同时,MySQL就会先做类型转换,再进行比较。转换规则是“按数据长度增加的方向”进行转换,类似于程序设计语言中的自动类型转换。如果是被驱动表里的字段做转换,就会用到函数操作,因此该字段的索引将无法使用,优化器会放弃走树搜索功能。由此可以得出导致被驱动表做全表扫描的直接原因是:连接过程中要求在被驱动表的索引字段上加函数操作。
优化方案:
例如:
alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null;
select d.* from tradelog l, trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
碰到这种情况大概率表被锁住了。可以使用 show processlist 命令,查看当前语句处于什么状态,然后根据不同的状态,用不同的方法处理。
如果语句对应的状态是 Waiting for table metadata lock, 那么表示的是,现在有一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了。
这类问题的处理方式,就是kill掉持有MDL写锁的线程。
当状态是 waiting for table flush 时,表示一个线程正要对表t做flush操作,flush操作通常很快。而这个flush table命令被别的语句堵住了,flush命令又堵住了select语句。
通过下面语句查询阻塞线程,然后通过 kill 线程号,杀死线程。
mysql> select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'`\G
session B 更新完100万次,会生成100万个回滚日志(undo log)。 session A 第一个select是一致性读,会依次执行 undo log,执行了100万次以后,才将1结果返回。 session A第二个select查询是当前读,因此会直接得出1000001 结果。
注意:下面的假设场景都是不成立的,是为了反正间隙锁的存在。
幻读指的是一个事物在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
幻读说明:
sessionA在T1时刻锁住了d=5的记录,但是在T2时刻sessionB更新语句可以执行,同样T4时刻sessionC的更新语句也可以执行,因为SessionA没有锁住id=5 和 id=1的行。因此就破坏了SessionA在T1时刻要锁住 d=5的数据行。
执行顺序:
结果:(0,5,100)、(1,5,100) 和 (5,5,100)
结果:(0,5,5)、(1,5,100),和(5,5,100)
上面两种假设都会造成严重的数据不一致问题。
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。
间隙锁,锁的是两个值之间的空隙。例如下面插入6条记录产生的7个间隙:
当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的6个记录加上了行锁,还同时加了7个间隙锁。这样就确保了无法再插入新的记录。
但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。间隙锁的目标是保护这个间隙,不允许插入值。但它们之间是不冲突的。
间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。例如 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20,25]、(25,+supernum]
间隙锁和 next-key lock 的引入帮我们解决了幻读的问题。但是间隙锁的引入,可能会导致同样的语句锁住更大的范围,这就影响了并发读。
间隙锁是在可重复读隔离级别下才会生效的。如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时要解决可能出现的数据和日志不一致的问题,需要把binlog格式设置为row。
加锁规则:包含了两个“原则“,两个“优化”和一个“bug”。
1,根据原则1,sessionA加锁范围是 (5,10]
2,同时根据优化2,这是等值查询,而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁范围是 (5,10)
覆盖索引上的锁:
在这个例子中,lock in share mode 只锁覆盖索引,但是如果是 for update ,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上锁。
锁是加在索引上的;需要注意的是,如果你要用 lock in share mode 来给行加读锁避免数据被更新的话,就必须的绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段,因此查询时会回表,使用主键索引查询,从而在主键索引上加锁。例如sessionA 查询改为: select d from t where c=5 lock in share mode;
主键索引加锁:行锁 id=10 和 next-key lock(10, 15]
sessionA加锁是:索引c上的 (5,10],(10,15] 两个 next-key lock。主键id索引上id=10行锁。
由于唯一索引的范围查询会扫描到第一个不满足条件的行为止,索引sessionA的加锁范围是 (10, 15], (15, 20]。
林晓斌认为:既然是唯一索引,当查询到id=15行时,就应该停止向前扫描,不必加(15, 20] next-key lock。
delete语句加锁的逻辑,其实是跟 select … for update 是类似的,也就是文章开始时总结的几点。
蓝色区域左右两边是虚线,表示开区间,即 (c=5,id=5) 和 (c=15,id=15) 这两行上都没有锁。
加锁范围:(c=5,id=5) 到 (c=10,id=30)这个前开后闭区间。因为有 limit 2的限制,因此遍历到(c=10,id=30)这一行时,已满足条件,就结束了。
这个例子的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
sessionA在索引c上加锁: next-key lock (5,10] 和 间隙锁 (10,15)。sessionB的加锁过程是,先加间隙锁 (5,10),加锁成功,再加行锁 c=10,这时候被锁住阻塞。sessionA这时执行insert语句,会被sessionB的间隙锁锁住。由于出现了死锁,InnoDB让sessionB回滚。
在分析加锁规则时可以用 next-key lock来分析,但是具体执行的时候,是要分成间隙锁和行锁两段来执行的。
上面所有案例都是在可重复读隔离级别下验证的。同时,可重复读隔离级别遵守两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的。
在读提交隔离级别下还有一个优化,即:语句执行过程中加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交。也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短。
正常的短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连。如果使用的是短连接,在业务高峰期时,就可能出现连接数突然暴涨的情况。
max_connections 参数,用来控制一个MySQL实例同时存在的连接数上限,超过这个值系统会拒绝接下来的连接请求,并报错提示“Too many connections”。
碰到超过连接池上限的情况,调高 max_connections 参数并不太可取。因为max_connections 值太大,就会有更多的连接进来,系统的负载可能会进一步加大,大量的资源耗费在权限验证等逻辑上,已经连接的线程拿不到CPU资源去执行业务的SQL请求。下面有两种有损的方案推荐给你。
通过 kill connection + id 主动剔除线程空闲的连接。这个行为跟事先设置 wait_timeout 的效果是一样的。wait_timeout参数表示线程空闲 wait_timeout 多秒之后,就会被MySQL直接断开连接。
如果连接数过多,你可以优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。因为如果断开事务内的连接,就会造成MySQL事务回滚,对业务是有影响的。
我们可以通过 show processlist 查看那些线程处于 sleep(空闲) 状态,然后通过查 information_schema 库的 innodb_trx表,找出trx_mysql_thread_id的值,表示值为该id的线程是处于事务内的。
mysql> show processlist;
mysql> select * from information_schema.innodb_trx\G;
从数据库端主动断开连接可能是有损的,尤其是有点应用端收到错误后,不重新连接,而是直接使用这个已经不能用的句柄重试查询。这会导致应用端看上去,“MySQL一直没恢复”。
让数据库跳过权限验证阶段。跳过权限验证的方法是:重启数据库,并使用 skip-grant-tables 参数启动。这样,整个MySQL会跳过所有的权限验证阶段,包括连接过程和语句执行过程。
但这样做的风险极高,不建议使用该方案。特别是数据库可以外网访问的话。
在mysql中,会引发性能问题的慢查询,大体有以下三种可能:
这种场景一般就是通过紧急创建索引来解决。最高效的做法就是直接执行 alter table 语句。
比较理想的是能够在备库先执行。假设现在的服务是一主一备,主库A,备库B,这个方案的执行流程是:
在紧急处理时,上述方案的效率是最高的。但是在平时考虑使用 gh-ost 方案可能更稳妥。
由于sql语句书写不当,导致语句没有使用上索引。这时我们可以通过改写SQL语句来处理。MySql5.7 提供了 query_rewrite 功能,可以把输入的语句改写成另一种模式。
mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");
call query_rewrite.flush_rewrite_rules();
这时的应急方案就是给这个语句加上 force index。同样的也可以使用查询重写功能,给原来的语句加上 force index。
在现实系统中,出现最多的是前两种情况,即:索引没设计好和语句没写好。而这两种情况可以在系统上线前避免的。
也可以使用开源工具 pt-query-digest 来分析。
有时候由于业务突然出现高峰,或者应用程序bug,导致某个语句的QPS突然暴涨,也可能导致MySql压力过大,影响服务。
如果是因功能引起的,最理想的情况是让业务把新功能下掉。
只要 redo log 和 binlog 保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复。
binlog的写入机制
binlog的写入逻辑:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
每个线程都有自己的 binlog cache,但是共用一份 binlog 文件。
write 和 fsync 的时机,是由参数 sync_binlog 控制的:
因此在出现IO瓶颈的场景中,将 sync_binlog 设置成一个比较大的值,可以提升性能。考虑到丢失日志量的可控性,比较常见的是将其设置为 100~1000 中的某个值。
将 sync_binlog 设置为N,对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志。
事务在执行过程中,生成的redo log 是要先写到 redo log buffer,并不是每次写入redo log buffer都要直接持久化到磁盘。在事务还没提交的时候,redo log buffer 中的部分日志有可能被持久化到磁盘。
1、存在redo log buffer中,物理上是在MySQL进程内存中,也就是图中的红色部分;
2、写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache 里面,也就是图中黄色部分;
3、持久化到磁盘,对应的是 hard disk,也就是图中绿色部分。
日志写到redo log buffer是很快的,write到 page cache 也很快,但是持久化到磁盘就慢很多了。
InnoDB提供了 innodb_flush_log_at_trx_commit 参数来控制 redo log的写入策略,它有三种可能的取值:
InnoDB有一个后台线程,每隔1秒,就会把 redo log buffer中的日志,调用write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
注意,事务在执行过程中的 redo log 也是会直接写入 redo log buffer中的,这些 redo log 也会被后台线程一起持久化到磁盘。也就是说,一个还没提交的事物的 redo log,也是可能被持久化到磁盘的。
除了后台线程每秒一次的轮询操作外,还有两种场景会把一个没提交事物的 redo log 写入到磁盘中。
两阶段提交:redo log 先 prepare,再写 binlog, 最后再把 redo log commit。
双1配置指的是将 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare阶段),一次是binlog。
日志逻辑序列号(log sequence number)LSN是单调递增的,用来对应 redo log 的一个个写入点。每写入length长度的redo log, LSN的值就会增加 length。
上图是三个并发事务(trx1、trx2、trx3)在 prepare 阶段,都写完了 redo log buffer,持久化到磁盘的过程,对应的LSN分别是 50、120、160。
因此,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。
MySQL为了让组提交的效果更好,把redo log做fsync的时间拖到了步骤1之后。
这样一来 binlog 也可以组提交了,不过由于第3步执行的很快,导致能集合到一起持久化的binlog比较少。如果想提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。
1、binlog_group_commit_sync_delay参数表示延迟多少个微秒后才调用 fsync;
2、binlog_group_commit_sync_no_delay_count 参数,表示累计多少次后才调用 fsync。
两个条件是或的关系,也就是只要满足其中一个就会调用 fsync。
WAL机制主要得益于两个方面:
如果你的MySQL出现性能瓶颈,而且瓶颈在IO上,那么可以通过下面三种方法提升性能:
在状态1中,客户端读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过来,到本地执行。建议将节点B(备库)设置成只读(readonly)模式。原因如下:
备库B和主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步过程是这样的:
binlog有三种格式:statement、row、mixed
当执行以下SQL语句后,不同的binlog日志格式会有不同的记录内容:
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
对于statement格式的binlog,可以通过以下语句查看记录:
mysql> show binlog events in 'master.000001';
可以看到statement格式的binlog会记录完整执行的SQL语句。但是这会可能在某些情况下出现主备数据不一致的情况,上述执行的SQL语句中两个判断条件,并且还有一个limit 1 的限制,如果主库delete语句使用的是索引a,备库delete语句使用的是索引t_modified,就可能两个库删除的不是同一条记录,从而造成数据不一致的情况。
通过 show warnings; 可以看到是有告警信息的。
对于row格式的binlog存储内容:
使用 mysqlbinlog 命令来解析:
mysqlbinlog -vv data/master.000001 --start-position=8900;
可以看到,当 binlog_format 使用的是 row 格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去执行的时候,肯定会删除 id=4的行,就不会出现主备删除不同行的问题了。
mixed格式指的是statement 和 row 格式同时存在。考虑到statement格式可能会导致主备不一致,所以要使用 row 格式。但是row 格式又太占空间了,而且还消耗 IO 资源,影响执行速度。因此就有了 mixed 格式,当mysql认为这条 SQL 语句可能引起主备不一致时,就用 row 格式,否则就用 statement 格式。
因此,线上的MySQL设置的binlog格式至少应该是 mixed,现在越来越多的场景要求把格式设置成row,这么做的一个直接的好处就是恢复数据。
用 binlog 来恢复数据的标准做法是,用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。类似下面的命令:
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
双M结构:
双 M 结构,即:节点A和B之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。
但是有一个问题需要解决,就是节点A上更新来一条语句,然后把生成的 binlog 发给节点 B,节点B执行完这条更新语句后也会生成binlog。如果节点A同时是B的备库,相当于又把节点 B 新生成的 binlog 拿过来执行来一次,然后节点 A 和 B间,会不断地循环执行这个跟新语句,也就是循环复制了。
MySQL在binlog 中记录了这个命令第一次执行时所在实例的 server id。因此,用下面的逻辑来解决循环复制的问题:
正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确执行,备库就能达到跟主库一致的状态,这就是最终一致性。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值。
在备库上执行 show slave status 命令,它的返回结果里面会显示 second_behind_master,用于表示当前主备延迟了多少秒。
如果主备库时间不一致,并不会导致主备延迟的值不准。因为备库在连接主库后会通过 SELECT UNIX_TIMESTAMP() 函数来获得当前主库的系统时间。在计算second_behind_master 时会扣除掉这个值。
网络正常时,主库传输binlog到备库的网络耗时是很少的,主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生成 binlog 的速度要慢。
第一种是在有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。
第二种是备库的压力大。在主备选用相同规格的机器后,备库可能会用于提供一些读能力。当备库上的查询消耗了大量的 CPU 资源,影响了同步速度,就会造成主备延迟。通常的解决方案如下:
第三种是大事物。大事物的执行时间过长,主库上必须等事物执行完成才会写入 binlog,再传给备库。比如一次性地用 delete 语句删除太多数据。还有大表的DDL。
第四种是备库的并行复制能力。
图中的SBM是参数 second_behind_master 参数的简写。
可靠性优先策略中是有不可用状态的,判断SBM=0的过程中,主库和备库都是只读状态,当SBM=0时才把B设置为可写状态。这端时间内数据库是不可以写数据的。
如果强行切换,不判断 SBM 是否等于0,直接把连接切到备库B,并且让备库可以读写,那么系统几乎就没有不可用时间了。但是这样切换的代价就是可能出现数据不一致的情况。
从上面的分析中,可以得出一些结论:
可靠性优先策略下,异常切换的效果。主备延迟30分钟,主库A掉电,HA系统要切换B作为主库。
在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。
第一个黑色箭头表示客户端写入主库,第二个黑色箭头表示备库上 sql_thread 执行中转日志(relay log)。如果客户端写入主库的并发度远大于备库执行中转日志的并发度,那么将会造成严重地主备延迟。
coordinator 就是原来的 sql_thread,不过现在它不再是直接更新数据了,只负责读取中转日志和分发事务。真正的日志更新变成了worker线程。worker线程的个数是由参数 slave_parallel_workers 决定的。根据经验,值设置为 8~16 之间最好(32核物理机)。
coordinator在分发的时候,需要满足以下两个基本要求:
MySQL5.5版本是不支持并行复制的,备库只能单线程复制。 林实现了两种并行策略:按表分发策略和按行分发策略。
按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行。因为数据是存储在两个不同的表里,所以按表分发,可以保证两个 worker 不会更新同一行。如果有跨表的事务,还是要放在一起考虑的。
假设事务T要修改表 t1 和 t3,事务T的分配流程:
事务在分发时,跟 worker 的冲突关系:
解决热点表的并行复制问题,就需要一个按行并行复制的方案。按行复制的核心思路是:如果两个事务没有更新相同的行,它们在备库上可以同时执行。显然,这个模式要求 binlog 格式必须是 row。
这时候,我们判断一个事务 T 和 worker 是否冲突,用的就不是“修改同一个表”,而是修改同一行。
基于行的策略,事务 hash 表中还需要考虑唯一键。即 key 应该是 “库名 + 表名 + 索引 a + a 的值”。可见相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。
两个方案都有一些约束条件:
虽然按行分发策略的并行度更高。不过,如果是要操作很多行的大事务的话,按行分发的策略有两个问题:
可以通过设置阈值,单个事务如果超过设置的阈值,就暂时退化为单线程模式。
官方5.6版本支持了并行复制,只是支持的粒度是按库并行。
MariaDB 策略,目标是“模拟主库的并行模式”。但它并没有实现“真正的模拟主库并发度”这个目标。
MariaDB 的并行复制策略利用了 redo log 组提交优化的特性:
具体实现:
可以看到,在主库执行的时候,多组事务是并行执行的,但在从库上同时只能一组事务执行,这样系统的吞吐量就不够。这个方案很容易被大事务拖后腿,如果 trx2 是一个超大事务,那么在 trx1 和 trx3执行完后,就只有一个worker线程在工作了,是对资源的浪费。
参数 slave-parallel-type 来控制并行复制策略:
MySQL 5.7 并行复制策略的思想是:
用于控制binlog 从 write 到 fsync 时间的参数,既可以减少 binlog 的写盘次数,也可以制造更多的“同时处于 prepare 阶段的事务”,从而提升备库复制并发度的目的。
新增了一个新的并行复制策略,基于 WRITESET 的并行复制。由参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。可取值:
一主多从结构:
图中虚线箭头表示的是主备关系,也就是 A 和 A’ 互为主备关系。从库 B、C、D 指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写和一部分读,其他的读请求由从库分担。
在一主多从架构下,主备切换问题:
相比于一主一备的切换流程,一主多从在切换完成后,A’会成为新的主库,从库 B、C、D 也要改连接到 A’。从而主备切换的复杂性也相应增加了。
我们把节点B设置成A’的从库的时候,需要执行一条 change master 命令:
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos
前四个参数分别是主库的 IP、端口、用户名和密码。最后两个参数是要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。这个位置就是我们所说的同步位点。
同步位点的获取是不精确的,考虑到切换过程中不能丢失数据,所以找位点的时候,总是要找一个“稍微往前”的,再通过判断跳过哪些在从库B上已经执行的事务。
一种取同步位点的方法:
等待新主库 A’ 把中转日志(relay log)全部同步完成;
在 A’ 上执行 show master status 命令,得到当前 A’ 上最新的 File 和 Position;
取原主库 A 故障的时刻 T;
用 mysqlbinlog 工具解析 A’ 的 file, 得到 T时刻的位点。
mysqlbinlog File --stop-datetime=T --start-datetime=T
end_log_pos 后面的值“123”可以把它作为 $master_log_pos。当然这个值是不精确的,因为如果主库在刚传完insert语句的binlog时掉电,那么从库 B和备库 A’ 都已执行过该insert语句,在从库执行 change master 命令时,就会把insert 的binlog同步到从库B执行,这时就会出错(主键重复),然后停止同步。
通常情况下,在切换任务的时候,要先主动跳过这些错误,有两种常用的方法:
1、主动跳过一个事务。每次碰到错误时,就执行一次跳过命令。
set global sql_slave_skip_counter=1;
start slave;
2、通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。这种设置是在主备切换时,当切换完成,稳定执行一定时间后,还需把这个参数设置为空。
例如设置 slave_skip_errors 为 “1032,1062”。
通过上述方式来实现主备切换,操作都很复杂,而且容易出错。MySQL5.6版本引入了GTID,彻底解决了这个困难。
Global Transaction identifier,也就是全局事务 ID,是一个事务在提交时生成的。格式: GTID=server_uuid:gno,server_uuid 是一个实例第一次启动时生成的,是一个全局唯一的值;gno 是一个整数,初始值为1,每次提交事务的时候分配给这个事务,并加1。
官方文档给的定义是: GTID=source_id:transaction_id
在启动MySQL实例时,通过加上参数 gtid_mode=on 和 enforce_gtid_consistency=on 就可以启动GTID模式了。在GTID模式下,每一个事务都与一个GTID一一对应。GTID的两种生成方式,由session变量gtid_next的值决定:
这样,每个 MySQL 实例都维护了一个GTID 集合,用来对应“这个实例执行过的所有事务”。
在 GTID 模式下,备库 B 要设置为新主库 A’ 的从库的语法如下:
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1
master_auto_position=1 表示这个主备关系使用的是 GTID 协议。
在主备切换时,从库会把自己的 GTID 集合发送个 新主库 A’ ,A’ 会用自己的 GTID 集合与从库比较,算出差集。如果 A’ 不包含差集中的所需要的 binlog 事务,那么直接返回错误;如果包含,则找出差集只中的第一个事务发给从库,之后就从这个事务开始,往后读文件,顺序取 binlog 发送给从库。
读写分离的主要目标就是分摊主库的压力。图1 中的结构是客户端(client)主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。由客户端来选择数据库进行查询。
还有一种架构是,在MySQL和客户端之间有一个中间代理层 proxy,客户端只连接 Proxy,由Proxy根据请求类型和上下文决定请求的分发路由。
两种架构的特点:
无论使用哪种架构,都会碰到由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。
这种“在从库上会读到系统的一个过期状态”的现象,我们暂且称之为“过期读”。
处理方案:
强制走主库方案就是,将查询请求做分类:
主库更新后,读从库之前先 sleep 一下。
这种方式看起来不靠谱,存在的问题:
在确保主备无延迟的情况下,才进行从库请求。
第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于0(表示主备无延迟)。直到参数等于 0 时才能执行查询请求。
第二种方法,对比位点确保主备无延迟:
如果两组值对应相等,就表示接收到的日志已经同步完成。
第三种方法,对比 GTID 集合确保主备无延迟:
当两个集合相同时,表示备库接收到的日志都已同步完成。
可见对比位点和对比 GTID 方法,都要比判断 second_behind_master 是否为 0 更准确。
但是还没有达到精确的程度,因为发往备库的 binlog 是始终少于等于主库 binlog 内容的,即使从库 binlog 都执行完成,与主库的状态还是可能不同步的,就会出现过期读。
半同步复制:semi-sync replication
semi-sycn 配合判断主备无延迟的方案,存在两个问题:
执行逻辑:
trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和Position;
选定一个从库执行查询语句;
在从库上执行 select master_pos_wait(File, Position, 1);
如果返回值是 >= 0 的正整数,则在这个从库执行查询语句;
否则,到主库执行查询语句。
select master_pos_wait(file, pos[, timeout]);
表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。
对于不予许过期读的要求,只有两个选择,一种是超时放弃,一种是转到主库查询。需要注意的是考虑到主库的压力,做好限流策略。
select wait_for_executed_gtid_set(gtid_set, 1);
命令的逻辑是:
等 GTID 的执行逻辑:
通过设置参数 session_track_gtids = OWN_GTID ,让 MySQL 在执行事务后,返回包中带上 GTID。然后通过 API 接口 mysql_session_track_get_first 从返回包中解析 GTID 的值。
select1成功返回,只能说明这个库的进程还在,并不能说明主库没有问题。
innodb_thread_concurrency 参数设置并发线程上限,一旦并发线程数达到这个值, InnoDB 在接收到新请求的时候,就会进入等待状态,直到有线程退出。默认值为0,表示不限制并发线程数量。如果不限制,CPU核数有限,线程上下文切换成本高。一般建议设置为 64-128 之间。
并发连接和并发查询是不同的概念。show processlist 的结果,指的是并发连接,达到几千个影响不大,多占些内存。“当前正在执行”的语句,才是并发查询,其占用CPU资源。
线程在进入锁等待以后,并发线程的计数会减一,也就是等行锁(也包括间隙锁)的线程并不在 innodb_thread_concurrency 中。原因是,进入锁等待的线程并不吃CPU了,再有就是如果计数不减一,线程等待数到达上限值,整个系统就会堵住了。
如果线程真正地在执行查询,比如上述的 select sleep(100) from t,还是要算进并发线程的计数的。
可以通过在MySQL里面创建一张表,然后定期执行查询语句。这样就可以检测出由于并发线程过多导致数据库不可用的情况。
但是,如果遇到磁盘空间满的时候,这种方法就变得不可用。更新事务时要写 binlog,而一旦 binlog 所在磁盘的空间占用率达到 100%, 那么所有的更新语句和事务提交的 commit 语句就都会被堵住。但是可以进行正常的读数据。
常见的做法是放一个 timestamp 字段,用来表示最后一次执行检测的时间。
节点可用性检测应该包含主库和备库。我们一般会把数据库 A 和 B 的主备关系设计为双 M 结构,所以在备库 B上执行的命令也要发回给主库 A。如果主库 A 和 备库 B 都用相同的更新命令,就可能出现行冲突,也就是可能会导致主备同步停止。所以更新的表就要有多行数据,id 对应每个数据库的 server_id 做主键。
mysql> CREATE TABLE `health_check` (
`id` int(11) NOT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
/* 检测命令 */
insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
MySQL 规定了主库和备库的 server_id 必须不同,这样就可以保证检测命令不会发生冲突。
如果检测命令的id一样,两个语句同时执行,那么在主库和备库上就都是“insert行为”,写到binlog里面就都是Write rows event,这个冲突就会导致主备同步停止。
但是依然存在一些问题,当 IO 利用率 100%时,表示系统的 IO 是在工作的,每个请求都有机会获得 IO 资源,执行任务。检测执行语句需要的资源很少,所以可能在拿到 IO 资源的时候就可以提交成功,并且在未到达超时时间时就返回给检测系统,那么就会得出“系统正常”的结论。但其实业务系统上正常的 SQL语句已经执行的很慢了。
上述所说的所有方法,都是基于外部检测的。存在两个问题:一是检测方式并不能真正完全的反应系统当前的状况,二是定时轮询检测有时间间隔,如不能及时发现,可能导致切换慢的问题。
可以通过MySQL 统计的 IO 请求时间,来判断数据库是否出问题。
可以通过下面语句,查看各类 IO 操作统计值:
select * from performance_schema.file_summary_by_event_name where event_name = 'wait/io/file/innodb/innodb_log_file';
从上到下分别统计的是:所有 IO 类型、读操作、写操作、其他类型数据。
performance_schema 统计是有性能损耗的,因此只需打开需要的监控即可。例如 redo log:
mysql> update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';
检测语句,200 ms 为异常:
mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;
清空语句:
mysql> truncate table performance_schema.file_summary_by_event_name;
用动态的观点看加锁加锁规则:两个“原则”,两个“优化”,一个bug。
在执行过程中,通过树搜索的方式定位记录的时候,用的是“等值查询”的方法。
出现死锁后,执行 show engine innodb status 命令输出相关信息。LATESTDETECTED DEADLOCK 记录的是最后一次死锁信息。
1、由于锁是一个一个加的,要避免死锁,对同一组资源,要按照尽量相同的顺序访问;
2、在死锁发生时刻,资源占用越多,回滚成本就越大。因此 InnoDB 会选择回滚成本更小的语句。
update 语句先插入再删除。
误删数据分类:
1、使用 delete 语句误删数据行;
2、使用 drop table 或者 truncate table 语句误删数据表;
3、使用 drop database 语句误删数据库;
4、使用 rm 命令误删整个 MySQL 实例。
在前提是 binlog_format=row 和 binglog_row_image=NULL 的前提下,如果使用 delete 语句误删了数据行,可以用 Flashback 工具通过闪回把数据恢复回来。
具体恢复数据时,对单个事务做如下处理:
对于误删数据涉及到多个事务的话,需要将事务的顺序调过来再执行。
恢复数据比较安全的做法是,恢复出一个备份,或者找一个从库作为临时库,在这个临时库上执行这些操作,然后再将确认过的临时数据库的数据,恢复回主库。
预防误删数据方法:
误删了库或表,就要求线上有定期的全量备份,并且实时备份 binlog。
恢复数据的流程:
1、为了加速数据恢复,在使用 mysqlbillog 命令时,加 database 参数来指定误删表所在的库。
2、在应用日志的时候,需要跳过 12 点误操作的那个语句的 binlog:
使用 mysqlbinlog 方法恢复数据不够快的原因:
一种加速的方法是,将临时实例设置成线上备库的从库:
把之前删掉的 binlog 放回备库的操作步骤:
如果距离上一个全量备份时间较长,那么恢复时间也会很长。对于非常核心的业务,是不允许太长的恢复时间。可以考虑搭建延迟复制的备库。
延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库 N 秒的延迟。这样就可以在备库上先 stop slave,再通过之前的方法跳过误操作命令,就可以恢复出需要的数据。
1、账号分离,不同的人有不同的权限,避免写错命令。
2、制定操作规范。避免写错要删除的表名。比如:在删除表之前先对表做改名操作。观察一段时间如果对业务没有影响,再通过管理系统删除有固定后缀的表。
尽量把备份跨机房,或者最好是跨城市保存。
MySQL中有两个 kill 命令:一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句;一个是 kill connection(可以缺省) + 线程 id,表示断开这个线程的连接,如果这个线程有语句正在执行,也是要先停止正在执行的语句。
kill 并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”。
当用户执行 kill query thread_id_B 时,MySQL 里处理 kill 命令的线程做了两件事:
1、一个语句执行过程中有多处 “埋点”,在这些 “埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY, 才开始进入语句终止逻辑;
2、如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到 “埋点”处;
3、语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。
先执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为2,然后执行
可以看到 session D 执行的 kill query C 命令没有什么效果,session E 执行了 kill connection 命令,才断开了 session C 的连接。但是这时,如果在 session E 中执行 show processlist,就会看到:
id=12 这个线程的 Command 列显示的是 Killed。客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中。
使用 kill query 命令之所以无效,是因为 12 号线程的等待逻辑是:每 10 毫秒判断一下是否可以进入 InnoDB 执行,如果不行,就调用 nanosleep 函数进入 sleep 状态。在这个过程中没有去判断线程的状态,因此根本不会进入终止逻辑阶段。因为等行锁进入等待状态是可以被唤醒的。
当执行 kill connection 命令时,过程是这样:
Command 列显示为 killed 是因为执行 show processlist 时有这样一个逻辑:如果一个线程的状态是 KILL_CONNECTION,就把 Command 列显示成 Killed。只有等到满足进入 InnoDB 的条件后,session C 的查询语句继续执行,然后才有可能判断线程的状态已经变成 KILL_QUERY 或者 KILL_CONNECTION ,再进入终止逻辑阶段。
造成 kill 无效的情况:
MySQL 是停等协议,线程执行的语句还没有返回的时候,再往这个连接里面继续发命令也是没用的。
第一个误解:如果库里面的表特别多,连接就会很慢。
我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢。
当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名的补全功能。在客户端连接成功后,需要多做一些操作:
连接命令中加上 -A ,就可以关掉这个自动补全的功能。
第二个误解:-quick 参数会让服务端变得更快。但实际上并不能让服务端加速,反而可能会降低服务端的性能,会让客户端变得更快。
MySQL客户端发送请求后,接收服务端返回结果的方式有两种:
MySQL客户处默认采用第一种方式,如果加上 -quick 参数,就会使用第二种不缓存的方式。
使用 -quick 参数,可以达到以下三点效果:
服务端取数据和发数据的流程是:
从流程中可以看到:
MySQL 是“边读边发的”。如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。
如果客户端不去读 socket receive buffer 中的内容,服务端线程的状态就会是 “Sending to client”,表示服务器端的网络栈写满了。
对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,建议使用 mysql_store_result 这个接口,也就是直接把查询结果保存在本地内存。如果执行了大查询,导致客户端占用很多内存,就需要使用 mysql_use_result 接口了,也就是不缓存,读一个处理一个。
一个查询语句的状态变化过程:
仅当一个线程处于“等待客户端接收结果”的状态,才会显示“Sending to client”;而如果显示成“Sending data”,它的意思只是“正在执行”。
查询的结果是分段发给客户端的,因此扫描全表,查询返回大量的数据,并不会把内存打爆。
内存的数据页是在 Buffer Pool(BP)中管理的,在 WAL里 Buffer Pool 不仅起到了加速更新的作用,还起到了加速查询的作用。
InnoDB 内存管理用的是最近最少使用(LRU)算法,算法的核心就是淘汰最久未使用的数据。
为了防止业务查询出大量不经常使用的数据,把 Buffer Pool 的空间之前的数据全部淘汰,存储这些未经常使用的数据,导致 Buffer Pool 命中率急剧下降,磁盘压力增加,SQL语句响应变慢,InnoDB对 LRU 算法做了改进。
在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。图中 LRU_old 指向的就是 old 区域的第一个位置,是整个链表的 5/8 处。靠近链表头部的 5/8 是 young 区,靠近链表尾部的 3/8 是 old 区域。
改进后 LRU 算法执行流程:
通过这个策略,在扫描大表的过程中,虽然也用到了 Buffer Pool,但是对 young 区域完全没有影响,从而保证了 Buffer Pool 响应正常业务的查询命中率。
由于 MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询结果来说,不会在 server 端保存完整的结果集。所有,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是不会把内存打爆。
对于 InnoDB 引擎内部,由于有淘汰策略,大查询也不会导致内存暴涨。并且,由于 InnoDB 对 LRU 算法做了改进,冷数据的全部扫描,对 Buffer Pool 的影响也能做到可控。
select * from t1 straight_join t2 on (t1.a=t2.a);
这个语句中改用 straight_join 让 MySQL 使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去 join。在这个语句中,t1 是驱动表,t2 是被驱动表。
这条语句的执行过程是,先遍历表 t1 ,然后根据从表 t1 中取出的每一行数据中的 a 值,去表 t2 中查找满足条件的记录。类似嵌套查询,用上了被驱动表中索引,称之为“Index Nested-Loop Join”,简称 NLJ。
如果不使用 join ,那么客户端会先用一个SQL语句查出 t1 表,再for循环连接数据库N次查询。效率显然不高,也就是 N+1 问题。
join 语句执行过程中,驱动表是走全表扫描,被驱动表是走树搜索(前提是被驱动表上有索引)。
假设被驱动表的行数是 M,每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引。时间复杂度为 2log2M。驱动表扫描 N 行,那么整个过程中复杂度近似 N + N2*log2M。
因此使用 join 语句的话,需要让小表做驱动。
如果被驱动表(t2)在匹配时没有用到索引,那么每次到 t2 去匹配的时候,就要做一次全表扫描,扫描行数就是 N * M 了。虽然结果是正确的,但是算法看上去太“笨重了”。这个算法叫做“Simple Nested-Loop Join”。
MySQL也并没有使用 Simple Nested-Loop Join 算法,而是使用了 “Block Nested-Loop Join” 算法,简称 BNL。
被驱动表上没有可用的索引,算法的流程如下:
可以看到整个过程中扫描的行数是 N + M。由于 join_buffer是以无序数组的方式组织,因此对表 t2 中的每一行,都要做 N 次判断。总共需要在内存中做 N * M 次判断。虽然 SNL 算法也是做了 N * M 次判断,但是BNL是在内存中做的判断,因此性能更好。
对于哪个表做驱动表更好,这里要考虑的因素是 join_buffer 的大小。如果 join_buffer 足够大,那么无论谁做驱动表,join_buffer 都可以一次性存入驱动表的全部数据,算法的扫描行数值都是 N + M ,内存判断次数值都是 N * M。
如果 join_buffer 存不下驱动表的全部内容,那么就会采用分段放的策略。其执行过程变为:
用 λ * N 表示驱动表的分段数,λ 的取值范围是 (0, 1)。那么算法的执行过程:
因此,在 join_buffer 大小不足以存入驱动表的全部数据情况下,驱动表为小表时,性能更好。
第一个问题:能不能使用 join 语句?
第二个问题:如果使用 join 语句,应该使用大表做驱动表,还是小表做驱动表?
所以这个问题的结论是选择小表做驱动表。
更准确的说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
回表是一行行搜索主键索引的,因为大多数的数据都是按照主键递增顺序插入得到的,所以可以认为,如果按照主键的顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
MRR 优化后的执行流程:
read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制。如果步骤 1 中 read_rnd_buffer 放满了,那么就先执行 2 、3 ,清空 read_rnd_buffer。之后继续找索引 a 的下一个记录,循环此步骤。
如果想要稳定地使用 MRR 优化的话,需要设置 set optimizer_switch=“mrr_cost_based=off”。现在的优化器策略更倾向于不使用 MRR。
需要注意的是,由于我们在 read_rnd_buffer 中按照 id 进行了排序,所以最后得到的结果集也是按照 id 递增的顺序。
MRR 能够提升性能的核心在于,查询语句在 a 索引上做范围查询,这样就可以排序主键 id,然后顺序读。
MySQL 5.6 版本引入 Batched Key Access (BKA)算法。其实就是对 NLJ 算法的优化。
NLJ 算法是从驱动表 t1 中,一行行取出 a 的值,再到被驱动表 t2 去做 join。对于 t2 表来说每次都是匹配一个值,这时 MRR 优化是用不上的。
NLJ 算法是没有用到 join_buffer的。要想一次性多传些值给表 t2,使用到 MRR 优化,可以通过先把从表 t1 取出来的数据放入 join_buffer,然后再传给表 t2,这就是 BKA 算法。
如果要使用 BKA 优化算法的话,需要设置:
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
在使用 BNL 算法时,如果被驱动表是一个大的冷数据表,除了会导致 IO 压力大以外,还会影响 Buffer Pool 的命中率。
多次扫描一个冷表,这个语句的执行时间超过 1 秒时,就会把冷表的数据页移到 LRU 链表头部。这种情况是对应与冷表数据量小于 Buffer Pool 的 3/8 时。如果冷表数据很大,old区域的数据页很有可能在 1 秒之内就被淘汰了。这样就不会进入young 区域,导致 young 区域的数据页没有被合理地淘汰。
大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。而对于 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。
为了减少这种影响,可以考虑增多 join_buffer_size 的值,减少对被驱动表的扫描次数。
BNL 算法对系统的影响主要包括三个方面:
优化常见的做法是在被驱动表上给 join 字段加索引,把 BNL 算法转成 BKA 算法。
一些情况下,可以直接在被驱动表上建索引,这时可以直接转成 BKA 算法。但是有些时候不适合建索引,比如这个 join SQL语句是低频执行,而且经过 where 条件过滤后数据量很少,如果这时加索引的话就很浪费。如果不加索引,就要比对很多次,占用 CPU 资源。可以同过使用临时表来解决:
把表 t2 中满足条件的数据放在临时表 tmp_t 中;
为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;
让表 t1 和 tmp_t 做 join 操作。
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
如果 join_buffer 里面维护的不是一个无序的数组,而是一个哈希表的话,那么就不是 10 亿次判断,而是 100 万次hash 查找,这样速度就快很多。但目前MySQL还不支持。
可以把优化放在业务端实现。比如把表 t1 的数据存入一个hash 结构里,再取出表 t2 数据,然后去 hash 结构中比较。
优化方法中:
内存表:指的是使用 Memory 引擎的表,建表语法是 create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。
临时表:可以使用各种引擎类型。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。
临时表在使用上的几个特点:
在 join 优化时使用临时表的原因:
临时表经常会被用在复杂查询的优化过程中。分库分表系统的跨库查询就是一个典型的使用场景。
当查询条件里面没有用到分区字段 f,只能到所有的分区中取查找满足条件的所有行,然后统一做 order by 操作。有两种常用的思路。
第一种思路是,在Proxy 层的进程代码中实现排序。优点是处理速度快,拿到分库的数据以后,直接在内存中参与计算。不过这个方案缺点也很明显:
第二种思路是,把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作。流程类似于:
在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified;
在各个分库上执行
select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100;
把分库执行的结果插入到 temp_ht 表中;
执行下面语句得到结果
select v from temp_ht order by t_modified desc limit 100;
在创建临时表时,MySQL会给表创建一个 frm 文件保存表结构定义,还要有地方保存数据。frm 文件放在临时目录下,文件名后缀是 .frm,前缀是“#sql{进程 id}_{线程 id}_序列号”。
临时表数据存放:
在内存中每一个表都对应一个 table_def_key,用来区分不同的表。
因此对于两个不同的 session 创建相同表名的临时表,它们的 table_def_key 不同,磁盘文件名也不同,因此可以并存。
每个线程都有维护自己的临时表链表。每次 session 操作表的时候,先遍历链表检查是否有这个名字的临时表,如果有就优先操作临时表,如果没有再操作普通表;在 session 结束时候,对链表里的每个临时表,执行 “DROP TEMPORARY TABLE + 表名”操作。binlog 中也记录了这个 DROP … 这条命令。
在主备复制时,普通表的操作需要用到临时表,因此备库也要执行操作临时表相关的语句,所以对临时表的操作会写入 binlog。如果当前的 binlog_format=row,那么跟临时表相关的语句,就不会记录到binlog中,因为row格式会记录改变前后的真实数据值。只在 binlog_format=statment/mixed 的时候,binlog 才会记录临时表的操作。
主库 M 上的两个 session 创建了同名的临时表 t1,这两个 create temporary table t1 语句都会被传到备库 s 上。MySQL在记录 binlog 的时候,会把主库执行这个语句的线程 id 写到 binlog 中,table_def_key 的命名规则是:库名 + t1 + “M 的 serverId” + “session A/B 的 thread id”
临时表一般用于处理比较复杂的计算逻辑。由于临时表是每个线程自己可见的,所以不需要考虑多个线程执行同一个逻辑时的重名问题。相处退出时,临时表也能自动删除。
在 binlog_format=‘row’ 的时候,临时表的操作不记录到binlog中。
union 执行流程
(select 1000 as f) union (select id from t1 order by id desc limit 2);
可以看到,这里的内存临时表起到了暂存数据的作用,而且计算过程中还用上了临时表主键 id 的唯一性约束,实现了 union 的语义。如果上面的 union 改成 union all 的话,就没有了“去重”的语义,得到的结果直接作为结果集的一部分返回给客户端。因此也用不上临时表了。
select id%10 as m, count(*) as c from t1 group by m;
内存表的大小是有限制的,参数 tmp_table_size 就是控制这个内存大小的,默认是 16M。如果执行过程中发现内存临时表大小到达了上限,就会被内存临时表转成磁盘临时表,磁盘临时表默认使用 InnoDB 引擎。
group by 的语义逻辑,是统计不同的值出现的个数。由于每一行的 id%100 的结果是无序的,所以我们就需要一个临时表,来记录并统计结果。
但是如果扫描过程中可以保证出现的数据是有序的,那么 group by 将不再使用临时表,也不需要排序(5.7)。只需要从左到右,顺序扫描,依次累加。
在 MySQL 5.7 版本支持了 generated column 机制,用来实现列数据的关联更新。你可以用下面的方法创建一个列 z,然后在 z 列上创建一个索引。
alter table t1 add column z int generated always as(id % 100), add index(z);
select z, count(*) as c from t1 group by z;
在 group by 语句中加入 SQL_BIG_RESULT 这个提示,就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表。避免是先使用内存临时表,在转换为磁盘临时表。
MySQL的优化器发现,磁盘临时表是 B+ 数存储,存储效率不如数组高,就会直接用数组存储。
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
执行流程:
MySQL什么时候会使用内部临时表:
group by 使用原则:
InnoDB 表的数据就放在主键索引树上,主键索引是 B+ 树。主键索引上的值是有序存储的,在执行 select * 的时候,就会按照叶子节点从左到右扫描。
与 InnoDB 引擎不同,Memory 引擎的数据和索引是分开的。
内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置。主键 id 是 hash 索引,可以看到索引上的key并不是有序的。
InnoDB 和 Memory 引擎的数据组织方式是不同的:
两个引擎的一些典型不同:
内存表也支持 B-Tree 索引。
alter table t1 add index a_btree_index using btree (id);
不建议在生产环境使用内存表的原因:
内存表不支持行锁,只支持表锁。因此,一张表只要有更新,就会堵住其他所有在这个表上的读写操作。
数据持久化问题
数据放在内存中,是内存表的优势,同时也是一个劣势。数据库在重启的时候,所有的内存表都会被清空。
把普通内存表都用 InnoDB 表来代替:
内存临时表可以忽视掉内存表的两个不足:
在查询语句优化时,使用内存临时表比 InnoDB 临时表效果更好的原因:
表的结构定义存放在后缀名为 .frm 的文件中,但是并不会保存自增值。
不同的引擎对于自增值的保存策略不同。
如果id被定义为 AUTO_INCREMENT,在插入一行数据时,自增值的行为如下:
插入完数据后,依据插入的值后当前自增值的大小关系,自增值的变更也会有所不同,假设 x 为插入的值,y 为当前自增值:
新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 x 的值,作为新的自增值。
auto_increment_offset表示自增的初始值,auto_increment_increment 表示步长,默认值都是1。在一些场景下,需要改变默认值,比如双M的主备结构里要求双写的时候,我们就可能会设置 auto_increment_increment=2,避免两个库生成的主键发生冲突。
由于自增值的改变是在执行插入语句之前,而且当发现唯一键冲突的时候,自增值并没有改回去,就导致了自增主键id不连续的情况。
可见,唯一键冲突是导致自增主键 id 不连续的第一种原因。同样地,事物回滚也会产生类似的现象,这是第二种原因。
自增值不回退的原因是为了提升性能。如果允许自增值回退,那么将会出现主键冲突的情况,比如先申请到id的事务执行失败,后申请到的事务执行成功,那么就会出现自增值小于当前最大id,再后面的插入语句就会出现“主键冲突”。要解决这个问题每次申请id之前,要先判断表里 id 是否存在。或者是自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才可以申请自增id。这两种方法都会导致性能下降。因此,InnoDB放弃了这个设计,语句执行失败也不回退自增 id。这样保证了自增 id 是递增的,但不保证是连续的。
自增 id 锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。
在Mysql5.0版本的时候,自增锁的范围是语句级别的。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然这样设计会影响并发度。
mysql5.1.22版本新增参数 innodb_autoinc_lock_mode ,默认值是1。
为了数据一致性才会在 insert… select 语句执行时值设置成1,等语句结束后才释放锁。
因为在一个事务执行 insert… select 语句过程中,另一个事务向同一个表执行插入语句,如果这时 binlog日志的格式是statement,那么将会先记录一个事务的插入,再记录另一个事务的插入。当binlog在备库执行时,insert… select 语句将会连续执行完成后,再执行另一个插入语句,这样跟主库的数据就会不一致。
要解决数据不一致问题又两种思路:
在生产环境上,尤其是有 insert… select 这种批量插入数据的场景时,从并发的角度考虑,建议设置 innodb_autoinc_lock_mode=2 ,同时binlog_format=row。
这里说的批量插入数据,包含的语句是 insert…select、replace … select 和 load data 语句。普通的inset语句包含多个value值的情况下,申请完id后会立即释放锁,因为这类语句是可以计算出需要多少个 id 的。前面几种语句是不知道需要多少 id 的。
mysql 有一个批量申请自增 id 的策略:
如果申请到的主键id没用完,比如第三次分配了4个,但第三次只插入了2条记录,那么其他语句申请的id 将会是第4个id 后的新自增id。这就是主键id出现自增 id 不连续的第三种原因。
当使用 insert … select 语句时,会在 select 表主键索引上加 next-key lock 锁,锁住需要访问的资源,从而保证在执行时 binlog 和数据的一致性。
对于 insert into t select … from t 语句的执行,在5.7 版本上所有间隙会加上 next-key lock,但在 8.0 版本,next-key lock 只会加在需要访问的资源间隙。
在可重复读(repeatable read)隔离级别下,发生唯一键冲突的时候,并不只是简单地报错返回,还在冲突的索引上加了锁。主键索引和唯一索引加的都是 next-key lock(读锁)。
insert into … on duplicate key update 这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。会在索引上加一个排他的 next-key lock (写锁)。
如果有多个列违反了唯一性约束,就会按照索引的顺序,修改跟第一个索引冲突的行。
在可重复读隔离级别下,使用 insert … select 语句在两个表直接拷贝数据,会给表里扫描到的记录和间隙加读锁。
如果 insert 和 select 的对象是同一个表,则有可能会造成循环写入。需要引入用户临时表做优化。
insert 语句如果出现唯一键冲突,会在冲突的唯一值上加共享的 next-key lock(S 锁)。因此,在遇到唯一键约束报错后,要尽快提交或回滚事务,避免加锁时间过长。
如果可以控制对源表的扫描行数和加锁范围很小的话,我们简单地使用 insert … select 语句即可实现。为了避免对源表加读锁,更稳妥的方案是先将数据写到外部文件,然后再写回目标表。
使用 mysqldump 命令将数据导出组成一组 INSERT 语句:
mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql
主要参数含义:
然后可以用下面命令,将这些 insert 语句放到 db2 库里去执行:
mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"
执行流程:
直接将结果导出成 .csv 文件:
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';
需要注意的是:
用下面的 load data 命令将数据导入到目标表 db2.t 中:
load data infile '/server_tmp/t.csv' into table db2.t;
执行流程:
在备库重放:
db2
.t
。load data 命令两种用法:
select … into outfile 方法不会生成表结构文件,mysqldump 提供了一个参数 -tab, 可以同时导出表结构定义文件(t.sql)和 csv(t.txt) 数据文件:
mysqldump -h$host -P$port -u$user ---single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --tab=$secure_file_priv
MySQL 5.6 版本引入了可传输表空间的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。
执行流程:
三种拷贝表方法的优缺点:
grant 语句是用来给用户赋权的。
用户权限范围:
给用户 ua 赋予最高权限:
grant all privileges on *.* to 'ua'@'%' with grant option;
这个 grant 命令做了两个动作:
基于上面的分析可以知道:
收回上面 grant 语句赋予的权限,执行动作与上面相反:
revoke all privileges on *.* from 'ua'@'%';
MySQL 也支持库级别的权限定义。使用下面命令让用户拥有库 db1 的所有权限:
grant all privileges on db1.* to 'ua'@'%' with grant option;
基于库的权限记录保存在 mysql.db 表中,在内存里则保存在数组 acl_dbs 中。这条命令做了如下动作:
grant 修改 db 权限的时候,是同时对磁盘和内存生效的。
MySQL也支持更细粒度的表权限和列权限。表权限定义存放在表 mysql.tables_priv 中,列权限定义存放在表 mysql.columns_priv 中。这两类权限,组合起来存放在内存的 hash 结构 column_pri_hash 中。
赋权命令:
create table db1.t1(id int, a int);
grant all privileges on db1.t1 to 'ua'@'%' with grant option;GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
这两个权限每次 grant 的时候都会修改数据表,也会同步修改内存的 hash 结构。因此这两类权限的操作,会影响到已经存在的连接,也就是立即生效。
flush privileges 命令会清空 acl_users 数组,然后从 mysql.user 表中读取数据重新加载,重新构造一个 acl_users 数组。对于 db 权限、表权限和列权限,MySQL也做同样的处理。
正常情况下,grant/revoke 语句执行完后,内存和数据表会保持同步更新,因此没有必要跟着执行 flush privileges 命令。
在不规范操作的情况下,会造成内存和数据表中的数据不一致,比如直接操作系统表。这时就需要 flush privilege 语句来重建内存数据。
grant 语句会同时修改数据表和内存,判断权限的时候是使用内存数据。因此,规范地使用 grant 和 revoke 语句,是不需要随后加 flush privilege 语句的。
flush privilege 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以尽量不要使用这类语句。
把表数据分别存入不同的分区,每个分区对应一个 .idb 文件。对于引擎层来说,这是 n 个表,对于 Server 层来说,这是 1 个表。
验证对应引擎层来说是 n 个表。
如果单表过大,不使用分区表的话,就要使用手动分表的方式。分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码决定使用哪个分表。从引擎层看,这两种方式没有什么差别。性能方面也没有实质的差别。主要区别在 server 层。
每当第一次访问一个分区表的时候,MySQL 需要把所有的分区都访问一遍。一个典型的报错情况是,分区表的个数已经超过了,open_files_limit 参数的值,就会报错。
MyISAM 分区表使用的分区策略称为通用分区策略(generic partitioning),每次访问分区都由 server 层控制,有比较严重的性能问题。
MySQL 5.7.9 开始,InnoDB 引擎引入 本地分区策略(native partitioning)。这个策略是在 InnoDB 内部自己管理打开分区的行为。
从 MySQL 8.0 版本开始,就不允许创建 MyISAM 分区表了,只允许创建已经实现本地分区策略的引擎。
1、MySQL 在第一次打开分区表的时候,需要访问所有的分区;
2、在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;
3、在引擎层,认为是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问必要的分区。
分区表的一个显而易见的优势是对业务透明,相对于用户分表来说,业务代码更简洁。分区表可以很方便的清理历史数据。
即使在 SQL 语句中写成了 left join,执行过程还是有可能不是从左到右连接的,在被优化器优化后可能变为 join。也就是说,使用 left join 时, 左边的表不一定是驱动表。
如果需要 left join 的语义,就不能把驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。
join 语句条件是否放到 on 里面,最后优化后都会放到 where 条件里。
Simple Nested Loop Join 算法相比于 BNL 算法的性能差距在哪:
distinct 的语义是:按照某个字段 a 做分组,相同的 a 的值只返回一行。
group by 在没有聚合函数的情况下,其执行逻辑是一样的,因此性能相同。
执行流程:
自增 id 的生成顺序和 binlog 的写入顺序是不同的。
binlog 在记录 insert 语句之前,会先记录 SET INSERT_ID 语句,这个语句表示,这个线程里下一次需要用到自增值的时候,不论当前表的自增值是多少,固定用这个值。因此即使 insert 语句在备库上执行的顺序不同,也不会造成主备数据不一致的问题。
表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。
如果创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节(类型是 8 字节的 bigint unsigned,实现上只留 6 个字节的长度)的 row_id。
row_id 的取值特征:
写入表的 row_id 是从 0 到 2^48 - 1。达到上限后,下一个值就是 0 ,然后继续循环。
在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中。如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。因此在建 InnoDB 表时应该主动创建主键。覆盖数据,就意味着数据丢失,影响的是数据可靠性;报主键冲突,是插入失败,影响的是可用性。一般情况下可靠性优先于可用性。
MySQL 中 Xid 是用来对应事务的。
MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后加 1。事务的第一条执行语句执行时,会把 Query_id 赋值给事务的 Xid。
global_query_id 是一个纯内存变量,重启之后就清零。MySQL 重启之后会重新生成新的 binlog 文件。 global_query_id 到达上限 2^64 - 1 后,会继续从 0 开始计数。
Xid 是由 server 层维护的。InnoDB 内部使用 Xid ,是为了能够在 InnoDB 事务和 server 之间做关联。
InnoDB 内部维护了一个 max_trx_id 全局变量,每次申请一个新的 trx_id 时,就获得 max_trx_id 的当前值,然后将 max_trx_id 加1。
InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。
对于只读事务,InnoDB 并不会分配 trx_id,其值是把当前事务的 trx 变量的指针地址转成整数,再加上 2^48。这样做是为了保证:同一个只读事务查出来的 trx_id 是一样的;并发的只读事务查出来的 trx_id 是不同的。
有时候实验的时候会发现,trx_id 的值不止加1。这是因为:
只读事务不分配 trx_id 的好处:
系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。大小是 4 个字节,达到 2^32 -1 后,会重置为 0。
每种自增 id 达到上限后的表现不同: