WRY

Where Are You?
You are on the brave land,
To experience, to remember...

0%

MySQL库知识点汇总

Version 2 对本文内容重新进行了总结整理。

整体面貌

数据库引擎

数据库的存储引擎,有如下两种,目前主要使用的是InnoDB引擎,之后的内容默认都将是InnoDB数据库的知识

InnoDB MyISAM
底层实现 B+树
锁粒度 表锁、行锁 表锁
事务 支持事务的四种原则,四种隔离级别,默认RR 不支持

组成部分

Mysql数据库的主要组成部分如下图:

整体架构如下,包含后台线程和内存缓冲池。

后台线程

InnoDB是多线程的模型,不同线程负责处理不同的任务,常见的有以下几种:

  • Master Thread:将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新,合并插入缓冲(INSERT BUFFER),UNDO页的回收等,以及负责调度其他各线程。
  • IO Thread :对InnoDB使用的大量Async IO 发起的IO请求的回调处理
  • Purge Thread:收回已经使用并分配的 undo 页(事务被提交后,其所使用的undo log可能不再需要),减轻Master Thread 的工作。
  • Page Cleaner Thread:将之前版本中的脏页的刷新操作都放入到单独的线程中完成,减少用户查询线程的阻塞,减轻Master Thread 的工作。

内存缓冲池

InnoDB是基于磁盘存储的,并按照页的方式管理其中的纪录。由于CPU和磁盘速度之间的严重不匹配,所以引入内存缓冲池技术,通过内存的速度来弥补的磁盘速度较慢对数据库性能的影响。

  • 读取页时,若该页在缓冲池中,直接读取,否则先将该页从磁盘中读入缓冲池,再读取;
  • 修改页时,首先修改缓冲池中的页,通过Checkpoint机制刷新回磁盘。(脏页就是指那些缓冲池中的页和磁盘上的页数据不一致的页。)

具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、AHI等。下图是InnoDB中内存的结构情况:

通常来说,不同类型的存储引擎下的缓冲池都是通过LRU(最近最少使用)算法来管理页(数据页和索引页,其他页不需要LRU算法进行维护),而InnoDB在LRU上做了改进,新读取到的页并不是直接放入LRU列表的首部,而是在LRU列表长度的5 / 8处,也就是它认为前5 / 8是热点页,这种改进可以应对诸如select *等仅查询少数几次的非热点数据替换掉热点数据的情况。额外补充,InnoDB允许有多个缓冲池实例,每个页根据哈希值平均分配到不同的缓冲池实例中,减少了数据库内部的竞争,加强了并发能力。

InnoDB的内存区域除了有缓冲池,还有重做日志缓冲、额外内存池。

checkpoint技术

当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先修改重做日志,再修改物理页。当由于发生宕机导致数据丢失时,通过重做日志来完成数据的恢复,这也是事务ACID中D(Durability持久性)的要求。但是缓冲池受制于操作系统内存不可能无限大,重做日志如果很大,维护困难且宕机之后重新应用重做日志的时间会非常久,因此,引入检查点(checkpoint)技术主要为了解决以下三个问题:

  • 缩短数据库的恢复时间

    当数据库发生宕机时,数据库不需要重做所有的日志,因为CheckPoint之前的页都已经刷新回磁盘。故数据库值需要对checkpoint后的重做日志进行恢复,这样就大大缩短了恢复时间。

  • 缓冲池不够用,将脏页刷新到磁盘

    当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页面为脏页,那么需要强制执行checkpoint,将脏页也就是页的最新版本刷回磁盘。

  • 重做日志不够用时,刷新脏页面

    当前事务数据库系统对重做日志的设计都是循环使用的(默认redo log buffer是2G),也就是旧日志需要被覆盖重用,因此必须强制产生checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

在InnoDB内部,有两种checkpoint:

  • Sharp checkpoint:数据库关闭时将所有的脏页都刷新回磁盘。

  • Fuzzy checkpoint:在数据库运行时,每次只刷新部分脏页,而不是刷新所有的脏页回磁盘。

关键特性

  • 插入缓冲

    对于非主键且非唯一的索引的插入或更新操作,先判断插入的索引页是否在缓冲池中,若在,直接插入,否则先放入一个Insert Buffer对象中,假装这个索引已经插入到叶子节点。实际上需要等待一段时间后,Insert Buffer和辅助索引页子节点才会执行merge操作(在一个索引页中),这时才真正写入磁盘。带给InnoDB性能上的提升。

  • 两次写

    存在一种情况,InnoDB正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,之后就发生了宕机,这种情况称为部分写失效。因此需要两次写技术,在应用重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再逐条运行重写日志中的SQL语句,这就是doublewrite,避免了因为部分写失效而导致数据丢失的情况。

  • 自适应哈希索引

    InnoDB自动为热点页在B+树索引的基础上建立哈希索引,DBA也无法控制

  • 异步IO

    用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作完成。这个过程可以优化的,即将多个IO合并为1个IO,减少IO操作次数

  • 刷新邻接表

    当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,那么一起进行刷新。

文件

日志文件

  • 错误日志

    记录MySQL运行错误、警告信息,可以通过这个来优化数据库参数配置。

  • 慢查询日志

    记录运行时间超过某个阈值的所有SQL语句,通过这个来做SQL层面的优化。

  • 二进制日志

    记录对MySQL数据库执行更改的所有操作,但是不包括查询语句。用于数据库的恢复、复制和审计(判断是否有SQL注入攻击)。

InnoDB文件

  • 表空间文件

    管理InnoDB存储引擎的存储,包括索引页和数据页等。

  • 重做日志文件

    当实例或介质失败时,如数据库由于所在主机掉电导致实例失败,InnoDB会使用重做日志(redo log)恢复到掉电前的时刻,以此来保证数据完整性。

    与二进制日志的区别 | | 二进制日志(binlog) | 重做日志文件(redo log file) | | -------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 实现方式 | MySQL数据库的Server层实现,记录所有与数据库有关的各种引擎的日志记录 | InnoDB存储引擎层实现,仅记录InnoDB有关的事务日志 | | 内容形式 | 逻辑日志,循环写入,关于事务的具体操作,是一条条SQL语句,非幂等 | 物理格式日志,追加写入,记录每个页的更改的物理情况,是幂等的 | | 写入时间 | 仅在事务提交前进行提交,只写盘一次,不论这时该事务有多大 | 事务进行的过程中,不断有重做日志条目写入 | | 适用场景 | 主从复制和数据恢复 | 崩溃恢复 |

    幂等的意思是\(f(f(x)) = f(x)\),即一个操作一次执行与多次执行的效果一致。

InnoDB逻辑存储结构

表空间 -> 段 -> 区 -> 页 -> 行数据

  • 表空间:默认情况下InnoDB中有一个共享表空间ibdata1,所有数据都存放在这个表空间中。
  • 段:包含数据段(B+树叶子节点)、索引段(B+树非叶子节点)、回滚段等。
  • 区:每个区的大小是1MB。
  • 页:也叫块,是InnoDB磁盘管理的最小单元,默认每个页的大小是16KB。页结构有一个页目录(Page Directory),按照索引键值顺序存放页相对位置(页指针),这样可以利用二分查找迅速找到行记录的指针。
  • 行数据:每个页最多存放16 * 1024 / 2 - 200 = 7992行数据。B+索引只能找到一条行数据所在的页,并不能找到具体的物理位置。数据库需要把页载入到内存,然后通过页目录二分查找。

分区

对于访问数据库的应用来说,逻辑上只有一个表或一个索引,在物理上可能由数十个物理分区组成。

MYSQL仅支持水平分区(切分),不支持垂直分区。且其分区是局部分区索引,即一个分区中既存数据又存索引。

当前MySQL支持的分区:

  • RANGE分区:行数据基于一个给定连续区间的列值来分区
  • LIST分区:根据一个给定的离散列值来分区
  • HASH分区:根据用户自定义的表达式返回值来分区
  • KEY分区:根据MySQL数据库提供的哈希函数来分区

对于OLAP(在线分析处理),该应用大多需要频繁查询一张很大的表,分区可以很好的提升查询性能;对于OLTP(在线事务处理),该应用大多只是通过索引返回几条记录,而分区与否其对应的B+树高基本还是2-3,也就是都需要2-3次IO操作,因此使用分区不一定能带来性能上的提升。

索引

索引是存储引擎用于快速查找记录的一种数据结构,要额外占用内存,在加快检索数据的同时,插入、删除、修改数据都需要DBMS动态更新索引结构,占用一定的系统开销。

在InnoDB中的表是索引组织表,即每张表都有主键,都是根据主键顺序组织存放的。主键的生成规则如下:

  1. 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有多个,将选择建表时第一个定义的非空唯一索引作为主键;
  2. 否则InnoDB自动创建一个6字节大小的指针作为主键。

数据结构

B+ Tree

非叶子节点存放的是索引数据,叶子节点存放的是行数据,并且是顺序存放的(叶节点间是双向链表结构)。

  • 插入节点

    • 叶子节点未满,直属索引节点未满。直接插入叶子
    • 叶子节点满,直属索引节点未满。插入叶子,更新直属索引节点
    • 叶子节点满,直属索引节点满。先动叶子,更新直属索引节点,再更新上一层的索引节点
  • 删除节点

    定义填充因子,0.5是最小值,下面的满指的是到达填充因子。

    • 叶子节点未满,直属索引节点未满。直接删除叶子中的值
    • 叶子节点满,直属索引节点未满。删除叶子中的值,与其兄弟节点合并,更新直属索引节点
    • 叶子节点满,直属索引节点满。删除叶子中的值,与其兄弟节点合并,更新直属索引节点,再更新上一层的索引节点和他的兄弟节点

数据库中的索引一般是在磁盘上,每访问一个节点,就对应着一次硬盘 IO 操作,数据量的大的情况下,索引无法一次装入内存,B+树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。

面试实战问题

为什么是B+树,而不是其他的数据结构?

  • 每个节点的索引数据量的大小的角度,
  • 树高是否一致,查询写入是否稳定,
  • 顺序逆序访问,叶子结点双向链表

Hash

InnoDB中哈希冲突时采用拉链法,哈希函数为除法散列,除数的取值为略大于2倍的缓冲池页数量的质数,如当前参数innodb_buffer_pool_size大小为10M,则共有640个16KB大小的页。对于缓冲池页内存的哈希表来说,需要分配640 * 2 = 1280个槽,大于1280的最小质数为1399,所以在启动时会分配1399个槽的哈希表,用来哈希查询所在缓冲池中的页。

AHI,InnoDB自动为热点页在B+树索引的基础上建立哈希索引。

全文索引

底层实现为倒排索引。全文索引建立过程如下:

倒排索引结构

id word ilist(documentId,position)
1 cold (1:6), (4:8)
2 days (3:2), (6:2)
3 hot (1:3), (4:4)

FTS Index Cache

FTS Index Cache(全文检索索引缓存)是一棵常驻于内存中的红黑树,其根据(word,ilist)进行排序。

  • 在事务提交的时候将分词写入到FTS Index Cache中

  • 批量更新到Auxiliary Table,为了提高性能不会插入一条数据立刻更新到Auxiliary Table。进行批量更新的几种情况:

    • 全文检索索引缓存已满,默认大小为32M,可以通过修改innodb_ft_cache_size来改变FTS Index Cache的大小
    • 关闭数据库的时候,将FTS Index Cache中的数据库会同步到磁盘上的Auxiliary Table中
    • 当对全文检索进行查询时,首先会将在FTS Index Cache中对应的字段合并到Auxiliary Table中,然后在进行查询
    • 当数据库突然宕机时,可能会导致一些FTS Index Cache中的数据未同步到Auxiliary Table上。数据库重启时,当用户对表进行全文检索时,InnoDB存储引擎会自动读取未完成的文档,然后进行分词操作,在将分词的结果放入到FTS Index Cache中。innodb_ft_cache_size的大小会影响恢复的时间

与Insert Buffer功能相像,区别在于Insert Buffer是一个持久的B+树结构的对象。

Auxiliary Table,word保存在辅助表中,InnoDB中为了提高全文检索的并行性能,总共有六张,每张表根据word的Latin编码进行分区。

索引到数据

InnoDB表是索引组织表,根据主键索引构造一棵B+树,叶子节点顺序存放整张表的行记录数据,但B+树索引只能找到一条行数据所在的,并不能找到具体的物理位置,数据库需要把页加载到内存,每个页中的页目录上有行数据的相对位置(这些记录指针称为槽),一个槽中有多条记录,同时在槽中的记录是按照索引键值存放,因此,可以按键值二分查找,接着在槽内部根据next_record指针来具体查找。(先B+树查找,再二分查找,再顺序查找)

自己只能结合InnoDB逻辑存储结构部分的内容理解到页内二分查找的地步。TODO对于顺序查找的逻辑不理解

索引类型

聚集索引辅助索引是根据叶子节点是否有完整的数据行来进行区分的;因为辅助索引需要遍历两次B+树,所以InnoDB引入了覆盖索引,其叶子节点直接和数据行关联,但是因为覆盖索引的双向链表的的数据行顺序并不是他们在磁盘上的顺序,因此遍历覆盖索引会引入大量的随机读写,降低性能。此外针多个列,还提出了联合索引,将多个列作为一个key进行排序。

聚集索引

按照每张表的主键构造一颗B+树,数据页(叶子节点)上存放的是的完整的行记录,而在索引页(非叶子节点)中存放的是键值及指向数据页的偏移量。因为页是通过双向链表链接,按照主键顺序排序,所以聚集索引的存储是逻辑上连续而非物理上连续。

辅助索引

又称非聚集索引,叶子节点存放的是键值和相应行数据的聚集索引键。当通过辅助索引来寻找数据时,InnoDB 存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。 如果在一个高度为 3 的辅助索引树中查找数据,那需要对这棵辅助索引树遍历 3 次找到指定主键,如果聚集索引树的高度同样为 3,那么还需要对聚集索引树进行 3 次查找,最终找到一个完整的行数据所在的页,因此一共需要 6 次逻辑 IO 访问最终的一个数据页。

覆盖索引

从辅助索引中就能得到查询结果,而不需要查询聚集索引中的记录,从而减少大量的IO操作。常用于某些统计问题。如果table上有主键索引也有辅助索引,那么select count(*) from table优先走辅助索引。通过explain查看执行计划中的Extra字段,如果有Using index代表使用了覆盖索引。

优化器选择不使用辅助索引的情况

select * from table where id > 100 and id < 10000,这种需要行记录全部字段数据的情况,尽管走辅助索引数据是有序存放的,但是根据其叶指针查找的行记录则是无序的,因此变成了磁盘上的离散读操作,所以优化器会选择直接走主键索引。但如果访问的数据量很小的话,优化器会走辅助索引。

联合索引

对表上的多个列进行索引。从本质上说,联合索引也是一棵B+树,不同的是联合索引的键值数量大于等于2。如果一张表上有联合索引(a, b, c),那么找(a), (a, b), (a, b, c)都会走联合索引,但是找(b), (c), (b,c), (a,c)无法避免额外排序。

纠正本人之前的一个误解。联合索引仍是一棵B+树,它的key可以理解是(a,b,c)的一个tuple结构,来进行排序的。

优化策略

索引查询优化

  • Multi-Range Read(MRR)优化

    将查询得到的辅助索引键值存放于一个缓存中(这时缓存中的数据是根据辅助索引键值排序的),按照主键进行排序,并按照主键顺序来访问实际的数据文件。通过在缓冲池中的提前排序,将随机访问转化为较为顺序的数据访问,减少了磁盘的随机访问。

    此外,MRR还可以将某些范围查询拆分为键值对批量查询来提高查询效率。如select * from table where k1 >= 100 and k1 <= 1000 and k2 = 200将查询条件拆分为(100,200), (101,200), ... , (1000, 200)避免了无用数据被取出。

  • Index Condition Pushdown(ICP)优化

    MySQL在取出索引的同时,就会进行WHERE条件的过滤(这个条件是索引可以覆盖到的范围),然后再去获取数据。也就是将WHERE的部分过滤操作放在了存储引擎层,大大减少上层SQL层对记录的索取。

设计思路

  • 表必须有主键,推荐使用无符号自增列作为主键。
  • 优先考虑基于现有索引添加覆盖索引减少IO,选择率大的字段放在最前面。
  • 避免冗余索引、重复索引,单张表的索引数量不超过5个,单个索引中的字段不超过5个。
  • 不在低基数列上建立索引,例如“性别”,因为相比全表扫描不一定有性能优势。
  • 对过长的Varchar段建立索引。建议优先考虑前缀索引,或添加MD5伪列并建立索引。

排查思路

一条SQL查询语句执行很慢,可能是什么原因?

  • 一直都执行的很慢

    • 没有用上索引

      字段没有索引、有索引但是没有用上、函数操作导致没有用上索引等情况,用explain分析SQL语句。

    • 优化器选错索引

      优化器是通过采样来预测索引的基数,可能出现错误,不走索引走全表扫描。可以通过force index来强制指定索引。

  • 大多数情况正常,偶尔很慢

    • 执行的时候,遇到其他事务在占用锁
    • 数据库在刷新脏页,比如redo log写满了需要同步到磁盘。

锁机制用于管理对共享资源的并发访问,提供数据的完整性和一致性,是数据库系统区别于文件系统的一个关键特性。InnoDB根据每个事务访问的每个页对锁进行管理,采用的是位图的方式,也就是说,不管一个事务锁住页中一个记录还是多个记录,其开销通常是一致的。

锁类型

  • 行级锁

    • 共享锁(S Lock),允许事务读一行数据。
    • 排他锁(X Lock),允许事务删除或更新一行数据。

    InnoDB所有的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间。在默认的RR级别,InnoDB会自动为增删改操作的行加X锁,读不加锁(只有在Serializable级别会为读加S锁),并在事务提交或回滚后自动释放锁。

  • 表级锁

    • 表级共享锁(S Lock),用读锁锁表,会阻塞其他事务修改该表数据。
    • 表级排他锁(X Lock),用写锁锁表,会阻塞其他事务对该表的读和写。
    • 意向共享锁(IS Lock),事务想要获得一张表中某几行的行共享锁。
    • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。
    • 自增锁(AOTU-INC Lock),一种特殊表锁,用于保证自增长计数器的正常赋值。

    自己理解上的误区。意向共享或意向排他锁一个表都有一个,不会根据行范围的不同而有多个,不然和行锁就没有区别了。

    • 行锁是在行上加锁,当需要对表加锁,如果没有意向锁就需要逐行扫描,影响效率,因此提出了意向锁的概念。

    • 为了提高并发度,在为行数据加共享 / 排他锁之前,InooDB 会获取该数据行所在数据表的对应意向锁,用户无法手动操作意向锁。如此在加表级锁的时候就不需要全表扫描了。

    • 通过意向锁实现了表锁和行锁共存的多粒度锁机制

锁的兼容性 表级排他锁(X) 意向排他锁(IX) 表级共享锁(S) 意向共享锁(IS)
表级排他锁(X) N N N N
意向排他锁(IX) N Y(不同行时可以兼容) N Y
表级共享锁(S) N N Y Y
意向共享锁(IS) N Y Y Y

按照上面的兼容性,如果不同事务之间的锁兼容,则当前加锁事务可以持有锁,如果有冲突则会等待其他事务的锁释放。由于InnoDB支持的是行级别的锁,因此意向锁不会阻止除了全表锁定请求之外的任何锁请求,其主要目的是显示事务正在锁定某行或者正意图锁定某行。

通过在INFORMATION_SHECMA架构下的表INNODB_TRX,INNODB_LOCKS,INNODB_LOCK_WAITS,用户可以简单的监控当前事务并分析可能存在的锁问题。对应的SQL语句为 select * from information_shecma.INNODB_TRX;

使用表锁的场景:

对于InnoDB表,在绝大部分情况下都应该使用行级锁,在默认的RR级别,InnoDB会自动为增删改操作的行加X锁,读不加锁(只有在Serializable级别会为读加S锁),并在事务提交或回滚后自动释放锁。

只有在如下情况使用表级锁:

  1. 事务需要更新大部分或全部数据,表又比较大。如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
  2. 事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM引擎。使用方式如下:

1
2
3
4
5
6
7
8
9
# 仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁
set autocommit = 0;
lock tables t1 write, t2 read;
...
# do something with t1 and t2
...
# 先commit再unlock,因为unlock tables会隐含地提交事务
commit;
unlock tables;

对自增长的补充

为了保证自增长的并发效率,当对含有自增长计数器的表进行插入操作时,执行select max(auto_inc_col) from table for update来得到计数器的值,对该值加1,赋予自增长列。这种实现方式称为自增锁(AUTO-INC Lock),是一种特殊表锁,该锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。

自增锁对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入的完成(虽然不用等待事务的完成)。于是在后续的版本中,InnoDB提供了一种轻量级互斥量的自增长实现机制,比自增锁更高效,但是也存在一些问题,如因为并发插入的存在,每次插入时,自增长的值可能不是连续的。在InnoDB中,自增长值的列必须是索引,且必须是索引的第一个列,否则会抛出异常。

锁实现

行锁

InnoDB中的行锁是通过对索引数据上的记录加锁实现的。有3种行锁算法,分别是:

  • Record Lock(行锁):单个行记录的锁,锁数据,不锁Gap。
  • Gap Lock(间隙锁):锁定一个范围,不锁数据,仅锁数据前面的Gap。
  • Next-key Lock:行锁与间隙锁的结合,锁数据,锁Gap。

默认情况(隔离级别为RR),InnoDB对于行的查询都是采用Next-key Lock算法,例如一个索引有10,11,13,20这四个值,那么该索引可能被Next-Key Lock的区间为:(-无穷大,10](10, 11](11, 13](13,20](20,+无穷大],还有一种Previous-key Locking的技术,其区间为左闭右开。

Gap Lock解决了幻读的问题,但可能会引入死锁的问题,对应的解决方案是ROW,具体不理解。

普通索引加锁

当查询的索引是辅助索引或是多列唯一索引中的某一列,用Next-key Lock进行锁定。

举个例子,一张表z中,列a是主键(非空唯一),列b是一般键(可空可不唯一),即辅助索引,已有行数据(1, 1),(3, 1),(5, 3),(7, 6),(10, 8)

时间 会话A 会话B 会话C
1 begin;
2 select * from t where b = 3 for update; begin; begin;
3 select * from z where a = 5 lock in share mode; insert into z select 8, 6;
4 insert into z select 4, 2; insert into z select 2, 0;
5 insert into z select 6, 5; insert into z select 6, 7;

会话A中通过索引列b进行查询,需要对两个索引分别进行如下锁定,因此会话B中SQL语句全部阻塞,会话C中全部可以立即执行。 * 对于聚集索引,其仅对列a等于5的索引加上Record Lock,即范围为a∈[5];

  • 对于辅助索引,对键值3加上Next-Key Lock,作用范围(1, 3],且对下一个键值6,加上Gap Lock,作用范围(3, 6),总范围为b∈(1, 6);(b列没有唯一属性,不锁区间,可能造成b=6的新数据增添进来。)

唯一索引加锁

当查询的索引是唯一索引时,InnoDB会将Next-key Lock降级为Record Lock。

举个例子,在一张表t中,列a是主键,已有行数据1,2,5,执行如下操作:

时间 会话A 会话B
1 begin;
2 select * from t where a = 5 for update;
3 begin;
4 insert into t select 4;
5 commit; #成功,不需要等待
6 commit

在会话A中首先对a = 5进行X锁定,而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2, 5)这个范围,这样在会话B中插入值4不会阻塞,可以立即插入并返回。

非索引加锁

当查询不使用索引时,对主键索引全表加上Next-key Lock,相当于锁表了。

外键锁

TODO:并不理解

对于一个外键列,如果没有显示地对这个列加索引,InnoDB会自动对其加一个索引,这样可以避免表锁。对于外键值的插入或更新,首先需要select 父表 lock in share mode,即主动对父表加一个表级S锁,以此来保证与父表数据的一致性。

加锁读方式

举个例子,在一张表中,现在只有一条id = 1的行数据,执行如下操作:

时间 会话A 会话B
1 begin;
2 select * from parent where id = 1;
3 begin;
4 update parent set id = 3 where id = 1;
5 select * from parent where id = 1;
6 commit;
7 select * from parent where id = 1;
8 commit;

一致性非锁定读

对应乐观锁(例如MVCC)、快照读。直接读取一个快照数据,而不需要等待访问的行上X锁的释放,在事务隔离级别RC和RR下,InnoDB使用非锁定的一致性读。在上述例子中,在不同的隔离级别下

  • 在RC中读取被锁定行的最新一份快照数据。对于上述例子,步骤5,查询结果非空;步骤7,查询结果为空
  • 在RR中读取事务开始时的行数据版本。对于上述例子,步骤5,查询结果非空;步骤7,查询结果非空。

一致性锁定读

对应悲观锁、当前读。在默认配置下,事务的隔离级别为RR,InnoDB的select操作使用一致性非锁定读,但在某些情况下(转账交易等),用户需要显式地对数据库读取操作加锁以保证数据逻辑的一致性。这要求数据库支持加锁语句,即使是对select的只读操作。InnoDB对于select支持两种一致性等待锁定读:

  • select ... for update,对读取的行记录加一个X锁(事务要获取某些行的 X 锁,必须先获得表的 IX 锁)
  • select ... lock in share mode,对读取的行记录加一个S锁(事务要获取某些行的 S 锁,必须先获得表的 IS 锁)

锁问题

脏读

先区分脏页与脏数据:

  • 脏页:在缓冲池中已经被修改的页,但是还没有刷新到磁盘中
  • 脏数据:事务对缓冲池中行记录的修改,还没有被提交。

脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据。举个例子,在一张表中只有1个行数据,值为a = 1:

时间 会话A 会话B
1 set @@tx_isolation = 'read-uncommitted'
2 set @@tx_isolation = 'read-uncommitted'
3 begin;
4 select * from t ; # a = 1;
5 insert into t select 2;
6 select * from t # a = 1, 2;

在会话A中,事务并没有提交,但是在会话B中的两次select操作取得不同的结果,即产生了脏读,违反了事务的隔离性。

脏读看似毫无用处,但在特殊情况也可以使用,如replication环境中的slave节点,并且在该slave上查询并不需要特别精确的返回值。

不可重复读

不可重复读指的是在一个事务内多次读到的数据是不一样的情况。

区分不可重复读与脏读

  • 脏读是在当前事务内读取到未提交的数据
  • 不可重复读是在当前事务内读取到其他事务已提交的数据,但前后不一致。

举个例子,在一张表中只有1个行数据,值为a = 1:

时间 会话A 会话B
1 set @@tx_isolation = 'read-committed'
2 set @@tx_isolation = 'read-committed'
3 begin; begin;
4 select * from t ; # a = 1;
5 insert into t select 2;
6 commit;
7 select * from t # a = 1, 2;

在事务隔离级别RC下,InnoDB仅采用Record Lock,也就是在第一次查询时仅锁住了范围[1],允许行数据2的插入,于是在会话A中一个事务的两次读取结果因为另一个事务的修改而不一致,即产生了不可重复读,违反了事务的一致性。

一般来说,不可重复读也是可以接受的,Oracle,MS SQL Server的隔离级别是RC,即允许不可重复读的现象。

幻读

在MySQL官方文档中将不可重复读定义为幻读,也就是说他们本质是一类问题,即当前事务还未结束前读取到了其他事务提交之后的结果。

非要强行说区别的话(如果面试官的知识体系跟我们不一样的话),那么:

  • 不可重复读强调 update, delete
  • 幻读强调 insert

InnoDB采用Next-Key Lock算法避免出现幻读,对于上个例子,select * from t,其在范围(-无穷大,+无穷大)上加了X锁,因此其他事务执行的插入操作会被阻塞。

丢失更新

丢失更新指的是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据不一致。

在当前数据库的任何隔离级别下,即使是RU,对于行的DML操作,需要对行或其他粒度级别的对象加锁,因此理论上不会出现丢失更新的情况,但考虑这种情况,针对多用户计算机系统环境,出现以下情况,就会发生丢失更新:

时间 会话A 会话B
1 begin;
2 查询余额a = 1000,放入本地内存,显示给终端用户A begin;
3 update a = 100; 查询同一行数据a = 1000,显示给一个终端用户B
4 commit; update a = 500;
5 commit;

最终的结果是两个事务都提交成功,但是最终的余额却不是1000 - 100 - 500 = 400。

要避免丢失更新发生,需要让事务在这种情况下的操作变成串行化,即在查询时,对用户读取的记录加上一个排他锁,会话B必须等待会话A完成之后方可执行

死锁

死锁产生的四个必要条件:

  • 互斥:一个资源每次只能被一个进程使用。
  • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不抢占:进程已获得的资源,在没事用完之前,不强行剥夺。
  • 循环等待:多个进程之间形成一种互相循环等待资源的关系。

以上四个条件,任何一个被破坏,就有可能产生死锁。

MyISAM中仅有表锁的概念,所以不会发生死锁问题,而InnoDB是逐行加锁的,容易产生死锁,在发生死锁时,InnoDB会自动检测并回滚持有最少行锁的事务来解决死锁问题。但很多时候需要人工解决,最好的方式还是从根源上避免死锁的产生,对此有如下几种做法:

  • 加锁顺序一致,尽量一次性锁定所有所需的数据行。
  • 控制事务大小,尽量减少锁定数据量和锁定时间长度。
  • 减少范围更新,尤其非主键/非唯一键上的范围更新。(即对辅助索引的更新是Next-Key Lock级别,锁强度最大)
  • 减少锁定资源,SQL语句的where过滤尽量走索引。(避免直接读缓冲池中的数据页导致的低效)

三级封锁协议

  • 一级封锁协议:事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁,可以解决丢失更新问题。
  • 二级封锁协议:在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁,可以解决读脏数据问题。
  • 三级封锁协议:在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束才能释放 S 锁,可以解决不可重复读的问题。

事务

事务指的是满足ACID的一组操作,数据库事务的整个流程如下:

事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页,然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使用undo log进行事务回滚。事务执行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏页继续按照checkpoint进行落盘。如果此时发生了崩溃,则只使用redo log恢复数据。

事务的特性

事务遵循ACID四大原则:原子性、事务性、持久性、一致性

  • 原子性(atomicity):事务的所有操作,要么全部完成,要么全部不完成。通过redo log完成。
  • 一致性(consistency):由约束一致性(创建表时所指定的外键、唯一索引等)和数据一致性(由其他三个特性共同保证的结果)保证。通过undo log实现。
  • 隔离性(isolation):一个事务提交前的任何操作,其他事务都无法看见。通过锁实现。
  • 持久性(durability):事务一旦提交,其结果就是永久性的。通过redo log实现。

事务实现

InnoDB是事务的储存引擎,使用日志优先技术(Write-Ahead-Logging,WAL)实现事务的持久化。也就是说当一个事务提交时,必须先将该事务的所有日志写入重做日志文件中进行持久化,待事务的提交操作完成才算完成,若过程中事务更新失败,则通过重做日志文件回滚。

其中,该事务的所有日志包括redo log 跟 undo log,它们的区别是:

  • redo log:物理日志,顺序写,记录的是页的物理修改操作,恢复提交事务修改的页操作,用于保证事务的持久性。
  • undo log:逻辑日志,随机写,根据每行数据进行记录, 回滚行数据到某个特定版本,用于帮助事务回滚及MVCC功能。

undo + redo log的设计是为了保证事务原子性、一致性特性的同时,尽量提高IO性能。

redo log

为什么需要redo log?

为了保证事务的一致性,最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中,但是这么做会有严重的性能问题,主要体现在两个方面:

  1. 因为Innodb是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
  2. 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差。

因此引入redo log,具体来说就是只记录事务对数据页物理上做了哪些修改,相对而言文件更小并且是顺序IO,降低了对数据页刷盘的要求,这样就能更好地解决性能问题,额外需要说明的是,redo log需要WAL技术的支持,也就是先把日志写入磁盘再修改磁盘中的数据页(甚至都不要修改磁盘上的数据页,只用修改内存中的就行,反正redo log已经落盘了,数据库宕机也能恢复)一句话总结就是,数据库的性能瓶颈由写数据页的IO转换成写redo log的IO。

组成部分

redo log是用来进行事务重做所需的文件,由两部分组成:

  • redo log buffer(重做日志缓冲),在内存中

    重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。若一个页中产生的重做日志数量大于512字节,需要分割为多个重做日志块进行存储。InnoDB默认每次在执行事务的commit操作时将重做日志缓冲同步写到磁盘上的重做日志文件,即伴有一次fsync的调用,因为磁盘fsync的性能有限,因此提供 group commit 功能使得一次fsync可以刷新确保多个事务日志写入文件

  • redo log file(重做日志文件),在磁盘上

    重做日志文件写入流程

    每一次写入重做日志文件的操作都要先写入一个重做日志缓冲中,再写入操作系统文件系统缓存,最后才写入重做日志文件。由于重做日志块的大小和磁盘扇区大小一样,都是 512 字节,因此重做日志的写入可以保证原子性,不需要 doublewrite 技术。写入内容示例如下:

    1
    2
    3
    4
    5
    6
    7
    // 创建表
    CREATE TABLE t(a INT,b INT,PRIMARY KEY(a),KEY(b));
    // 执行sql语句
    INSERT INTO t SELECT 1,2;
    // 对应的简化版重做日志文件
    page(2,3),offset 32,value 1,2 #聚集索引
    page(2,4),offset 64,value 2 #辅助索引

根据重做日志文件恢复

由于 checkpoint 表示已经刷新到磁盘页上的 LSN(日志序列号),因此在恢复过程中仅需恢复 checkpoint 开始的日志部分。对于上述例子,当数据库在 checkpoint 的 LSN 为 10000 时发生宕机,恢复操作仅恢复 LSN 10000~13000 范围内的日志。

undo log

为什么需要undo log?

为了保证事务的原子性。因为原子性要求一个事务的内部操作要么全做,要么全不做,所以在执行事务之前,要备份当前数据,这样在发生错误时,就能回滚到事务之前的数据状态。

用来进行事务回滚所需的文件,与redo存放在重做日志文件不同,undo 存放在数据库中位于共享表空间内的 undo 段。undo log分为两类:

  • insert undo log:是指在 insert 操作中产生的 undo log。因为 insert 操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作。
  • update undo log:记录的是对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge 线程进行最后的删除。

undo log 写入流程

在事务执行时,即每一条SQL语句的执行,都会产生对应的undo log,当事务提交时,InnoDB将undo log 放入列表中,待之后的purge操作(而不是直接删除),同时事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志。若事务执行失败需要回滚时,由于undo是逻辑日志,因此只是将对应数据逻辑地恢复到原来的样子,但是数据结构和物理页本身在回滚之后可能大不相同。

undo log 回收即purge执行流程

在执行purge的过程中,InnoDB首先从history list中找到第一个需要被清理的记录,这里为trx1,清理之后InnoDB存储引擎会在trx1的undo log所在的undo page 1中继续寻找是否存在可以被清理的记录,这里会找到事务trx3,接着找到trx5(灰色表示还被其他事务引用),但是发现trx5被其他事务所引用而不能清理,故去再次去history list中查找,发现这时最尾端的记录为trx2,接着找到trx2所在的页,然后依次再把事务trx6、trx4的记录进行清理。由于undo page2中所有的页都被清理了,因此该undo page可以被重用。

总的来说,InnoDB通过purge线程先从history list中找undo log,然后再从该undo log 所在的undo page中找其他可回收的undo log。(数据结构上先链表再数组)

BLGC

考虑到备份和恢复的需要,需要保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致,为此引入BLGC(Binary Log Group Commit)技术。

在MySQL数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。BLGC的步骤分为以下三个阶段:

  • Flush 阶段:将每个事务的二进制日志写入内存中。
  • Sync 阶段:将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。
  • Commit 阶段:leader根据顺序调用存储引擎层事务的提交。当有一组事务在Commit阶段时,不影响其他新事务进入Flush阶段。

事务隔离级别

以下四种隔离级别的具体表现仅针对InnoDB,隔离级别越低,事务请求的锁越少或保持锁的时间越短。用set @@tx_isolation = 'READ-CCOMMITTED'来设置:

读未提交(RU, Read Uncommitted)

没有应用锁机制。一个事务可以读到另一个事务未提交的结果,存在脏读和不可重复读问题。

读提交(RC, Read Committed)

除了唯一性的约束检查和外键约束检查需要Gap Lock,其他全是用Record Lock的算法。只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题,存在不可重复读问题。

可重复读(RR, Repeatable Read)

基本都是用Next-Key Lock算法,除了当查询的索引是唯一索引时退化成Record Lock,支持的是一致性的非锁定读。在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读(幻读)问题。这是InnoDB默认级别。

可串行化(Serialiable)

对每个select语句后自动加上lock in share mode,即为每个读取操作加一个共享锁,支持的是一致性锁定读。事务串行化执行,隔离级别最高,单机不再具有并发性,常用于分布式事务。

MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 InnoDB 实现隔离级别的一种具体方式(和上文锁中的一致性非锁定读对应),用于实现RC和RR这两种隔离级别。RU总是读取最新的数据行,无需使用 MVCC,而Serialiable需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

先了解版本号的概念:

  • 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号。当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号

MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:

  • 创建版本号:创建一个数据行的快照时的系统版本号。如果该快照的创建版本号大于当前事务版本,表示该快照是其他事务的最新修改结果,不该读取。
  • 删除版本号:删除一个数据行的快照时的系统版本号。如果该快照的删除版本号大于当前事务版本号,表示该快照有效, 否则表示该快照已经被删除了。

MVCC 使用到的快照存储在 undo log中,该日志通过回滚指针把一个行数据的所有快照连接起来。

RR级别的具体实现如下:

  • select

    \(创建版本号 < 事务版本号 < 删除版本号\)

    仅读事务T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。

  • insert

    \(事务版本号 => 创建版本号\)

    将当前系统版本号作为数据行快照的创建版本号。

  • delete

    \(事务版本号 => 删除版本号\)

    将当前系统版本号作为数据行快照的删除版本号。

  • update

    先删除后创建

    将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 delete 后执行 insert

分布式事务

分布式事务

分布式系统存在3个重要指标:

  • Consistency:一致性,写操作之后的读操作,必须返回该值(问题的关键在于可能在服务器A上执行了写入操作,但是读是分配到了服务器B上,由于A、B不一致导致的数据不一致性)
  • Availability:可用性,任何时候服务都是可用的。只要收到用户的请求,服务器就必须给出回应。(为了实现一致性,服务器们就需要在写入操作发生时执行同步操作,以实现一致性)
  • Partition tolerance:分区容错性,指多个设备或单元之间的通讯不一定成功

上述三个指标也被称为CAP问题,但任何系统只能满足其中的两点。由于P是必须要考虑到的问题,所以现实中,CA不得兼容

InnoDB通过 XA 事务来支持分布式事务,此时的事务隔离级别必须设置为Serializable。XA 事务由一个或多个资源管理器、一个事务管理器以及一个应用程序组成:

  • 资源管理器(Resource Managers):提供访问事务资源的方法,通常一个MySQL数据库就是一个资源管理器。
  • 事务管理器(Transaction Manager):协调参与全局事务中的各个事务,需要和参与全局事务的所有资源管理器进行通信,通常为连接MySQL服务器的客户端。
  • 应用程序(Application Program):定义事务的边界,指定全局事务中的操作。

分布式事务使用两阶段提交(two-phase commit),与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行commit 或是 rollback 操作。

  • 在第一阶段,所有参与全局事务的节点都开始准备(prepare),告诉事务管理器它们准备好提交了。
  • 在第二阶段,事务管理器告诉资源管理器执行 rollback 还是 commit。如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。

实际操作中,基本都是通过编程语言来完成分布式事务的操作。

主从复制

主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。

  • binlog 线程 :负责将主服务器上的数据更改写入二进制日志中。
  • I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。
  • SQL 线程 :负责读取中继日志并重放其中的 SQL 语句。

读写分离

主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;

  • 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;

  • 增加冗余,提高可用性。

读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。

一致性哈希

对于分布式缓存,不同机器上存储不同对象的数据。一般通过除法散列来实现这些缓存机器的负载均衡。

然而,但当机器需要扩容或出现宕机时,即3台机器再加入1台机器时,缓存命中的概率只有25%(1/4),为了解决除法散列算法伸缩性差的问题,引入了一致性哈希算法,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器,即减少机器增减缓存失效的比例。

该算法将对象和机器都放置到同一个hash环中,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。当增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。这时,只有机器c3与c4之间的对象需要重新分配新的机器。对于我们的例子,只有对象o4被重新分配到了c4,其他对象仍在原有机器上:

缓存命中的概率变成75%(3/4),大大减轻了增加缓存机器带来的数据库访问的压力。不过存在新的问题,新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并没有因为机器c4的加入而减少负载压力。为此,引入虚拟节点来解决负载不均衡的问题。

假如开始时存在缓存机器c1,c2,c3,对于每个缓存机器,都有3个虚拟节点对应,新加入缓存机器c4,其对应的虚拟节点为c41,c42,c43,影响的虚拟节点包括c31,c22,c11(顺时针查找到第一个节点),即新加入的一台机器,同时影响到原有的3台机器。理想情况下,新加入的机器平等地分担了原有机器的负载,这正是虚拟节点带来的好处,同时缓存命中率保持不变还是75%。

其他

  1. 缓存雪崩、缓存击穿和缓存穿透

    缓存异常 产生原因 应对方案
    缓存雪崩 大量数据同时过期 1. 在原有的失效时间基础上增加一个随机值,避免同一时间过期
    2. 加锁 / 队列方式,保证缓存的但线程 / 进程写
    3. 后台更新缓存,定时更新、消息队列通知更新
    Redis 故障宕机 1. 请求限流。只将少部分请求发送到数据库处理,其余请求在入口直接拒绝
    2. 服务熔断。暂停业务应用对缓存服务的访问,直接返回错误
    3. 构建Redis缓存高可靠集群,如主从节点等
    缓存击穿 频繁访问的热点数据过期,压力直接给到数据库,是缓存雪崩的一个子集 1. 互斥锁
    2. 由后台更新缓存,而不是给热点数据设置过期时间
    缓存穿透 访问的数据既不在缓存,也不在数据库 1. 非法请求的限制。在API入口处判断请求参数是否合理
    2. 缓存控制或默认值。针对查询的数据,在缓存中设置一个空值 / 默认值
    3. 使用布隆过滤器快速判断数据是否存在。若不存在,不会访问到数据库。

    布隆过滤器介绍,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器

    在数据库写入数据 x 后,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果 1、4、6,对应位置的值设置为 1。在查询时,只要位图数组的第 1、4、6 位置的值不全为 1,就认为数据 x 不在数据库中。

    布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。如果存在误判的样本,可以通过建立白名单来防止误报。

  2. 一条SQL查询语句执行很慢,可能是什么原因?

    • 一直都执行的很慢

      • 没有用上索引

        字段没有索引、有索引但是没有用上、函数操作导致没有用上索引等情况,用explain分析SQL语句。

      • 优化器选错索引

        优化器是通过采样来预测索引的基数,可能出现错误,不走索引走全表扫描。可以通过force index来强制指定索引。

    • 大多数情况正常,偶尔很慢

      • 执行的时候,遇到其他事务在占用锁
      • 数据库在刷新脏页,比如redo log写满了需要同步到磁盘。
  3. 若在数据库底层进行排序,该如何设计排序算法

    参考MySQL的order by的底层实现,正常排序都是采用快排,考虑以下情况:

    • 如果排序取的数据很少,小于sort_buffer,那么使用优先级队列进行堆排序。
    • 如果排序取的数据量大于sort_buffer,则创建排序临时文件,分次读入,分批快排,最终归并排序。