diff --git a/README.md b/README.md index a08b6ae..c6e71f5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **适合阅读人群:** -* 面临校招的相关专业大学生。 +* 面临校招的相关专业大学生。 * 准备面试的 Java 初/中级工程师。 * 希望掌握 Java 最流行知识点的编程爱好者。 @@ -22,7 +22,7 @@ * **微信:naerjiajia207** # Java 知识点及面试题 -## Java 基础 +## Java 基础 - [Java 基础](https://github.com/lvminghui/Java-Notes/blob/master/docs/Java%E5%9F%BA%E7%A1%80.md) - [计算机网络面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Java 容器 @@ -44,6 +44,11 @@ - [MySQL 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [MySQL 常见知识点总结](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93.md) +### 深入理解 MySQL +- [基础篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3MySQL%E5%9F%BA%E7%A1%80%E7%AF%87.md) + +- [高性能实战篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%AB%98%E6%80%A7%E8%83%BD%E5%AE%9E%E8%B7%B5%E7%AF%87.md) + ## Redis - [Redis 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Redis%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) @@ -51,10 +56,10 @@ - [SpringBean 的生命周期](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBean%20%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.md) - [Spring 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [Spring 的设计理念和设计模式分析](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E5%92%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%88%86%E6%9E%90.md) -## Mybatis +### Mybatis - [Mybatis](https://github.com/lvminghui/Java-Notes/blob/master/docs/Mybatis.md) -## Spring Boot +### Spring Boot - [Spring Boot 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBoot.md) ## Spring Cloud diff --git "a/docs/typora-user-images/B+\346\240\221.png" "b/docs/typora-user-images/B+\346\240\221.png" new file mode 100644 index 0000000..4823b10 Binary files /dev/null and "b/docs/typora-user-images/B+\346\240\221.png" differ diff --git "a/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" new file mode 100644 index 0000000..7c4b17d Binary files /dev/null and "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" differ diff --git "a/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" new file mode 100644 index 0000000..5a9e407 Binary files /dev/null and "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" new file mode 100644 index 0000000..1664ddc Binary files /dev/null and "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" differ diff --git a/docs/typora-user-images/MVCC3.png b/docs/typora-user-images/MVCC3.png new file mode 100644 index 0000000..8e7eb5e Binary files /dev/null and b/docs/typora-user-images/MVCC3.png differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" new file mode 100644 index 0000000..b62bf71 Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" new file mode 100644 index 0000000..b64a96a Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" differ diff --git "a/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" new file mode 100644 index 0000000..e1f9661 Binary files /dev/null and "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" new file mode 100644 index 0000000..9a070ee Binary files /dev/null and "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" differ diff --git "a/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" new file mode 100644 index 0000000..5632da0 Binary files /dev/null and "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" differ diff --git "a/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" new file mode 100644 index 0000000..4ab8906 Binary files /dev/null and "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" differ diff --git "a/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" new file mode 100644 index 0000000..6393995 Binary files /dev/null and "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" differ diff --git "a/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" new file mode 100644 index 0000000..e46c780 Binary files /dev/null and "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" differ diff --git "a/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..0d97774 --- /dev/null +++ "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,343 @@ +# MySQL体系结构与存储引擎 + +## MySQL体系结构 + +先看 MySQL 数据库的体系结构,如下图所示。 + + + + + + + +MySQL体系结构由ClientConnectors层、MySQLServer层及存储引擎层组成。 + +**ClientConnectors层** + +负责处理客户端的连接请求,与客户端创建连接。目前 MySQL 几乎支持所有的连接类型,例如常见的 JDBC、Python、Go 等。 + +**MySQL Server 层** + +MySQLServer层主要包括ConnectionPool、Service&utilities、SQLinterface、Parser解析器、Optimizer查询优化器、Caches缓存等模块。 + +1. ConnectionPool,负责处理和存储数据库与客户端创建的连接,线程池资源管理,一个线程负责管理一个连接。还包括了用户认证模块,就是用户登录身份的认证和鉴权及安全管理,也就是用户执行操作权限校验。 + +2. Service & utilities 是管理服务&工具集,包括备份恢复、安全管理、集群管理服务和工具。 +3. SQL interface,负责接收客户端发送的各种 SQL 语句,比如 DML、DDL 和存储过程等。 +4. Parser 解析器会对 SQL 语句进行语法解析生成解析树。 +5. Optimizer 查询优化器会根据解析树生成执行计划,并选择合适的索引,然后按照执行计划执行 SQL 语言并与各个存储引擎交互。 +6. Caches缓存包括各个存储引擎的缓存部分,比如:InnoDB存储的BufferPool,Caches中也会缓存一些权限,也包括一些 Session 级别的缓存。 + +**存储引擎层** + +存储引擎包括MyISAM、InnoDB,以及支持归档的Archive和内存的Memory等。MySQL是插件式的存储引擎,只要正确定义与MySQLServer交互的接口,任何引擎都可以访问MySQL。 + +**物理存储层** + +存储引擎底部是物理存储层,是文件的物理存储层,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、redo/undo 日志等。 + + + +下面是一条SQL SELECT语句的执行过程: + + + +## 存储引擎 + +存储引擎是 MySQL 中具体与文件打交道的子系统,它是根据 MySQL AB 公司提供的文件访问层的抽象接口,定制的一种文件访问机制,这种机制就叫作存储引擎。 + +InnoDB 存储引擎的具体架构如下图所示。上半部分是实例层(计算层),位于内存中,下半部分是物理层,位于文件系统中。 + + + + + +### 实例层 + +实例层分为线程和内存。 + +InnoDB 重要的线程有 Master Thread,Master Thread 是 InnoDB 的主线程,负责调度其他各线程。 + +* MasterThread的优先级最高,其内部包含几个循环:主循环(loop)、后台循环(backgroundloop)、刷新循环(flushloop)、暂停循环(suspendloop)。Master Thread 会根据其内部运行的相关状态在各循环间进行切换。 + + 大部分操作在主循环(loop)中完成,其包含 1s 和 10s 两种操作。 + +* buf_dump_thread 负责将 buffer pool 中的内容 dump 到物理文件中,以便再次启动 MySQL 时,可以快速加热数据。 + +* page_cleaner_thread负责将bufferpool中的脏页刷新到磁盘,在5.6版本之前没有这个线程,刷新操作都是由主线程完成的,所以在刷新脏页时会非常影响MySQL的处理能力,在5.7 版本之后可以通过参数设置开启多个 page_cleaner_thread。 + +* purge_thread 负责将不再使用的 Undo 日志进行回收。 + +* read_thread 处理用户的读请求,并负责将数据页从磁盘上读取出来,可以通过参数设置线程数量。 + +* write_thread 负责将数据页从缓冲区写入磁盘,也可以通过参数设置线程数量,page_cleaner 线程发起刷脏页操作后 write_thread 就开始工作了。 + +* redo_log_thread 负责把日志缓冲中的内容刷新到 Redo log 文件中。 + +* insert_buffer_thread 负责把 Insert Buffer 中的内容刷新到磁盘。 + +实例层的内存部分主要包含InnoDBBufferPool,这里包含InnoDB最重要的缓存内容。数据和索引页、undo页、insertbuffer页、自适应Hash索引页、数据字典页和锁信息等。additionalmemorypool后续已不再使用。Redobuffer里存储数据修改所产生的Redolog。doublewritebuffer是 double write 所需的 buffer,主要解决由于宕机引起的物理写入操作中断,数据页不完整的问题。 + +### 物理层 + +物理层在逻辑上分为系统表空间、用户表空间和 Redo日志。 + +系统表空间里有 ibdata 文件和一些 Undo,ibdata 文件里有 insert buffer 段、double write段、回滚段、索引段、数据字典段和 Undo 信息段。 + +用户表空间是指以 .ibd 为后缀的文件,文件中包含 insert buffer 的 bitmap 页、叶子页(这里存储真正的用户数据)、非叶子页。 + +Redo日志中包括多个Redo文件,这些文件循环使用,当达到一定存储阈值时会触发checkpoint刷脏页操作,同时也会在MySQL实例异常宕机后重启,InnoDB表数据自动还原恢复过程中使用。 + +### 内存和物理结构 + +内存和物理结构,如下图所示。 + + + +**BufferPool** + +用户读取或者写入的最新数据都存储在BufferPool中,如果BufferPool中没有找到则会读取物理文件进行查找,之后存储到BufferPool中并返回给MySQLServe。Buffer Pool 采用LRU 机制。 + +BufferPool决定了一个SQL执行的速度快慢,如果查询结果页都在内存中则返回结果速度很快,否则会产生物理读(磁盘读),返回结果时间变长。但我们又不能将所有数据页都存储到BufferPool中。在单机单实例情况下,我们可以配置BufferPool为物理内存的60%~80%,剩余内存用于session产生的sort和join等,以及运维管理使用。如果是单机多实例,所有实例的bufferpool总量也不要超过物理内存的80%。开始时我们可以根据经验设置一个BufferPool的经验值,比如16GB,之后业务在MySQL运行一段时间后可以根据show global status like'%buffer_pool_wait%' 的值来看是否需要调整 Buffer Pool 的大小。 + +**Redolog** + +Redolog是一个循环复用的文件集,**负责记录InnoDB中所有对BufferPool的物理修改日志**,当Redolog文件空间中,检查点位置的LSN和最新写入的LSN差值(checkpoint_age)达到Redolog文件总空间的75%后,InnoDB会进行异步刷新操作,直到降至75%以下,并释放Redolog的空间;当checkpoint_age达到文件总量大小的 90% 后,会触发同步刷新,此时 InnoDB 处于挂起状态无法操作。 + +补充: + +* 日志序号 (LSN:Log sequence number) 标识特定日志文件记录在日志文件中的位置。 +* checkpoint_age:检查点,将缓冲池中的脏页刷回到磁盘。当缓冲池不够用时,根据LRU算法溢出的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。 + +每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过命令SHOW ENGINE INNODB STATUS来观察 。 + +这样我们就看到**Redolog的大小直接影响了数据库的处理能力**,如果设置太小会导致强行checkpoint操作频繁刷新脏页,那我们就需要将Redolog设置的大一些,5.6版本之前Redo log 设置的大一些,5.6 版本之前 Redo log 总大小不能超过 3.8GB,5.7 版本之后放开了这个限制。 + +事务提交时 log buffer 会刷新到 Redo log 文件中,具体刷新机制由参数控制。 + +### Myisam和InnoDB的区别 + +* **是否支持行级锁** : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁,适合高并发操作。 +* **是否支持外键**: MyISAM不支持,而InnoDB支持 +* **是否支持事务**:MyISAM不支持,而InnoDB支持 +* **缓存**:MyISAM只缓存索引,InnoDB缓存索引和真实数据,所以对内存要求高 +* **崩溃恢复**:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +* InnoDB 支持 MVCC,MyISAM 不支持; + + +InnoDB 表最大还可以支持 64TB,支持聚簇索引、支持压缩数据存储,支持数据加密,支持查询/索引/数据高速缓存,支持自适应hash索引、空间索引,支持热备份和恢复等 + +## InnoDB 核心要点 + + + + + + +ARIES 三原则 + +WriteAheadLogging(WAL)。 + +* 先写日志后写磁盘,日志成功写入后事务就不会丢失,后续由checkpoint机制来保证磁盘物理文件与Redo日志达到一致性; +* 利用Redo 记录变更后的数据,即 Redo 记录事务数据变更后的值; +* 利用 Undo 记录变更前的数据,即 Undo 记录事务数据变更前的值,用于回滚和其他事务多版本读。 + +show engine innodb status\G 的结果里面有详细的 InnoDB 运行态信息,分段记录的,包括内存、线程、信号、锁、事务。 + +# 深入理解事务与锁机制 + +## 事务及其特性 + +一个逻辑工作单元要成为事务,在关系型数据库管理系统中,必须满足 4 个特性,即所谓的 ACID:原子性、一致性、隔离性和持久性。 + +Atomicity(原子性):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行 + +Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 + +Isolation(隔离性):同一时间,只允许一个事务操作同一数据,不同的事务之间彼此没有任何干扰。 + +Durability(持久性):事务处理结束后,对数据的修改是永久的。 + +### 一致性 + +一致性其实包括两部分内容,分别是约束一致性和数据一致性。 + +* 约束一致性:数据库创建表结构时所制定的外键,唯一索引等约束。 +* 数据一致性:是一个综合性的规定,或者说是一个把握全局的规定。因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。 + +### 原子性 + +原子性就是前面提到的两个“要么”,即要么改了,要么没改。也就是说用户感受不到一个正在改的状态。MySQL 是通过 WAL(Write Ahead Log)技术来实现这种效果的。 + +举例来讲,如果事务提交了,那改了的数据就生效了,如果此时BufferPool的脏页没有刷盘,如何来保证改了的数据生效呢?就需要使用Redo日志恢复出来的数据就需要使用Redo日志恢复出来的数据。而如果事务没有提交,且BufferPool的脏页被刷盘了,那这个本不应该存在的数据如何消失呢?就需要通过 Undo 来实现了,Undo 又是通过 Redo 来保证的,所以最终原子性的保证还是靠 Redo 的 WAL 机制实现的。 + +### 持久性 + +所谓持久性,就是指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,接下来的操作或故障不应该对其有任何影响。持久性是如何保证的呢?一旦事务提交,通过原子性,即便是遇到宕机,也可以从逻辑上将数据找回来后再次写入物理存储空间,这样就从逻辑和物理两个方面保证了数据不会丢失,即保证了数据不会丢失,即保证了数据库的持久性。 + +### 隔离性 + +所谓隔离性,指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。锁和多版本控制就符合隔离性。 + +## 并发事务控制 + +### 单版本控制-锁 + +锁用独占的方式来保证在只有一个版本的情况下事务之间相互隔离,所以锁可以理解为单版本控制。在MySQL事务中,锁的实现与隔离级别有关系,在RR(RepeatableRead)隔离级别下,MySQL为了解决幻读的问题,以牺牲并行度为代价,通过Gap锁来防止数据的写入,而这种锁,因为其并行度不够,冲突很多,经常会引起死锁。现在流行的Row模式可以避免很多冲突甚至死锁问题,所以推荐默认使用 Row + RC(Read Committed)模式的隔离级别,可以很大程度上提高数据库的读写并行度。 + +补充: 在row level模式下,bin-log中可以不记录执行的sql语句的上下文相关的信息,仅仅只需要记录那一条被修改。 + +### 多版本控制-MVCC + + MVCC,是指在数据库中,为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。 + +那个多版本是如何生成的呢?每一次对数据库的修改,都会在Undo日志中记录当前修改记录的事务号及修改前数据状态的存储地址(即ROLL_PTR),以便在必要的时候可以回滚到老的数据版本。例如,一个读事务查询到当前记录,而最新的事务还未提交,根据原子性,读事务看不到最新数据,但可以去回滚段中找到老版本的数据,这样就生成了多个版本。多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。 + +## 技术原理 + +### 原子性技术原理 + +每一个写事务,都会修改BufferPool,从而产生相应的Redo日志,这些日志信息会被记录到ib_logfiles文件中。因为Redo日志是遵循WriteAheadLog的方式写的,所以事务是顺序被记录的。在MySQL中,任何BufferPool中的页被刷到磁盘之前,都会先写入到日志文件中,这样做有两方面的保证。如果BufferPool中的这个页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写下去的数据不会丢失,所以必须要保证 Redo 先写。 + +为 Buffer Pool 的空间是有限的,要载入新页时,需要从 LRU 链表中淘汰一些页,而这些页必须要刷盘之后,才可以重新使用,那这时的刷盘,就需要保证对应的 LSN 的日志也要提前写到 ib_logfiles 中,如果没有写的话,恰巧这个事务又没有提交,数据库挂了,在数据库启动之后,这个事务就没法回滚了。所以如果不写日志的话,这些数据对应的回滚日志可能就不存在,导致未提交的事务回滚不了,从而不能保证原子性,所以原子性就是通过 WAL 来保证的。 + +### 持久性技术原理 + +通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持久性。这个过程与前面提到的Redo日志、事务状态、数据库恢复、参数innodb_flush_log_at_trx_commit 有关,还与 binlog 有关。 + +### 隔离性技术原理 + +InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。 + +具体说到隔离性的实现方式,我们通常用ReadView表示一个事务的可见性。RC级别的事务可见性比较高,它可以看到已提交的事务的所有修改。而RR级别的事务,则没有这个功能,一个读事务中,不管其他事务对这些数据做了什么修改,以及是否提交,只要自己不提交,查询的数据结果就不会变。 + +随着时间的推移,读提交每一条读操作语句都会获取一次 Read View,每次更新之后,都会获取数据库中最新的事务提交状态,也就可以看到最新提交的事务了,即每条语句执行都会更新其可见性视图。 + +而反观可重复读,这个可见性视图,只有在自己当前事务提交之后,才去更新,所以与其他事务是没有关系的。 + +**在 RR 级别下,长时间未提交的事务会影响数据库的 PURGE 操作,从而影响数据库的性能,所以可以对这样的事务添加一个监控**。 + +### 一致性技术原理 + +数据的完整性是通过其他三个特性来保证的,为了保证数据的完整性,提出来三个特性,这三个特性又是由同一个技术来实现的,所以理解 Redo/Undo 才能理解数据库的本质。 + +### MVCC 实现原理 + +MVCC最大的好处是读不加锁,读写不冲突。在读多写少的OLTP(On-LineTransactionProcessing)应用中,读写不冲突是非常重要的,极大的提高了系统的并发性能。 + +在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。 + +* 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。 +* 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。 + +如何区分快照读和当前读呢? 可以简单的理解为: + +* 快照读:简单的 select 操作,属于快照读,不需要加锁。 +* 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 + +下面用一个事务对某行记录更新的过程来说明MVCC中多版本的实现。 + +假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如下图所示。 + +image-20200412113401177 + +* 隐含ID(DB_ROW_ID),6个字节,当由InnoDB自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值。 +* 事务号(DB_TRX_ID),6个字节,标记了最新更新这条行记录的TransactionID,每处理一个事务,其值自动+1。 +* 回滚指针(DB_ROLL_PT),7个字节,指向当前记录项的RollbackSegment的Undolog记录,通过这个指针才能查找之前版本的数据。 + +首先,假如这条数据是刚 INSERT 的,可以认为 ID 为 1,其他两个字段为空。 + +然后,当事务 1 更改该行的数据值时,会进行如下操作,如下图所示。 + + + +* 用排他锁锁定该行;记录 Redo log +* 把该行修改前的值复制到 Undo log,即图中下面的行 +* 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行 + +如果数据继续执行,此时Undolog中有两行记录,并且通过回滚指针连在一起。因此,如果Undolog一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的是在InnoDB中存在purge 线程,它会查询那些比现在最老的活动事务还早的 Undo log,并删除它们,从而保证 Undo log 文件不会无限增长,如下图所示。 + + + +## 并发事务问题及解决方案 + +脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。 + +不可重复读 :是指在一个事务内,多次读同一数据数据发生了变化。 + +幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。 + +产生的这些问题,MySQL 数据库是通过事务隔离级别来解决的。值得一提的是,InnoDB通过Gap锁解决了幻读的问题。 + +不可重复读重点在于 UPDATA 和 DELETE,而幻读的重点在于 INSERT。它们之间最大的区别是如何通过锁机制来解决它们产生的问题。 + + + +## InnoDB 的锁 + +InnoDB 的锁分为行锁和表锁。 + +其中行锁包括两种: + +* 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 +* 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。 + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。表锁又分为三种。 + +* 意向共享锁(IS):事务计划给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁 +* 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的的 IX 锁。 +* 自增锁(AUTO-INC Locks):特殊表锁,自增长计数器通过该“锁”来获得子增长计数器最大的计数值。 + +在加行锁之前必须先获得表级意向锁,否则等待 innodb_lock_wait_timeout 超时后根据innodb_rollback_on_timeout 决定是否回滚事务。 + +### InnoDB自增锁 + +在MySQLInnoDB存储引擎中,我们在设计表结构的时候,通常会建议添加一列作为自增主键。这里就会涉及一个特殊的锁:自增锁(即:AUTO-INCLocks),它属于表锁的一种,在 INSERT 结束后立即释放。我们可以执行 **show engine innodb status\G** 来查看自增锁的状态信息。 + +InnoDB 锁关系矩阵如下图,其中:+ 表示兼容,- 表示不兼容。 + + + + + +### InnoDB 行锁 + +InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的。主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。 + +* RecordLock锁:单个行记录的锁(锁数据,不锁Gap)。 +* GapLock锁:间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。 +* Next-keyLock 锁:同时锁住数据,并且锁住数据前面的 Gap。 + +**排查 InnoDB 锁问题** + +排查InnoDB锁问题通常有2种方法。打开innodb_lock_monitor表,注意使用后记得关闭,否则会影响性能。在MySQL5.5版本之后,可以通过查看information_schema 库下面的 innodb_locks、innodb_lock_waits、innodb_trx 三个视图排查 InnoDB 的锁问题。 + +### InnoDB死锁 + +在MySQL中死锁不会发生在MyISAM存储引擎中,但会发生在InnoDB存储引擎中,因为InnoDB是逐行加锁的,极容易产生死锁。那么死锁产生的四个条件是什么呢? + +* 互斥条件:一个资源每次只能被一个进程使用; +* 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放; +* 不剥夺条件:进程已获得的资源,在没使用完之前,不能强行剥夺; +* 循环等待条件:多个进程之间形成的一种互相循环等待资源的关系。 + +在发生死锁时,InnoDB存储引擎会自动检测,并且会自动回滚代价较小的事务来解决死锁问题。但很多时候一旦发生死锁,InnoDB存储引擎的处理的效率是很低下的或者有时候根本解决不了问题,需要人为手动去解决。 + +既然死锁问题会导致严重的后果,那么在开发或者使用数据库的过程中,如何避免死锁的产生呢?这里给出一些建议: + +* 加锁顺序一致; +* 尽量基于primary或uniquekey更新数据。 +* 单次操作数据量不宜过多,涉及表尽量少。 +* 减少表上索引,减少锁定资源。 +* 相关工具:pt-deadlock-logger + +查看MySQL数据库中死锁的相关信息,可以执行showengineinnodbstatus\G来进行查看,重点关注“LATESTDETECTEDDEADLOCK”部分。 + +一些开发建议来避免线上业务因死锁造成的不必要的影响。 + +* 更新SQL的where条件时尽量用索引; +* 加锁索引准确,缩小锁定范围; +* 减少范围更新,尤其非主键/非唯一索引上的范围更新。 +* 控制事务大小,减少锁定数据量和锁定时间长度 (innodb_row_lock_time_avg)。 +* 加锁顺序一致,尽可能一次性锁定所有所需的数据行。 + diff --git "a/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" new file mode 100644 index 0000000..7909d36 --- /dev/null +++ "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" @@ -0,0 +1,470 @@ +# 高性能库表设计 + +### 范式和反范式 + +#### 范式 + +范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。数据库的设计范式是数据库设计所需要满足的规范。只有理解数据库的设计范式,才能设计出高效率、优雅的数据库,否则可能会设计出低效的库表结构。目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,还又称完美范式。 + +满足最低要求的叫第一范式,简称1NF。在第一范式基础上进一步满足一些要求的为第二范式,简称2NF。其余依此类推。各种范式呈递次规范,越高的范式数据库冗余越小。通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)。 + +**第一范式** + +第一范式无重复的列,表中的每一列都是拆分的基本数据项,即列不能够再拆分成其他几列,强调的是列的原子性。 + +如果在实际场景中,一个联系人有家庭电话和公司电话,那么以“姓名、性别、电话”为表头的表结构就没有达到1NF。要符合1NF我们只需把电话列拆分,让表头变为姓名、性别、家庭电话、公司电话即可。 + +**第二范式** + +属性完全依赖于主键,首先要满足它符合1NF,另外还需要包含两部分内容: + +* 表必须有一个主键; +* 没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。即要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。 + +**第三范式** + +第三范式属性不传递依赖于其他非主属性,首先需要满足 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。 + +**第二范式和第三范式的区别第二范式:** + +* 非主键列是否依赖主键(包括一列通过某一列间接依赖主键),要是有依赖关系就是第二范式; +* 第三范式:非主键列是否直接依赖主键,不能是那种通过传递关系的依赖。要是符合这种依赖关系就是第三范式。通过对前三个范式的了解,我们知道 3NF 是 2NF 的子集,2NF 是 1NF 的子集。 + +**范式优缺点** + +优点: + +* 避免数据冗余,减少维护数据完整性的麻烦; +* 减少数据库的空间; +* 数据变更速度快。 + +缺点: + +* 按照范式的规范设计的表,等级越高的范式设计出来的表数量越多。 +* 获取数据时,表关联过多,性能较差。 + +表的数量越多,查询所需要的时间越多。也就是说所用的范式越高,对数据操作的性能越低。 + +#### 反范式 + +范式是普适的规则,满足大多数的业务场景的需求。对于一些特殊的业务场景,范式设计的表,无法满足性能的需求。此时,就需要根据业务场景,在范式的基础之上进行灵活设计,也就是反范式设计。 + +反范式设计主要从三方面考虑: + +**业务场景,相应时间,字段冗余。** + +反范式设计就是用空间来换取时间,提高业务场景的响应时间,减少多表关联。主要的优点如下: + +允许适当的数据冗余,业务场景中需要的数据几乎都可以在一张表上显示,避免关联; + +可以设计有效的索引。 + +#### 范式与反范式异同 + +**范式化模型**:数据没有冗余,更新容易;当表的数量比较多,查询数据需要多表关联时,会导致查询性能低下。 + +**反范式化模型**:冗余将带来很好的读取性能,因为不需要join很多很多表;虽然需要维护冗余数据,但是对磁盘空间的消耗是可以接受的。 + +## MySQL 使用原则和设计规范 + +### 概述 + +MySQL虽然具有很多特性并提供了很多功能,但是有些特性会严重影响它的性能,比如,在数据库里进行计算,写大事务、大SQL、存储大字段等。想要发挥MySQL的最佳性能,需要遵循3个基本使用原则。 + +* 首先是需要让MySQL回归存储的基本职能:MySQL数据库只用于数据的存储,不进行数据的复杂计算,不承载业务逻辑,确保存储和计算分离; +* 其次是查询数据时,尽量单表查询,减少跨库查询和多表关联; +* 还有就是要杜绝大事务、大 SQL、大批量、大字段等一系列性能杀手。 + +补充: + +大事务,运行步骤较多,涉及的表和字段较多,容易造成资源的争抢,甚至形成死锁。一旦事务回滚,会导致资源占用时间过长。 + +大SQL,复杂的SQL意味着过多的表的关联,MySQL数据库处理关联超过3张表以上的SQL时,占用资源多,性能低下。 + +大批量,意味着多条SQL一次性执行完成,必须确保进行充分的测试,并且在业务低峰时段或者非业务时段执行。大字段,blob、text等大字段,尽量少用。必须要用时,尽量与主业务表分离,减少对这类字段的检索和更新。 + +下面具体讲解数据库的基本设置规则: + +1. 必须指定默认存储引擎为InnoDB,并且禁用MyISAM存储引擎,随着MySQL8.0版本的发布,所有的数据字典表都已经转换成了InnoDB,MyISAM存储引擎已成为了历史。 +2. 默认字符集UTF8mb4,以前版本的UTF8是UTF8mb3,未包含个别特殊字符,新版本的UTF8mb4包含所有字符,官方强烈建议使用此字符集。 +3. 关闭区分大小写功能。设置lower_case_tables_name=1,即可关闭区分大小写功能,即大写字母 T 和小写字母 t 一样。 + +MySQL数据库提供的功能很全面,但并不是所有的功能性能都高效。 + +1. 存储过程、触发器、视图、event。为了存储计算分离,这类功能尽量在程序中实现。这些功能非常不完整,调试、排错、监控都非常困难,相关数据字典也不完善,存在潜在的风险。一般在生产数据库中,禁止使用。 +2. lob、text、enum、set。这些字段类型,在MySQL数据库的检索性能不高,很难使用索引进行优化。如果必须使用这些功能,一般采取特殊的结构设计,或者与程序结合使用其他的字段类型替代。比如:set 可以使用整型(0,1,2,3)、注释功能和程序的检查功能集合替代。 + +### 规范命名 + +统一的规范命名,可以增加可读性,减少隐式转换。 + +命名时的字符取值范围为:a~z,0~9 和 _(下画线)。 + +1. 所有表名小写,不允许驼峰式命名; + +2. 允许使用 -(横线)和 (空格);如下图所示,当使用 -(横线),后台默认会转化成 @002d; + +3. 不允许使用其他特殊字符作为名称,减少潜在风险。 + +数据库库名的命名规则必须遵循“见名知意”的原则,即库名规则为“数据库类型代码 + 项目简称 + 识别代码 + 序号”。 + +**表名的命名规则分为:** + +单表仅使用 a~z、_; + +分表名称为“表名_编号”; + +业务表名代表用途、内容:子系统简称\_业务含义_后缀。 + +常见业务表类型有:临时表,tmp;备份表,bak;字典表,dic;日志表,log。 + +**字段名精确**,遵循“见名知意”的原则,格式:名称_后缀。 + +* 避免普遍简单、有歧义的名称。用户表中,用户名的字段为 UserName 比 Name 更好。 +* 布尔型的字段,以助动词(has/is)开头。用户是否有留言 hasmessage,用户是否通过检查 ischecked 等。 + +**索引命名格式**,主要为了区分哪些对象是索引: + +前缀\_表名(或缩写)_字段名(或缩写);主键必须使用前缀“pk\_”;UNIQUE约束必须使用前缀“uk\_”;普通索引必须使用前缀“idx\_”。 + +### 表创建的注意事项 + +1. 主键列,UNSIGNED整数,使用auto_increment;禁止手动更新auto_increment,可以删除。 +2. 必须添加comment注释。 +3. 必须显示指定的engine。 +4. 表必备三字段:id、xxx_create、xxx_modified。 + * id为主键,类型为unsignedbigint等数字类型; + * xxx_create、xxx_modified的类型均为 datetime 类型,分别记录该条数据的创建时间、修改时间。 + +#### 备份表/临时表等常见表的设计规范 + +1. 备份表,表名必须添加bak和日期,主要用于系统版本上线时,存储原始数据,上线完成后,必须及时删除。 +2. 临时表,用于存储中间业务数据,定期优化,及时降低表碎片。 +3. 日志类表,首先考虑不入库,保存成文件,其次如果入库,明确其生命周期,保留业务需求的数据,定期清理。 +4. 大字段表,把主键字段和大字段,单独拆分成表,并且保持与主表主键同步,尽量减少大字段的检索和更新。 +5. 大表,根据业务需求,从垂直和水平两个维度进行拆分。 + +### 字段设计要求 + +1. 根据业务场景需求,选择合适的类型,最短的长度;确保字段的宽度足够用,但也不要过宽。所有字段必须为NOTNULL,空值则指定default值,空值难以优化,查询效率低。比如:人的年龄用unsignedtinyint(范围0~255,人的寿命不会超过255岁);海龟就必须是smallint,但如果是太阳的年龄,就必须是int;如果是所有恒星的年龄都加起来,那么就必须使用bigint。 +2. 表字段数少而精,尽量不加冗余列。 +3. 单实例表个数必须控制在 2000 个以内。 +4. 单表分表个数必须控制在 1024 个以内。 +5. 单表字段数上限控制在 20~50 个。 + +**禁用ENUM、SET类型。**兼容性不好,性能差。 + +解决方案:使用TINYINT,在COMMENT信息中标明被枚举的含义。\`is_disable` TINYINT UNSIGNED DEFAULT'0'COMMENT '0:启用1:禁用2:异常’。 + +**禁用列为NULL。**MySQL难以优化NULL列;NULL列加索引,需要额外空间;含NULL复合索引无效。 + +解决方案:在列上添加 NOT NULL DEFAULT 缺省值。 + +**禁止 VARBINARY、BLOB 存储图片、文件等。**禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统中,数据库中存储路径。 + +不建议使用TEXT/BLOB:处理性能差;行长度变长;全表扫描代价大。 + +解决方案:拆分成单独的表。 + +存储字节越小,占用空间越小。尽量选择合适的整型,如下图所示。 + + +建议: + +* 主键列,无负数,建议使用INTUNSIGNED或者BIGINTUNSIGNED;预估字段数字取值会超过42亿,使用BIGINT类型。 +* 短数据使用TINYINT或SMALLINT,比如:人类年龄,城市代码。 +* 使用 UNSIGNED 存储非负数值,扩大正数的范围。 + +推荐字符集都为 UTF8mb4,中文存储占三个字节,而数据或字母,则只占一个字节。 + +CHAR(N) 和 VARCHAR(N) 的长度 N,不是字节数,是**字符数**。 + +例:username VARCHAR(40):username 最多能存储 40 个字符,占用 120 个字节。 + +**Char 与 Varchar 类型** + +存储字符串长度相同的全部使用 Char 类型;字符长度不相同的使用 Varchar 类型,不预先分配存储空间,长度不要超过 255。 + +## 案例处理 + +### IP处理 + +一般使用Char(15)进行存储,但是当进行查找和统计时,字符类型不是很高效。MySQL数据库内置了两个IP相关的函数INET_ATON()、INET_NTOA(),可以以实现 IP 地址和整数的项目转换。 + +因此,我们使用 INT UNSIGNED(占用 4 个字节)存储 IP。 + +将 IP 的存储从字符型转换成整形,转化后数字是连续的,提高了查询性能,使查询更快,占用空间更小。 + +### TIMESTAMP处理 + +同样的方法,我们使用MySQL内置的函数(FROM_UNIXTIME(),UNIX_TIMESTAMP()),可以将日期转化为数字,用INTUNSIGNED存储日期和时间。 + +# 高性能索引设计 + +## 索引概述 + +数据库索引是一种数据结构,它以额外的写入和存储空间为代价来提高数据库表上数据检索操作的速度。通俗来说,索引类似于书的目录,根据其中记录的页码可以快速找到所需的内容。 + +MySQL官方对索引(Index)的定义是存储引擎用于快速查找记录的一种数据结构。 + +* 索引是物理数据页,数据库页大小(PageSize)决定了一个页可以存储多少个索引行,以及需要多少页来存储指定大小的索引。 +* 索引可以加快检索速度,但同时也降低索引列插入、删除、更新的速度,索引维护需要代价。 + +## 索引原理 + +二分查找是索引实现的理论基础。在数据库中大部分索引都是通过 B+Tree 来实现的。当然也涉及其他数据结构,在 MySQL 中除了 B+Tree 索引外我们还需要关注下 Hash 索引。 + +### Hash 索引 + +哈希表是数据库中哈希索引的基础,是根据键值存储数据的结构。简单说,哈希表是使用哈希函数将索引列计算到桶或槽的数组,实际存储是根据哈希函数将key换算成确定的存储位置,并将 value 存放到该数组位置上。访问时,只需要输入待查找的 key,即可通过哈希函数计算得出确定的存储位置并读取数据。 + +#### Hash 索引的实现 + +数据库中哈希索引是基于哈希表实现的,对于哈希索引列的数据通过Hash算法计算,得到对应索引列的哈希码形成哈希表,由哈希码及哈希码指向的真实数据行的指针组成了哈希索引。哈希索引的应用场景是只在对哈希索引列的等值查询才有效。 + +因为哈希索引只存储哈希值和行指针,不存储实际字段值,所以其结构紧凑,查询速度也非常快,在无哈希冲突的场景下访问哈希索引一次即可命中。但是哈希索引只适用于等值查询,包括=、IN()、<=>(安全等于,selectnull<=>null和selectnull=null是不一样的结果),不支持范围查询。 + +#### Hash碰撞的处理 + +Hash碰撞是指不同索引列值计算出相同的哈希码,表中字段为A和B两个不同值根据Hash算法计算出来的哈希码都一样就是出现了哈希碰撞。对于 Hash 碰撞通用的处理方法是使用链表,将 Hash 冲突碰撞的元素形成一个链表,发生冲突时在链表上进行二次遍历找到数据。 + +#### MySQL 中如何使用 Hash 索引? + +在 MySQL 中主要是分为 Memory 存储引擎原生支持的 Hash 索引 、InnoDB 自适应哈希索引及 NDB 集群的哈希索引3类。 + +InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。 + +为什么要为B+Tree索引页二次创建自适应哈希索引呢?这是因为B+Tree索引的查询效率取决于B+Tree的高度,在数据库系统中通常B+Tree的高度为3~4层,所以访问数据需要做 3~4 次的查询。而 Hash 索引访问通常一次查找就能定位数据(无 Hash 碰撞的情况),其等值查询场景 Hash 索引的查询效率要优于 B+Tree。 + +自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。功能开启后可以通过 Show Engine Innodb Status 看到当前自适应哈希索引的使用情况。 + +### B+Tree 索引 + +如下图所示为一个简单的、标准的 B+tree,每个节点有 K 个键值和 K+1 个指针。 + + +B+Tree索引能够快速访问数据,就是因为存储引擎可以不再需要通过全表扫描来获取数据,而是从索引的根结点(通常在内存中)开始进行二分查找,根节点的槽中都存放了指向子节点的指针,存储引擎根据这些指针能快速遍历数据。 + +叶子节点存放的 ,对于真正要存放哪些数据还得取决于该 B+Tree 是聚簇索引(Clustered Index)还是辅助索引(Secondary Index)。 + +#### 聚簇索引和辅助索引 + +聚簇索引是一种数据存储方式,它表示表中的数据按照主键顺序存储,是索引组织表。InnoDB的聚簇索引就是按照主键顺序构建B+Tree,B+Tree的叶子节点就是行记录,数据行和主键值紧凑地存储在一起。这也意味着InnoDB的主键索引就是数据表本身,它按主键顺序存放了整张表的数据。而InnoDB辅助索引(也叫作二级索引)只是根据索引列构建B+Tree,但在B+Tree的每一行都存了主键信息,加速回表操作。 + +聚簇索引占用的空间就是整个表数据量的大小,而二级索引会比聚簇索引小很多, 通常创建辅助索引就是为了提升查询效率。 + +在MySQLInnoDB中索引通常可以分为两大类:主键索引(即聚簇索引)和辅助索引(非聚簇索引)。对于没有指定主键的表,InnoDB会自己选择合适字段为主键,其选择顺序如下: + +* 显式主键; +* 第一个唯一索引(要求唯一索引所有列都非 NULL); +* 内置的 6 字节 ROWID。 + +建议使⽤ UNSIGNED 自增列显示创建主键。 + +#### 联合索引和覆盖索引 + +根据索引列个数和功能描述不同索引也可以分为:联合索引和覆盖索引。 + +* 联合索引是指在多个字段联合组建索引的。 +* 当通过索引即可查询到所有记录,不需要回表到聚簇索引时,这类索引也叫作覆盖索引。 +* 主键查询是天然的覆盖索引,联合索引可以是覆盖索引。 + +那么如何看SQL语句是否使用到覆盖索引了呢?通常在查看执行计划时,Extra列为Usingindex则表示优化器使用了覆盖索引。 + +我们通常建议优先考虑使用覆盖索引,这是因为如果SQL需要查询辅助索引中不包含的数据列时,就需要先通过辅助索引查找到主键值,然后再回表通过主键查询到其他数据列(即回表查询),需要查询两次。而覆盖索引能从索引中直接获取查询需要的所有数据,从⽽避免回表进行二次查找,节省IO,效率较⾼。 + +## 索引使用技巧 + +接下来聊一聊索引使用技巧的基础知识,这些知识可以帮助你建立高效索引,主要有谓词、过滤因子、基数(Cardinality)、选择率和回表。 + +#### 谓词和过滤因子 + +谓词本身就是条件表达式,通俗讲就是过滤字段。如下图中这句SQL语句,可以拆解为下面所示: + +`select * from city where city ='BeiJing' and last_updata = '2019-09-01'` + +简单谓词:city和last_updata。组合谓词:cityandlast_updata。 + +知道谓词后就可以计算谓词的过滤因子了,过滤因子直接描述了谓词的选择性,表示满足谓词条件的记录行数所占比例,过滤因子越小意味着能过滤越多数据,你需要在这类谓词字段上创建索引。 + +过滤因子的计算算法,就是满足谓词条件的记录行数除以表总行数。 + +* 简单谓词的过滤因子 = 谓词结果集的数量 / 表总行数 +* 组合谓词的过滤因子 = 谓词 1 的过滤因子 × 谓词 2 的过滤因子 + +下面用一个例子来看下,如何快速根据SQL语句计算谓词、过滤因子。 + +1. 根据SQL语句可以快速得到谓词信息:简单谓词 city 和 last_update,组合谓词 city and last_update。 +2. 计算每个谓词信息的过滤因子,过滤因子越小表示选择性越强,字段越适合创建索引。例如: + * city 的过滤因子=谓词 city 结果集的数量/表总行数=select count(\*) from city where city = ‘BeiJing’ / select countt(\*) from city = 20%; + * \*last_update 的过滤因子 = 谓词 last_update 结果集的数量 / 表总行数 = select count(\*) from city where last_update = ‘2019-08-01’ / select count(\*) from city = 10%; + * 组合谓词 = city 过滤因子 * last_update 过滤因子 = 20% × 10% = 2%,组合谓词的过滤因子为 2%,即只有表总行数的 2% 匹配过滤条件,可以考虑创建组合索引 (city,last_update)。 + +#### 基数和选择率 + +基数(Cardinality )是某个键值去重后的行数,索引列不重复记录数量的预估值,MySQL优化器会依赖于它。选择率是count (distinct city) / count(*),选择率越接近1则越适合创建索引,例如主键和唯一键的选择率都是 1。回表是指无法通过索引扫描访问所有数据,需要回到主表进行数据扫描并返回。 + +Cardinality 能快速告知字段的选择性,高选择性字段有利于创建索引。优化器在选择执行计划时会依赖该信息,通常这类信息也叫作统计信息,数据库中对于统计信息的采集是在存储引擎层进行的。 + +执行 show index from table_name会看到 Cardinality,同时也会触发 MySQL 数据库对 Cardinaltiy 值的统计。除此之外,还有三种更新策略。 + +* 触发统计:Cardinality 统计信息更新发生在 INSERT 和 UPDATE 时,InnoDB 存储引擎内部更新的 Cardinality 信息的策略为:1.表中超过1/16的数据发生变化;2.stat_modified_counter > 2000 000 000 (20亿)。 +* 采样统计(sample):为了减少统计信息更新造成的资源消耗,数据库对 Cardinality 通过采样来完成统计信息更新,每次随机获取 innodb_stats_persistent_sample_pages 页的数量进行 Cardinality 统计。 +* 手动统计:alter table table_name engine=innodb或 analyze table table_name,当发现优化器选择错误的执行计划或没有走理想的索引时,执行 SQL 语句来手动统计信息有时是一种有效的方法。 + +由于采样统计的信息是随机获取8个(8是由innodb_stats_transient_sample_pages参数指定)页面数进行分析,这就意味着下一次随机的8个页面可能是其他页面,其采集页面的 Carinality 也不同。因此当表数据无变化时也会出现 Cardinality 发生变化的情况。 + +## 索引使用细节 + +创建索引后如何确认SQL语句是否走索引了呢?创建索引后通过查看执行SQL语句的执行计划即可知道SQL语句是否走索引。执行计划重点关注跟索引相关的关键项,有type、possible_keys、key、key_len、ref、Extra 等。 + +其中,**possible_keys** 表示查询可能使用的索引,**key**表示真正实际使用的索引,**key_len** 表示使用索引字段的长度。另外执行计划中Extra选项也值得关注,例如**Extra**显示use index时就表示该索引是覆盖索引,通常性能排序的结果是use index>use where>use filsort + +当索引选择组合索引时,通过计算key_len来了解有效索引长度对索引优化也是非常重要的,接下来重点讲解key_len计算规则。key_len表示得到结果集所使用的选择索引的长度[字节数],不包括orderby,也就是说如果orderby也使用了索引则key_len不计算在内。key_len计算规则从两个方面考虑,一方面是索引字段的数据类型,另一方面是表、字段所使用的字符集。 + +索引字段的数据类型,根据索引字段的定义可以分为变长和定长两种数据类型: + +1. 索引字段为定长数据类型,比如char、int、datetime,需要有是否为空的标记,这个标记需要占用1个字节; +2. 对于变长数据类型,比如Varchar,除了是否为空的标记外,还需要有长度信息,需要占用两个字节。 + +表所使用的字符集,不同的字符集计算的 key_len 不一样,例如,GBK 编码的是一个占用 2 个字节大小的字符,UTF8 编码的是一个占用 3 个字节大小的字符。 + +通过key_len计算也帮助我们了解索引的**最左前缀匹配原则**。最左前缀匹配原则是指在使用B+Tree联合索引进行数据检索时,MySQL优化器会读取谓词(过滤条件)并按照联合索引字段创建顺序一直向右匹配直到遇到范围查询或非等值查询后停止匹配,此字段之后的索引列不会被使用,这时计算 key_len 可以分析出联合索引实际使用了哪些索引列。 + +## 设计高性能索引 + +1. **定位由于索引不合适或缺少索引而导致的慢查询。**通常在业务建库建表时就需要提交业务运行相关的SQL给DBA审核,也可以借助ArkcontrolArkit来自动化审核。比如,慢查询日志分析,抓出运行慢的SQL进行分析,也可以借助第三方工具例如Arkcontrol慢查询分析系统进行慢查询采集和分析。在分析慢查询时进行参数最差输入,同时,对SQL语句的谓词进行过滤因子、基数、选择率和SQL查询回表情况的分析。 +2. **设计索引**的目标是让查询语句运行得足够快,同时让表、索引维护也足够快,例如,使用业务不相关自增字段为主键,减缓页分裂、页合并等索引维护成本,加速性能。也可以使用第三方工具进行索引设计,例如Arkcontrol SQL 优化助手,会给出设计索引的建议。 +3. **创建索引策略:**优先为搜索列、排序列、分组列创建索引,必要时加入查询列创建覆盖索引;计算字段列基数和选择率,选择率越接近于1越适合创建索引;索引选用较小的数据类型(整型优于字符型),字符串可以考虑前缀索引;不要建立过多索引,优先基于现有索引调整顺序;参与比较的字段类型保持匹配并创建索引。 +4. **调优索引:**分析执行计划;更新统计信息(AnalyzeTable);Hint优化,方便调优(FORCEINDEX、USEINDEX、IGNOREINDEX、STRAIGHT_JOIN);检查连接字段数据类型、字符集;避免使用类型转换;关注 optimizer_switch,重点关注索引优化特性 MRR(Multi-Range Read)和 ICP(Index Condition Pushdown)。 + * MRR优化是为了减少磁盘随机访问,将随机IO转化为顺序IO的数据访问,其方式是将查询得到辅助索引的键值放到内存中进行排序,通常是按照主键或RowID进行排序,当需要回表时直接根据主键或 RowID 排序顺序访问实际的数据文件,加速 SQL 查询。 + * ICP优化同样也是对索引查询的优化特性,MySQL根据索引查询到数据后会优先应用where条件进行数据过滤,即无法使用索引过滤的where子句,其过滤由之前Server层的数据过滤下推到了存储引擎层,可以减少上层对记录的检索,提高数据库的整体性能。 + +## 创建索引规范 + +* 命名规范,各个公司内部统一。 +* 考虑到索引维护的成本,单张表的索引数量不超过5个,单个索引中的字段数不超过5个。 +* 表必需有主键,推荐使⽤UNSIGNED自增列作为主键。表不设置主键时InnoDB会默认设置隐藏的主键列,不便于表定位数据同时也会增大MySQL运维成本(例如主从复制效率严重受损、pt工具无法使用或正确使用)。 +* 唯一键由3个以下字段组成,并且在字段都是整形时,可使用唯一键作为主键。其他情况下,建议使用自增列或发号器作主键。 +* 禁止冗余索引、禁止重复索引,索引维护需要成本,新增索引时优先考虑基于现有索引进行rebuild,例如(a,b,c)和(a,b),后者为冗余索引可以考虑删除。重复索引也是如此,例如索引(a)和索引(a,主键ID) 两者重复,增加运维成本并占用磁盘空间,按需删除冗余索引。 +* 联表查询时,JOIN 列的数据类型必须相同,并且要建⽴索引。 +* 不在低基数列上建⽴索引,例如“性别”。 在低基数列上创建的索引查询相比全表扫描不一定有性能优势,特别是当存在回表成本时。 +* 选择区分度(选择率)大的列建立索引。组合索引中,区分度(选择率)大的字段放在最前面。 +* 对过长的Varchar段建立索引。建议优先考虑前缀索引,或添加CRC32或MD5伪列并建⽴索引。 +* 合理创建联合索引,(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c)。 +* 合理使用覆盖索引减少IO,避免排序。 + +# 查询优化 + +MySQL采用基于开销的优化器,以确定处理查询的最佳方式,也就是说执行查询之前,都会先选择一条自以为最优的方案。在很多情况下,MySQL能够计算最佳的可能查询计划,但在某些情况下,MySQL没有关于数据的足够信息,或者是提供太多的相关数据信息,它所采用的可能并非就是事实上的最优方案。这里举了两个例子来说明。 + +### 案例一 + +**Range Query Optimizer的流程** + +1. 根据查询条件计算所有的 possible keys。 +2. 计算全表扫描代价(cost_all)。 +3. 计算最小的索引范围访问代价(这一步很关键,直接决定了 Range 的查询效率),它有三步: + 1. 对于每一个possible keys(可选索引),调用records_in_ranges函数计算范围中的rows; + 2. 根据rows,计算二级索引访问代价; + 3. 获取cost最小的二级索引访问(cost_range)。 +4. 选择执行最小化访问代价的执行计划。如果 cost_all <= cost_range,则全表扫描,否则索引范围扫描。 + +Range使用了records_in_range函数估算每个值范围的rows,结果依赖于possible_keys;possible_keys越多,随机IO代价越大,Range查询效率。所以,索引不是越多越好,相反,我们应该尽量减少possible_keys,减少records_in_range从而减少IO的消耗。这里给大家推荐两个工具,用pt-index-usage工具来删除冗余索引,用 pt-duplicate-key-checker 工具来删除重复索引。 + +### 案例二 + +优化前有一个索引idx_global_id。图中的这条SQL语句的where条件包括一个sub_id的等值查询和一个global_id的范围查询。执行一次需要2.37秒。从下一页的执行计划中,我们可以看到虽然查询优化器使用了唯一索引uniq_subid_globalid,但是由于idx_global_id的干扰,实际只使用了前面的4个长度就access,剩余8个长度都被filter了。 + +从优化后的执行计划中可以看到,使用了forceindex来强制使用唯一索引。正如上文列举的,相似的命令还有ignoreindex忽略索引,straght_join强制优化器按特定的顺序使强制优化器按特定的顺序使用数据表,high_priority 或 low_priority 两个命令来控制 SQL 的执行优先权。 + +### ICP,MRR,BKA + +1.ICPICP是IndexConditionPushdown的简称,是MySQL使用索引从表中检索行数据的一种优化方式。目的是减少从基表中全记录读取操作的数量,从而降低IO操作。 + +在没有开启ICP之前,存储引擎会通过遍历索引查找基表中的行,然后返回给MySQLServer层,再去为这些数据行进行where后的条件过滤。开启ICP之后,如果部分where条件能使用索引中的字段,MySQLServer会把这部分下推到存储引擎层,存储引擎通过索引过滤,把满足的行从表中读取出来。ICP能减少引擎层访问基表的次数和MySQLServer访问存储引擎的次数。对于 InnoDB 表来说,ICP 只适用于辅助索引. + +2.MRR + +MRR 是 Multi-Range Read 的简称,是 MySQL 优化器将随机 IO转化为顺序IO以降低查询过程中IO开销的一种手段。MRR的适用场景是辅助索引,如INDEX(key1),查询key1在n到m范围内的数据。使用限制就是MRR,MR适用于range、ref、eq_ref的查询。 + +3.BKA和BNLBKA是BatchedKeyAccess的简称,是MySQL优化器提高表join性能的一种手段,它是一种算法。而BNL是BlockNestedLoop的简称,它是默认的处理表join的方式和算法。那么二者有什么区别呢? + +•BNL比BKA出现的早,BKA直到MySQL 5.6 版本才出现,而 BNL 至少在 MySQL 5.1 版本中就存在了; + +• BNL 主要用于被 join 的表上无索引时; + +• BKA 只在被 join 表上有索引时可以使用,那么就在行提交给被 join 的表之前,对这些行按照索引字段进行排序,因此减少了随机 IO,排序才是两者最大的区别,但如果被 join 的表没有索引呢?那么就只能使用 BNL 了。 + +使用BKA需要注意一些问题,比如:BKA的适用场景支持inner join、outer join、semi-join operations、including nested outer joins等; + +BKA 有两个使用限制,一个是使用 BKA 特性,必须启用 MRR 特性;二是 BKA 主要适用于 join 的表上有索引可使用的情况,否则只能使用 BNL。 + +## MySQL执行计划分析三部曲 + +当有慢查询或者执行 SQL 遇到瓶颈时,我们分析这类问题时可以参考 MySQL 执行计划分析“三步曲”。 + +1. 查看 SQL 执行计划: + * explain SQL; + * desc 表名; + * show create table 表名。 +2. 通过 Profile 定位 QUERY 代价消耗: + * setprofiling=1; + * 执行SQL; + * show profiles;获取Query_ID。 + * show profile for query Query_ID;查看详细的profile信息 +3. 通过Optimizer Trace表查看SQL执行计划树: + * set session optimizer_trace = 'enabled = on'; + * 执行SQL; + * 查询information_schema.optimizer_trace 表,获取 SQL 查询计划树; + * set session optimizer_trace=‘enabled=off';开启此项影响性能,记得用后关闭。 + +## 查询相关参数和分析工具 + +MySQL 可以通过设置一些参数,将运行时间长或者非索引查找的 SQL 记录到慢查询文件中。可以分析慢查询文件中的 SQL,有针对性的进行优化。 + +* 参数slow_query_log,表示是否开启慢查询日志,ON或者1表示开启,OFF或者0表示关闭。 +* 参数long_query_time,设置慢查询的阈值,MySQL5.7版本支持微秒级。 +* 参数slow_query_log_file,慢查询文件的存放路径。 +* 参数log_queries_not_using_indexes,表示是否将非索引查找的SQL也记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 log_throttle_queries_not_using_indexes,表示每分钟记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 max_execution_time,用来控制 SELECT 语句的最大执行时间,单位毫秒,超过此值MySQL 自动 kill 掉该查询。 + + + + +如上图所示是一个慢查询的例子,通过这个例子你可以看到慢查询文件中记录了哪些信息。包括了慢SQL产生的时间,SQL源自的IP和对应的数据库用户名,以及访问的数据库名称;查询的总耗时,被lock 的时间,结果集行数,扫描的行数,以及字节数等。当然还有具体的 SQL 语句。 + +分析慢查询常用的工具有: + +explain; + +Mysql dump slow,官方慢查询分析工具; + +pt-query-digest,Percona公司开源的慢查询分析工具; + +vc-mysql-sniffer,第三方的慢查询抓取工具; + +pt-kill,Percona公司开源的慢查询kill工具,常用于生产环境的过载保护。 + +这里重点介绍pt-query-digest,它是用于分析MySQL慢查询的一个常用工具,先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间、次数、占比等,同时把分析结果输出到文件中。也可以结合 Anemometer 工具将慢查询平台化展示。 +## 如何优化SQL + +1. 全表扫描还是索引扫描。对于小表来说,二者IO调用次数和返回时间相差不大;但对于大表,如果全表扫描,那么查询返回的时间就会很长,就需要使用索引扫描加快查询速度。但并不是要求DBA根据每一种查询条件组合都要创建索引,索引过多也会降低写入和修改的速度,而且如果导致表数据和索引数据比例失调,也不利于后期的正常维护。 + + + +2. 如何创建索引,在哪些列上建立索引适合业务需求?一般情况下,你可以在选择度高的列上创建索引,也可以在status列上创建索引。创建索引时,要注意避免冗余索引,除非一些特殊情况外。如index(a,b,c)和index(a),其中a的单列索引就是冗余索引。 +3. 创建索引以后,尽量不要过频修改。业务可以根据现有的索引情况合理使用索引,而不是每次都去修改索引。能在索引中完成的查找,就不要回表查询。比如SELECT某个具体字段,就有助于实现覆盖索引从而降低IO次数,达到优化SQL的目的。 +4. 多表关联的SQL,在关联列上要有索引且字段类型一致,这样MySQL在进行嵌套循环连接查找时可以使用索引,且不会因为字段类型不一致或者传入的参数类型与字段类型不匹配的情况,这样就会导致无法使用索引,在优化SQL时需要重点排查这种情况。另外索引列上使用函数也不会涉及索引。多表关联时,尽量让结果集小的表作为驱动表,注意是结果集小的表,不是小表。 +5. 在日常中你会发现全模糊匹配的查询,由于MySQL的索引是B+树结构,所以当查询条件为全模糊时,例如‘%**%’,索引无法使用,这时需要通过添加其他选择度高的列或者条件作为一种补充,从而加快查询速度。当然也可以通过强制SQL进行全索引扫描,但这种方式不好,尽量不要在SQL中添加hints。对于这种全模糊匹配的场景,可以放到ES或者solr中解决。尽量不要使用子查询,对子查询产生的临时表再扫描时将无索引可查询,只能进行全表扫描,并且MySQL对于出现在from中的表无所谓顺序,对于where中也无所谓顺序,这也是可以优化SQL的地方。 +6. 另外orderby/groupby的SQL涉及排序,尽量在索引中包含排序字段,并让排序字段的排序顺序与索引列中的顺序相同,这样可以避免排序或减少排序次数。 +7. 除此之外,复杂查询还是简单查询?貌似总会面临这样的疑问和选择。不要总想着用一个SQL解决所有事情,可以分步骤来进行,MySQL也十分擅长处理短而简单的SQL,总体耗时会更短,而且也不会产生臃肿的 SQL,让人难以理解和优化。 + +常用的SQL编写规范如下所示。 + +* SELECT只获取必要的字段,禁止使用SELECT*。这样能减少网络带宽消耗,有效利用覆盖索引,表结构变更对程序基本无影响。 +* 用IN代替OR。SQL语句中IN包含的值不宜过多,应少于1000个。过多会使随机IO增大,影响性能。 +* 禁止使用orderbyrand()。orderbyrand()会为表增加几个伪列,然后用rand() 函数为每一行数据计算 rand() 值,最后基于该行排序,这通常都会生成磁盘上的临时表,因此效率非常低。建议先使用 rand() 函数获得随机的主键值,然后通过主键获取数据。 +* SQL中避免出现now()、rand()、sysdate()、current_user()等不确定结果的函数。在语句级复制场景下,引起主从数据不一致;不确定值的函数,产生的SQL语句无法使用QUERY CACHE。 +