0%

极客时间Mysql总结

13 为什么表数据删掉一半,表文件大小不变

如果把数据行删掉,那么对应到页里面就是把这一行标记为删除了,并没有对空间进行删除。

如果是把整个数据页删掉,那么这个数据页会被重新利用,同样也不会删掉。如果两个页中的数据所占比例都太少,那么会把一个页的数据搬动到另外一个页里面,但是仍然不会让页被清理。

所以就算delete掉了整个表格,所占空间还是不会变少。而且根据上面的描述,随着对数据库表的操作,会有越来越多的空洞产生,所以需要进行”压缩“操作。也就是重建一个新的表,这样空间利用率又达到了最大。

重建有两种办法,一种是锁住要重建的表,然后复制数据到新的表,那新的表就是重建后的表;还有一种是不锁,但是记录下在重建过程中发生的数据变化,然后把这些变化在重建的表中复现一下就可以了。

14 count(*)这么慢,我该怎么办

MyISAM只需要读取磁盘上的数字就可以了,Innodb需要读取数据并且计数,因为innodb是支持MVCC的。

所以,我们只能自己计数了。如果用Redis之类的缓存,那么可能会发生丢失数据或者数据不一致性的问题。

count(*)、count(主键id)、count(字段) 和 count(1)。只要记住count是从返回集合中,一行行拿出来判断,如果是NULL就不计数。count(1)和count(主键)来说,结果是一样的,但是count(1)速度会快一点,因为server只需要对取出的每一行放一个1进去就可以,不需要解析出主键。count(*)做了特别优化,只取行数。

也就是效率来说是count(*)≈count(1)> count(主键)>count(字段)。对,所以其实标题是有问题的,count(*)是不借助其他的帮助下,最快的了。

15 答疑文章(一):日志和索引相关问题

1
2
3
4
5
begin;
insert...
update...
insert...
commit;

这段操作来说,insert之后,redo log buffer(内存)就会有对应的日志记录,然后update之后也有,insert之后也有。当真正commit的时候,会从redo log buffer里面把内容写入到磁盘(其实不是,当时可以这么理解)上的redo log中。当然如果是自动提交的,那么每一句其实最后都是这么一个步骤。

16 “order by”是怎么工作的?

假设排序字段不是索引字段,那么工作流程是:

  1. 首先初始化sort_buffer(内存),放入需要查出来的数据。
  2. 找出符合要求的一整行数据(如何过普通索引的话就从索引中找到,然后再回表;如果主键索引就直接去找,反正找到符合要求的行)
  3. 找完之后按照指定字段进行排序,然后返回结果。

这里第一个问题就是要是内存放不下要排序的数据怎么办?可以使用磁盘文件进行外部的归并排序;或者是只把排序的字段和主键id给扔进sort_buffer里面,最后大不了再回表。这个过程就看你是想节约内存来减少时间,还是希望消耗更多的时间但是节约内存了。

其他的方法还有可以尽量在排序字段上弄索引,这样就天然有序;搞联合索引、覆盖索引进一步提高效率,不过这些都是需要视业务而决定的。

17 如何正确地显示随机消息?

如果有一个业务需求,随机的从mysql中取出三条数据(假设总行数是一万),这个要怎么做呢?

直接使用order by rand() limit 3就可以了,但是这样效率很低,来看看这么做mysql背后的逻辑是怎么样的:

  1. 使用memory引擎创建一张临时表,注意是在内存中的。
  2. 取原表中取出数据,并且为这个数据新增一个rand值。此时取了一万行了。
  3. 再把这个临时表的数据放到sort_buffer里面,又是搬动了一万行。
  4. 排序,并且把拍完序的数据(3条)返回。

总计扫描了20003行。如果是磁盘表的话,会采用优先队列(最大堆)来进行排序,进行加速。

结论是,不管你用了什么方式的临时表,都是很低效的,我们只需要三个,但是实际上却把整个表都排了一遍,下面给出代替方法:

  1. 直接用java生成对应的随机数,然后利用该随机数作为主键取数据。如果数据不均匀,会导致算法不够公平。
  2. 根据行数来取随机数r,并且取第r行的数据。比方法1效率低,但是相对来说更加随机。

记住,在实际中不要使用order by rand()就行。

18 为什么这些SQL语句逻辑相同,性能却差异巨大?

条件函数为什么就没办法用索引了?

因为对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。注意!索引仍然是使用的,只是放弃了在索引树上进行树的搜索,而是改用了使用遍历整棵树的操作。也就是无法使用的是索引快速定位的功能,但是可以使用的是全索引扫描。

类型转换的坑

在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字。所以在转化的时候会在不经意间触发上面那条的规则,导致不走索引优化。

字符编码转化的坑

字符编码转化,其实也是在做函数操作(字符转化),所以也会用不到。

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

CPU和IO压力大,所有的语句都会慢,这个不在讨论范围内。

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

一般基本都是死锁了。show processlist ;可以查询当前的信息,然后根据得到的信息进行对应的kill即可。

还有一种可能是在等flush,因为flush到磁盘上本身是需要获取表的读锁才可以的,而这个动作可能会被sleep阻塞住,而select又会被flush所阻塞。

除了表的锁,还有innodb中特有的行锁也会阻塞,处理方法也是一样的。

第二类:查询慢

上面的第一类其实就是锁的问题,接下来就是慢的问题了,慢的话最优可能的问题显然是没有走索引,导致走了全表扫描,自然就慢。

还有一种可能是,由于MVCC的存在,导致了需要读取的数据需要通过不停追溯才可以读到,这样效率也是很慢的。

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

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

select 语句如果用“当前读 for update”看到,不能称为幻读,而是正常的行为。幻读仅专指“新插入的行”。

幻读带来的问题显然是数据不一致性,发生幻读本质是,我们无法对一个不存在的行加行锁,这就导致了幻读的发生。

解决方法就是加间隙锁,当然原来的行锁肯定也是在的。间隙锁只在可重复读隔离级别下才有效。

行锁分成了读锁和写锁,而且读锁和写锁是冲突的;但是间隙锁之间是不冲突的,因为它们的目的就是不让数据插入,所以彼此之间不会冲突。

行锁+ 间隙锁 = next-key lock,next-key是前开后闭的。

间隙锁存在降低了并发度,而且可能会带来新的死锁(但是很容易检测)

21 *为什么我只改一行的语句,锁这么多?

加锁的原则(next-key是行锁+间隙锁):

  1. 原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
  2. 原则 2:查找过程中访问到的对象才会加锁。比如你给某个非主键索引树上了锁,但是主键索引是不受影响的,还有一点是for update的话,是会锁上主键索引的。
  3. 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
  4. 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
  5. 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

22 MySQL有哪些“饮鸩止渴”提高性能的方法?

连接数太多了怎么办?

处理掉连接着但是没在工作的线程。如果要踢掉,选择那些空闲的线程,而且踢掉的话可能会导致事务回滚,而且客户端不能马上知道,所以在应用层需要通知下。

也可以跳过权限认证,减少消耗,当然极其危险。

慢查询

慢查询一般发生在没有索引、mysql选错了索引或者是语句没写好。一般来说这些都是可以在上线之前通过测试就发现的,如果上线了还存在这些问题,可以通过在线DDL强行修改表结构让它有索引,或者使用query_rewrite功能来重写查询语句。

23 MySQL是怎么保证数据不丢的?

我之前对数据库有个误解,就是crash-safe不是真正的数据不丢失,而是保证了数据的一致性。因为只要没写到磁盘上的,断电都没了;而数据库为了性能不可能每次都去写磁盘的。

binlog写盘过程

事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中,这个文件也其实不是真正写到磁盘里,而是把日志写入到文件系统的 page cache,所以快。而写到磁盘中的操作就叫fsyn,这个是可以通过参数设置的。

redo log buffer 里面的内容,是不是每次生成后都要直接持久化到磁盘呢?不需要。丢了就丢了,没损失,因为没提交。

redolog写盘过程

redolog也是一样的,有内存中的区域redo log buffer,然后有page cache,最后才是通过fsync写入到磁盘中。它还有点特殊,其他线程在提交的时候,可能会把那些还没提交事务的redo log buffer里面的东西也写出去。

我们一般会设置成“双 1”配置,即每次事务提交时都将 redo log 直接持久化到磁盘每次提交事务都对binlog执行 fsync,所以在一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

当然还可以通过延时操作,让一些零散的操作一起写盘,提高效率。

24 MySQL是怎么保证主备一致的?

首先主从备份是用binlog进行的,而binlog有三种模式,这种应该是大家都知道的。

之前理解的是备库有两个线程,一个负责与主库建立连接,另外一个负责执行sql语句。当然目前技术可以让多个线程同时执行sql语句来加速。

statement的最大弱点就是,可能会导致主从之间的数据不一致。比如同一条语句,主从分别走了不同的索引,就会导致不同的结果。当然如果用了Row格式就不会有这个问题,而且row格式的话可以方便的进行insert、delete、update的数据恢复,因为它记录了数据变化过程,而statement显然做不到。

目前主流起码有两台master,为了避免循环复制,会在binlog中记录下server id,这样如果发现是自己的,那就直接丢弃。

25 MySQL是怎么保证高可用的?

主库写入到binlog时间是T1,然后从库执行完对应的语句是T2,这个T2-T1就是主从延迟,这个延迟主要不是因为网络而造成的。主要还是从库的机器性能差、备库的压力大、事务时间久。

如果要做主从一致性,那么我们可以检测当主从时间差距小于某个值的时候,把主库设置成read-only状态,并且从库继续执行,直到两者完全同步之后,再把从库切换成主库。显然这里是有一小部分时间不可用的。

如果希望高可用,那么其实是可以强制把数据库直接切换,但是这样会导致严重的数据不一致(如果是statement则会不一致得比row严重,所以这里也体现了row的优越性),所以特别特别不推荐这么做。

所以为了高可用,还是让主从延迟尽可能的小。

26 备库为什么会延迟好几个小时?

如果备库的执行速度,还不如主库的写入速度,那么就会导致数据有差异,并且越来越大。

那怎么提升备库的执行速度呢?多线程并发执行呗!但是牵扯到了多线程,就势必会要聊一下多线程可能会出现的问题,比如明明两个事务是有先后顺序的,在单线程下是不会有问题的,但是到了多线程下,这两个事务被分配给了不同的线程,而线程之间的执行顺序又是不一定的,就会出现数据不一致的情况。

所以要指定相应的规则,下面是基本的规则:

  1. 不能造成更新覆盖。这就是说如果有语句更新了相同的一行,那么它们必须被分配到同一个线程里面(worker)
  2. 一个事务必须在一个worker里面。

换言之,只要两个事务不更新同一行,就可以并行执行。通过解析row格式的binlog,可以知道修改了哪些行,这样就可以实现不同的行对应不同的worker了,缺点是比较消耗CPU资源,因为要进行解析操作。

redo log用了组提交,组提交的事务之间肯定不会修改同一行。那么只需要在主库写入binlog的时候进行标记,这样到了从库中直接把有相同标记的语句分发给不同的worker去执行就可以了。

27 主库出问题了,从库怎么办?

一般是双主设置,然后当一个主库出问题的话,它的备库就会顶上来接替它。那么接下来那么多的从库就都会去这个备库上获取信息。问题是从哪里开始呢?当然是从从库的某一个位点开始喽。这个位置可以通过时间来计算得到,但是为了防止最极端的情况,可以让时间稍微前推。但是如果往前推,可能会导致某些插入删除语句有问题(查询和更新是不会有问题的),所以可以选择跳过这些错误。

然后有了新的方法,MySQL 5.6 版本引入了 GTID来解决这个问题。GTID是全局唯一的一个数字,是在事务提交的时候进行分配的。每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”,这样切换就非常方便了,因为现在能够自动判断出应该从哪里开始。

所以GTID本质上还是找位点,不过这个位点是自动找的,就方便很多了。