读书笔记:Mysql实战45讲 (11-21讲)

11、怎么给字符串加索引

         比如email这个字段,如果email字段没有索引,那么这个语句只能做全表扫描

         mysql支持前缀索引,所以可以定义字符串一部分为索引。

            alter table s add index index1(email);

            alter table s add index index2(email(6))

     存储图:

   

   前缀索引这个占用的空间更小,但是会增加额外的记录扫描次数

  如果使用index1:

     1.从index1索引树找到满足值‘[email protected]’,取得ID2的值

     2.到主键上查找主键值是ID2的行,判断email值正确,将这行记录加入结果集,然后取index索引上查找位置下一条记录发现不满足条件,循环结束

     这个过程,只需要回主键索引取一次数据,所以系统认为只扫描了一行

  如果使用index2:

     1.从index2索引树找到满足索引值是'zhangs'记录,然找到ID1,到主键查找主键值为ID1的行,判断email值是不是'zhangsss...com',如果不是丢弃,然后取下一条记录,接着判断,如果值对,记录加入结果集,然后重复,直到取到值不是‘zhanggs’时,循环结束

      使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本

      在建立索引时关注的是区分度,区分度越高越好。

前缀索引对覆盖索引的影响

       如果使用email整个字符串的索引结构的话,可以利用覆盖索引,从index1查到结果后直接返回了,不需要回到ID索引再查一次,而如果使用index2(即email(6)索引结构的话),就不得不回到ID索引再去判断email字段的值

      使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是选择是否使用前缀索引考虑的一个因素

其他方式:

  比如身份证前6位是地址码,所以可以创建12以上的前缀索引,但是索引选取越长,占用的磁盘空间就大,搜索效率越低

即占用更小的空间,也能达到相同的查询效率:

第一种:倒序存储

第二种:使用hash字段。在表上再创建一个整数字段,来保存身份证的校验码,同时再这个字段创建索引

    每次插入新纪录的时候,同时用crc32()这个函数得到校验码填到这个新字段。

相同点:都不支持范围查询,只支持等值查询

区别:从查询效率上看,使用hash字段方式查询性能更稳定,因为crc32算出来值虽然有冲突概率但是非常小,而倒序存储毕竟还是用的前缀索引方式,也就是说还是会增加扫描行数、

小结:

    1、直接创建完整索引,这样比较占用空间
    2、创建前缀索引,节省空间,但是会增加查询扫描次数,并且不能使用覆盖索引
    3、倒序存储,再创建前缀索引,用于绕过字符串本身前缀区分度不够问题
    4、创建hash字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描

12、为什么我的MySQL会抖一下

     在工作中,有种这样的场景,一条SQL语句,正常执行的时候特别快,但是有时也不知道怎么回事,它就会变得特别慢,而且这样的场景很难浮现,它不只随机,而且持续时间还很短。

     InnoDB在处理更新语句的时候,只做了一个写日志这一个磁盘操作,这个日志叫做redo log(重做日志),在更新内存写完redo log后,就返回给客户端,本次更新成功

     当内存数据也跟磁盘数据也内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入(flush)到磁盘后,内存和磁盘上的数据也的内容就一致了,称为“干净页”

    平常执行很快的更新操作,其实就在写内存和日志,而MySQL偶尔抖一下这个瞬间,可能就是在刷脏页(flush)

    第一种场景:InnoDB的redo log写满了。这个时候系统会停止所有更新操作,把chekpoint往前推进,redo log留出来空间继续写

 对应图,把checkpoint位置从CP推到CP`,浅绿色部分对应的所有脏页都flush到磁盘上。

第二种场景:系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据也,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先flush到磁盘

  在这里并没直接把内存淘汰,下次需要请求的时候,从磁盘读入数据页,然后拿redo log出来应用,这里其实从性能考虑的,如果刷脏页一定会写盘,就保证了每个数据页有两种状态

     一种是内存里存在,内存里就肯定是正确结果,直接返回

     另一种内存里没有数据,就可以肯定数据文件上市正确结果,读入内存后返回,这样效率最高

第三种场景:属于MySQL空闲的时候,这时系统没什么压力,就会把脏页进行flush到磁盘上

第四种场景:MySQL正常关闭的时候,这个时候会把内存的脏页都flush到磁盘上。这样下次MySQL启动的时候,就直接从磁盘上读数据,启动速度会很快

主要分析前两种:

    第一种:是“redo log”写满了,要flush脏页。这种情况是innoDB尽量避免的,因为这个时候,整个系统不能再接受更新了,所有的更新必须堵住,如果你从监控上看,这个时候更新数会跌为0

    第二种:内存不够用,要先将脏数据写到磁盘。这个情况是常态,InnoDB用缓存池(buffer pool)管理内存,缓冲池中的内存页有三种状态:

        a>还没有使用的       b>使用了并且是干净页      c>使用了并且是脏页

    当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这个时候只能把最久不适用的数据页从内存中淘汰掉,如果淘汰的是干净页直接释放;如果是脏页就必须flush到磁盘,变成干净页,然后释放

    但是出现这样的情况会明显影响性能:1>一个查询要淘汰的脏页个数太多,会导致查询时间明显变长;2>日志写满,更新全被堵住,写性能跌为0,这种情况对敏感业务是不能接受的。所以Innodb需要有控制脏页比例的机制来尽量避免上面两种情况

InnoDB刷脏页的控制策略:

     首先需要innodb_io_capacity这个参数,告知InnoDB的磁盘能力,这样InnoDB才知道主机的IO能力,才能知道需要全力刷脏页的时候可以多快。一般这个值建议设置成磁盘的IOPS。磁盘的IOPS(磁盘性能指标)可以通过fio这个工具来测试。

    实例:因为没有正确设置这个参数导致的问题比比皆是。之前就有公司一个库的性能问题,说MySQL的写入速度很慢,TPS很低,但是就看主机的IO压力并不大。经过一番排查发现罪魁祸首是这个参数设置出问题,他的主机磁盘用的SSD,但是innodb_io_capacity的值设置为300。于是,InnoDB认为这个系统的能力差,所以刷脏页刷的特别慢,甚至比脏页生成速度还蛮,这就造成脏页累积,影响了查询和更新性能

InnoDB怎么控制引擎按照“全力“的百分比来刷脏页:

 InnoDB刷盘速度参考两个因素:1、脏页比例 2、redo log写盘速度

 参数innodb_max_dirty_pages_pct是脏页比例上限,默认值是75%

 InnoDB在后台刷脏页,而过程是要将内存页写入磁盘。所以无论是你的查询语句在需要内存的时候淘汰一个脏页,还是由于刷脏页的逻辑占用IO并可能影响到更新于巨,都会造成业务端感知到mysql抖一下的原因,所以要合理设置innodb_io_capacity的值不要让他接近75%

InnoDB刷新脏页的策略:

  一旦一个查询请求需要在执行过程flush掉一个脏页时,这个查询就要比平常慢。而在mysql中有这样的一个机制,在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好也是脏页,就顺带一起刷掉,然后继续延续。

  其中innodb_flush_neighbors参数就是控制这个行为的,值为1的时候出现这种‘连坐’机制,值为0的话自己刷自己的

  情景对比:这个优化在机器硬盘的时候很有意义,可以减少很多随机IO。机械硬盘的随机IOPS一般只有几百,所以减少很多随机IO意味着性能大幅度提升

   但是如果SSD这类IOPS比较高的设备,这个时候IOPS往往不是瓶颈,只刷自己的这样的话可以更快执行完必要的刷脏数据操作,减少SQL语句响应时间

  在MYSQL 8.0  innodb_flush_neighbors默认值为0

总结:

  在第二章的WAL的概念,这个机制后续需要刷脏页操作和执行时机。利用WAL技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,但是,由此也带来了内存脏页的问题,脏页会被后台线程自动flush,也会由数据页淘汰而出发flush,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句响应时间过长

表删掉一半,表文件大小不变?

  当删除整个表的时候,可以使用drop table命令回收表空间。但是,当删除数据的场景是删除某些行,这时就遇到了表中数据被删掉,但是表空间却没有被回收

 数据删除流程

     InnoDB里数据都是用B+树的结构组织的

如果删除R4这个记录的话,InnoDB引擎只会把R4这个记录标记删除,如果之后要插入一个ID在300和600直接的记录会复用这个位置。但是磁盘文件的大小并不会缩小

Innodb的数据是按页存储的,如果删掉一个数据页上的所有记录怎么样呢?

 整个数据页就可以被服用了,但是数据页的复用跟记录的复用是不同的,记录的复用只限于符合范围条件的数据,而当整个页从B+树里面摘除以后,可以复用到任何位置。如果相邻两个数据页利用率都很小,系统会把这个页上的数据合在其中一个页上,另一个数据页会被标记为可复用。

如果用delet 命令把整个表数据删除呢? 结果就是所有数据页都会标记为可复用。但是磁盘上,文件不会变小

所以,delet命令其实只是把记录的位置或者数据页标记为可复用,但磁盘文件大小不变,不能回收表空间,这些可以复用但是没有被使用的空间,就会造成空洞,其实插入数据也会这样

插入数据:

  如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂

 

如图由于page A满了再插入一个ID为550的值,就不得不申请一个新的页面page B来保存数据了。页分裂完成后,page A的末尾就留下了空洞(实际上可能不止一个空洞),更新索引上的值

    可以理解为删除一个旧的值,再插入一个新的值,不难理解,这也是会造成空洞

重建表:去除这些空洞,能够达到收缩表空间的目的

      语句: alter table A engine=InnoDB命令来重建表

原理:在MySQL5.6版本开始引入了online DDL

      首先建立临时文件,扫描表A主键所有数据页,然后根据表A记录生成B+树,存储在临时文件中,在生成临时文件过程中,将所有对A的操作记录在一个日志文件(row log)中,临时文件生成,将日志文件操作应用到临时文件,然后用临时文件代替表A的数据文件

  在重建表过程允许对表A做DDL操作,也就是这个Online DDL名字来源

 补充:在这里DDL之前本身要拿MDL写锁,但是这个写锁在真正拷贝数据之前就退化成读锁。退化的原因:就是为了实现Online,MDL读锁不会阻塞增删改查,但是不直接解锁的原因是禁止其他线程对这个表同事做DDL

  因为重建方法都会扫描原表数据和构造临时文件。对于很大的表来说,这个操作是很消耗IO和CPU资源的,因此,如果是线上服务,要很小心控制操作时间,在GitHub上有gh-ost来做

问题:如果一个表t文件大小为1TB,对这个表执行重建表,发现执行完成后,空间不仅没变小,还变成了1.01TB?

           有可能本身没有空洞,在DDL期间刚好有外部的MDL执行,又引入一些空洞,还有一个重要的机制,在重建表,InnoDB不会把整张表占满,每个页留1/16给后续更新用。也就是,其实重建表之后不是最紧凑的,所以如果有这么一个过程:

            t表重建,然后插入一部分数据,但是插入这些数据用掉了预留空间,这种情况,重建一次t表,就出现这种现象

Count为什么这么慢?

    select count(*) from t;

count(*)的实现:

   MyISAM引擎把这个表的总行数存在了磁盘,因此执行count(*)的时候会直接返回这个数,效率很高

  InnoDB引擎它执行count(*)的时候需要把数据一行行从引擎里面读出来,然后累计数。

为什么不一样?

  因为InnoDB支持事物,即使在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表应该返回多少行也是不确定的。比如

   

   因此对于count(*)这个请求来说,InnoDB只好把数据一行行读出来依次判断,可见行才能够用于计算“基于这个查询”的表总行数

 InnoDB的优化:

  InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于count(*)这样操作,遍历哪个索引树得到的结果逻辑都一样。因此,MySQL优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提,尽量减少扫描的数据量,是数据库系统设计的通用法则之一

     

show table status 获取表的信息,这里面也有一个TABLE_ROWS用于显示这个表当前有多少行,这个命令很快但是不能代替count(*)吗?

  实际上,TABLE_ROWS这个采样估算来的,因此它很不准。并且官方文档说误差可能达到40%到50%。

如果缓存系统保存计数:

  比如Redis保存表总行数,加一条数据Redis计数加1,删除减一。

  存在问题:缓存系统可能会丢失更新

  首先,Redis不可能永久留在内存,如果持久化起来,如果刚刚在数据表插入一行,Redis保存值也+1,然后Redis异常重启,重启后刚才的数据就丢失了,解决的办法异常重启后,到数据库里面单独执行一次count(*)获取真实行数,然后写到Redis。

  但是还是存在逻辑上不精确,比如有这么一个页面,要显示操作记录的总数,同时还要显示最近操作的100条记录。

 1、如果先数据库,查到100行记录有最新插入记录,而Redis没加1

 2、如果先Redis,计数里已经+1,查到100行结果里没有最新插入记录

 在这个时序图中,A插入一个记录,T3会话B查询,会显示出新插入的这个记录,但是Redis计数还没加1,出现数据不一致

 因此在并发系统里面,无法精确控制不同线程的执行时刻,因此存在这个操作时许,也就是Redis缓存和数据不同步的问题

  先写数据库,再写缓存有什么问题?先写缓存再写数据库有什么问题?写库成功缓存更新失败怎么办?缓存更新成功写库失败怎么办? 这个问题可以参考一下

在数据库保存计数:

    首先,这解决了崩溃丢失的问题,InnoDB是支持崩溃恢复不丢数据的

    再者查看计数精确的问题:

这个时候,会话B读操作仍然给T3执行的,但是因为更新事务还没提交,所以计数+1这个操作会话B还不可见,所以B看到的结果里,计数数值和记录结果逻辑上是一致的

小结:

   其实把计数放在Redis里面,不能够保证计数和MySQL表里的数据精确一致的原因是,这两个不同存储系统,不支持分布式事务,无法拿到精确一致的视图。而把计数值也放在MySQL中,就解决了一致性视图的问题。InnoDB引擎支持事务,我们利用好事务的原子性和隔离性,可以简化业务开发时的逻辑。

问题:由于事务可以保证中间结果不被其他事务读到,因此修改计数值和插入新纪录的顺序是不影响逻辑结果的。但是从并发系统性能的角度考虑,你觉得这个事务序列里,先插入操作记录,还是更新技术表呢

    因为更新计数表涉及到行锁的竞争,先插入再更新能最大程度减少事务之间的锁等待,提升了并发度

16、order by 是怎么工作的?

 

当在city上创建索引后,Using filesory表示需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer

全字段排序:

工作流程如下:

这个动作在内存中完成还是外部排序,取决于排序所需的内存和参数sort_buffer_size(MySQL开辟的内存sort_buffer大小),如果要排序的数据量大于sort_buffer_size,内存放不下,利用磁盘临时文件辅助排序,外部排序一般使用归并排序算法,MySQL将要排序的数据分成几份,每一份独立排序完再合并成一个有序的大文件,sort_buffer_size越小,分成的份越多

总结:在这个算法过程中,只对原表的数据读了一遍,剩下的操作都是在sort_buffer和临时文件执行,存在的问题,如果查询字段很多,那么sort_buffer放的字段太多,要分成很多个临时文件,排序性能很差

rowid排序

  set max_length_for_sort_data=16 是mysql中专门控制排序行数据长度一个参数,如果单行太大,就换一个算法

整个执行流程:

全字段 VS rowid排序

   体现了MySQL一个设计思想:如果内存多,就多利用内存,尽量减少磁盘访问

还有一个解决办法:alter table t add index city_user_age(city,name,age); 创建联合索引。覆盖索引是指,索引上的信息足够满足查询请求,不再回到主键索引上面取数据

查询过程变成了

explain变成了:

问题:表中有city_name(city,name)这个联合索引,查杭州和苏州两个城市中所有市民的姓名,而且按名字排序,显示前100条记录。
  

   1>这个语句执行时候时候有排序过程嘛?为什么?

      虽然有(city,name)联合索引,对于单个city内部,name是递增的。但是由于这条SQL语句不是单独查一个city的值,同时查了两个,因此需要排序


   2>需要实现一个在数据段不需要排序的方案,怎么实现?

     执行select * from where city="杭州" order by name limit 100;客户端用100个内存数组A保存,   执行select * from where city="suzhou" order by name limit 100;客户端用100个内存数组B保存,因为AB都是有序的,然后用归并思想就可以得到需要的结果


   3>如果分页需求,要改成10000,100,你怎么实现?
     同2>,order by name limie 10100;然后用归并排序拿到另个结果集里,按顺序取10001~10100的name值

18、为什么这些SQL语句性能差异巨大

案例一:条件字段函数操作

    `t_modified` datetime DEFAULT NULL        key `t_modified`(`t_modified`)

      注:对于TIMESTAMP,它把客户端插入的时间从当前时区转化为UTC(世界标准时间)进行存储。查询时,将其又转化为客户端当前时区进行返回。而对于DATETIME,不做任何改变,基本上是原样输入和输出。并且存储范围也不同:

      timestamp所能存储的时间范围为:'1970-01-01 00:00:01.000000' 到 '2038-01-19 03:14:07.999999'。

      datetime所能存储的时间范围为:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'。

总结:TIMESTAMP和DATETIME除了存储范围和存储方式不一样,没有太大区别。当然,对于跨时区的业务,TIMESTAMP更为合适。

    mysql>  select count(*) from t where month(t_modified)=7;

   问题:在生产库中执行这条语句,却发现执行了很久才返回结果

   原因:对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树索引功能。

   分析:

如果SQL是2018-7-1的话引擎会用B+树提供的快速定位能力,但是计算month()函数,放弃了树搜索,优化器可以选择遍历主键索引,也可以选择遍历索引t_modified,优化器对比索引大小后发现,索引t_modified更小,遍历这个索引比遍历主键索引来的更快。导致了全索引扫描

可以通过这样修改,用上索引的快速定位能力

例如:select * from t where id +1 =10000这个SQL语句,虽然不会改变有序性,但是也是不能用ID索引快速定位,手动改成 id=10000 -1 才可以

案例二:隐式类型转换

tradeid字段类型是varchar(32),而参数是整形,所以需要做类型转换

怎么检查数据类型转换?

 select "10" >9 的结果

1、如果规则是 将字符串转换成数字 ,那么就是做数字比较,结果应该是1;

2、如果规则是 将数字转换成字符串 ,那么就是做字符串比较 ,结果是0

对于优化器语句执行相当于:

对索引字段做函数操作,优化器放弃走树搜索功能

案例三、隐式字符串编码转换

当查询id=2的交易操作信息,

并没有使用索引,其原因是两个表字符集不同,一个是utf8,一个是utf8mb4,所以做表连接查询的时候用不上关联字段的索引

注意:字符集utf8mb4是utf8的超集,所以当这两个类型的字符串做比较的时候,MySQL内部先把utf8字符串转成utf8mb4字符集在做比较(按数据长度增加的方向转换)

CONVERT()把输入字符串转成utf8mb4字符集。对索引字段做函数操作,放弃走树搜索功能

修改:

小结对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器放弃走树搜索功能,但是MySQL确实有偷懒的嫌疑,即使简单地把 where id +1=10000改为where id= 10000-1就能够上索引快速查找,也不会主动做这个语句重写

           

问题:假设现在表里面有100万行数据,其中10万行数据的b的值为·  1234567890·,现在执行语句这样写。

MySQL会怎么执行呢?

最理想的是MySQL字段b定义的是varchar(10),肯定返回空。但是MySQL并没有这样做,

这条SQL语句执行很慢,流程是这样的:

  1、在传给引擎执行的时候,做了字符截断,只截了前10个字节,也就是‘1234567890’这个匹配

  2、这样满足条件的数据有10万行

 3、因为select* ,所以做10万次回表,然后每次回表后查出整行,到server层判断,b值都不是'123456789.abcd',返回结果为空

19、为什么我查一行语句,也执行这么慢?

不包括,MySQL数据本身有很大的压力,导致数据库服务器CPU詹欧用铝很高或者IO利用率很高,这种情况下所有语句执行都有可能慢,不属于讨论范围

第一类:查询长时间不返回

 

查询结果长时间不返回

 一般情况下是大概率表t被锁住了。接下来执行 show processlist命令,看看当前语句处于什么运行状态

等MDL写锁:

执行show processlist命令查看:

 

state:waiting for table metadata lock 这个状态标识,现在有一个线程正在表t上请求或者持有MDL写锁,把select语句堵住了

如何复现:

通过查询sys.schema_table_locl_waits这张表,我们可以直接找出造成阻塞的process id,然后把这个连接kill命令断开

等flush

 在表中执行语句查出来状态是 Waiting for table flush;

 原因:现在有一个线程正对表做flush操作,MySQL里面对表做flush操作用法,一般有两个用法:

   flush tables w with read lock;

   flush tables with read lock;

注:FTWRL流程: 1、请求获取相关类型的MDL lock  2、清空query cache中的内容 3、flush table 将当前所有打开的table的fd关闭 4、请求获取全局 table-level lock 5、上全局 COMMIT锁

       当有很大的事务在进行的时候,此时FTWRL的步骤一,步骤二可以完成,但是进行到步骤三的时候,由于表相关的事务正在执行中,相应table的句柄被占用,无法进行flush table操作。

   指定t表的话,只关闭表t;如果没有指定具体表明,则表示关闭MySQL所有打开的表。

    但是正常这两个语句执行起来都很快,触发它们也被别的线程堵住了。所以出现waiting for table flush状态可能情况:有一个flush tables命令被别的语句堵住了,然后它又堵住了我们的slect语句

复现步骤:

 在session A中,故意每行都调用一次sleep(1)这样这个语句默认执行10万秒。然后sessionB的flush tables t命令再去关闭表t就要等session A的查询结束,这样session C就被flush 堵住了

行级锁:

知识补充:

列出表所有字段:SHOW FULL FIELDS FROM tablename;

查看: select @@autocommit;

注:select....lock in share mode 是IS锁(意向共享锁),在符合条件的rows上都加了共享锁,这样的话,其他session可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录直到你这个加锁的session执行完成(否者直接锁等待超时)

       select...for update 走得是IX锁(意向排它锁),即在符合条件的rows上都加了排它锁,其他session无法添加任何s和x锁。 如果不存在一致性非锁定读的话,那么其他session是无法读取和修改这些记录的,但是innodb有非锁定读(快照读并不需要加锁),for update之后并不会阻塞其他session的快照读取操作,除了select ...lock in share mode和select ... for update这种显示加锁的查询操作。

通过对比,发现for update的加锁方式无非是比lock in share mode的方式多阻塞了select...lock in share mode的查询方式,并不会阻塞快照读。

实例:

     mysql> select * from t where id =1 lock inshare mode;

由于id=1这个记录要加读锁,如果这个时候已经有一个事务在这行记录上持有一个写锁,我们select语句就会被堵住。

复现步骤:

行锁复现:

session A启动了事务,占有写锁,还不提交,是导致session B被堵住的原因

可以通过 sys.innodb_lock_waits;表查到

查看到 blocking_pid 线程时造成阻塞的罪魁祸首。KILL Query 623或者kill 623。

          不过,这里不应该显示“KILL QUERY 623”这个命令表示停止623号线当前正在执行的语句,而这个方法是没有用的。因为占有行锁的是update语句,这个语句已结是之前执行完成了的,现在执行KILL QUERY,无法让这个事务去掉id=1上的行锁

           KILL 4 才有效,也就是直接断开这个链接。这里隐含的逻辑就是,链接被断开,会自动回滚到这个链接里面正在执行的线程,也就是放id=1上的行锁

第二类:查询慢

    一致性读,又称为快照读。使用的是MVCC机制读取undo中的已经提交的数据。所以它的读取是非阻塞的。普通的SELECT就是快照读。

       当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT …  LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

     mysql> select * from t  where c=50000 limit 1;

 由于字段没有索引,这个语句只能走id主键顺序扫描,因此需要扫描5W行。

select * from t where id=1  虽然扫描行数是1,但是执行时间却长达800毫秒

复现步骤:

session B执行100万次update语句,生成了100W个回滚日志(undo log),带lock in share mode的SQL,是当前读,因此直接读到100万零1次这个结果,所以速度很快;而select * from t where id=1这个语句是一致性读,因此需要从100万零一次开始,依次执行undo log,执行100万次以后才将1这个结果返回。

输出结果:

扩展:

      innodb的默认事务隔离级别是rr(可重复读)。它的实现技术是mvcc。基于版本的控制协议。该技术不仅可以保证innodb的可重复读,而且可以防止幻读。但是它防止的是快照读,也就是读取的数据虽然是一致的,但是数据是历史数据。如何做到保证数据是一致的(也就是一个事务,其内部读取对应某一个数据的时候,数据都是一样的),同时读取的数据是最新的数据。innodb提供了一个间隙锁的技术。也就是结合grap锁与行锁,达到最终目的。当使用索引进行插入的时候,innodb会将当前的节点和上一个节点加锁。这样当进行select的时候,就不允许加x锁。那么在进行该事务的时候,读取的就是最新的数据。

      在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
所以从上面的显示来看,如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现。

问题:

20、幻读是什么,幻读有什么问题?

  

执行这个语句:

语句命中d=5,对应主键id=5,因此select语句执行完后,id=5会加一个写锁,因为两阶段锁协议,这个写锁会在执行commit语句的时候释放。

假设: 如果id=5这一行加锁

Q3读到了id=1这一行现象,被称为“幻读”。也就是说幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行

这里,幻读的说明:

1、在可重复读下,普通的查询是快照读,不会看到别的事务插入的数据,因此,幻读是当前读才出现的

2、上面B修改的结果,被A之后的select语句用当前读看到,不能称为幻读。幻读仅专指“新插入的行”

假设:如果把扫描的行,都加上写锁,再来看看效果

由于A把所有的行都加了写锁,所以B在执行第一个update语句的时候就被锁住了。等T6的A提交以后,B才能执行,但是。即使把所有的记录都加上了锁,也还是阻止不了新插入的记录。

如果解决幻读?

   行锁只能锁住行。但是新增加记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)

 

 当执行select * from t where d=5 for update的时候,就不只是给数据库中已有的6个记录加上行锁,还同时加了间隙锁,这样就无法再插入新的记录

 

 但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙插入一个记录”这个操作。间隙锁直接不存在冲突关系

比如:

间隙锁之间不互锁:这里B并不会被堵住。因为t并没有c=7这个记录,因此A加的是(5,10)这个间隙锁,而B也是在这个间隙加的间隙锁。它们有共同目标,即:保护这个间隙,不允许插入值,但是他们之间不冲突

   间隙锁和行锁合称 next-key lock,每个next-key lock 是前开后闭区间。也就是类似(-无穷,0]、(0,5]等等

   间隙锁和next-key lock的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”,间隙锁的引入可能导致同一的语句锁住更大的范围,这其实是影响了并发度的。当锁定一个范围键值后,即使某些不存在的键值也会被锁定,无法造成锁定时候插入任何数据。在某些场景下会对性能造成影响

 间隙锁是在可重复读隔离级别下才会生效的、所以,如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现数据和日志不一致问题,需要把binlog格式设置为row,这也是不少公司使用的配置组合\

扩展: 

 除了间隙锁,通过索引实现锁定的方式还存在其他几个较大的隐患:
      1.当query无法利用索引的时候,innodb会放弃使用行锁而改用表锁,造成并发性能降低
      2.当query使用索引并不包含所有过滤条件时候,数据检索使用到的索引建指向的数据可能有部分并不属于该query结果集的行列,但是也会被锁定,因为间隙锁是锁定范围
      3.当query在使用索引定位数据时候,如果使用索引建一样但访问数据行不同时候(索引只是过滤条件一部分),一样被锁

问题:

   

判断一下A,B,C状态

21、为什么我只改了一条数据,加了这么多锁?

因为间隙锁是在可重复读隔离级别下才有效,总结的加锁规则:两个原则,两个优化和一个bug

分析:根据原则1,加锁单位是 next-key lock session A加锁范围就是(5,10】,同时根据优化2,这个是等值查询,next-key退化成间隙锁,因此最终加锁范围时(5,10),C可以因为前开后闭

分析:

但session C要插入就会被session A的间隙锁(5,10)锁住,在这个例子中 lock in share mode 值锁覆盖索引,但是如果是for update就不一样。执行 for update时,系统会认为你接下来更新数据,因此顺便给主键索引上满足条件的行加上行锁

猜你喜欢

转载自blog.csdn.net/ligupeng7929/article/details/89098516