本文是mysql45讲的1-5的总结
文章目录
- 基础架构
- 连接器
- 分析器
- 优化器
- 执行器
- SQL查询执行过程
- 详细执行步骤
- SQL更新执行过程
- 重要的日志模块:redo log
- 重要的日志模块:binlog
- 阶段性提交
- 事务
- 事务隔离的实现
- 启动
- 索引
- 数据库索引模型
- InnoDB索引组织结构
- 主键选择
- B+树维护操作
- 索引重建问题
- 覆盖索引
- 最左前缀原则
- 索引下推
- 策略选择和权衡
- 建议
基础架构
先来看一张图:
MySQL分为Server层和存储引擎层两部分。
Server层:连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。
不同存储引擎的表数据存取方式不同,支持的功能也不同
从图中不难看出,不同的存储引擎共用一个Server层,也就是从连接器到执行器的部分。
连接器
顾名思义就是负责跟客户端建立连接、获取权限、维持和管理连接的
我们通常命令行会这么写:
mysql -h$ip -P$port -u$user -p
用户的权限是在登录时就确认好的, 也就是说如果在登录过程中对当前用户的权限进行了修改,当前已登录用户的权限并不会发生改变,直到用户再次登录,权限才会更新
我们可以通过
show processlist
对当前链接用户进行查看
客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数wait_timeout控制的,默认值是8小时。
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。
分析器
作用:语句词法分析
语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句select少打了开头的字母“s”。
mysql> elect * from t where ID=1;ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
优化器
在真正执行之前还需要一个优化,比如说当表里面有多个索引的时候,有多个join的时候,谁先谁后,这就是优化器要决定的事情,如何使用让效率变得更高
执行器
分析器知道了要怎么做,优化器知道了该怎么做,于是开始执行;
执行优化器选择的执行计划,与存储引擎协同工作,完成数据的实际检索和修改。
执行器会先查看是否有表读取权限,如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
SQL查询执行过程
- 查询示例:
select * from T where ID=10;
- 输入:用户输入的SQL查询语句。
- 输出:查询结果。
详细执行步骤
-
SQL解析:
- 词法分析:将SQL语句分解成有意义的元素(词法单元),如关键字、标识符、常量。
- 语法分析:根据MySQL的语法规则验证词法单元的结合是否正确。
-
查询优化:
- 执行计划生成:优化器评估多种可能的查询路径,生成多个执行计划。
- 成本估算:为每个执行计划估算执行成本,选择成本最低的计划。
-
查询执行:
- 执行器与存储引擎交互:执行器指挥存储引擎按照优化后的计划执行查询。
- 数据检索:存储引擎从数据文件中检索数据,返回给执行器。
SQL更新执行过程
更新里面其实就是查询+修改
MySQL可以恢复到半个月内任意一秒的状态,那是怎么做到的?
与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。
重要的日志模块:redo log
其实就是MySQL里经常说到的WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos和checkpoint之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
要理解crash-safe这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
重要的日志模块:binlog
Server层也有自己的日志,称为binlog(归档日志)
因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。
这两种日志有以下三点不同。
- redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
- redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
更新过程
- 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的binlog,并把binlog写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
阶段性提交
最后三步看上去有点“绕”,将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交”
binlog会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的DBA承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
为什么日志需要“两阶段提交”。这里不妨用反证法来进行解释。
由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的update语句来做例子。假设当前ID=2的行,字段c的值是0,再假设执行update语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了crash,会出现什么情况呢?
-
先写redo log后写binlog。假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。
但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。
然后你会发现,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。
-
先写binlog后写redo log。如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
不只是误操作后需要用这个过程来恢复数据。当你需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用binlog来实现的,这个“不一致”就会导致你的线上出现主从数据库不一致的情况。
简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
事务
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
隔离得越严实,效率就会越低。因此我们在二者之间寻找一个平衡点。
SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable ):
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
Oracle数据库的默认隔离级别其实就是“读提交”,因此对于一些从Oracle迁移到MySQL的应用,为保证数据库隔离级别的一致,你一定要记得将MySQL的隔离级别设置为“读提交”。
事务隔离的实现
在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
假设一个值从1被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录:
,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
回滚日志会当系统里没有比这个回滚日志更早的read-view的时候被删除
为什么不要用长事务
- 长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
- 长事务还占用锁资源,也可能拖垮整个库
启动
MySQL的事务启动方式有以下几种:
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。
有些客户端连接框架会默认连接成功后先执行一个set autocommit=0的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。
因此,我会建议你总是使用set autocommit=1, 通过显式语句的方式来启动事务。
但是有的开发同学会纠结“多一次交互”的问题。对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一次 “begin”,减少了语句的交互次数。如果你也有这个顾虑,我建议你使用commit work and chain语法。
在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
你可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
索引
数据库索引模型
- 哈希表
- 适用于等值查询,比如Memcached及其他一些NoSQL引擎
- 查询效率高,但不支持范围查询
- 插入删除效率低,可能会出现哈希冲突
- 有序数组
- 在等值查询和范围查询场景中的性能就都非常优秀
- 查询效率高
- 插入删除效率低,需要移动数据
- 有序数组索引只适用于静态存储引擎
- B+树
- InnoDB采用B+树作为索引模型
- 支持等值查询和范围查询
- 查询效率高,插入删除效率也较高
InnoDB索引组织结构
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)。
- 主键索引对应聚集索引
- 数据按主键物理顺序存储;主键查询只需要搜索B+树就行
- 辅助索引的叶子节点为主键值
- 通过主键索引查找数据,普通索引需要先搜索引树得到ID的值,在到ID索引树搜索,称为回表操作;
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询
主键选择
- 自增主键
- 插入顺序,不会触发分裂
- 空间占用小
- 业务主键
- 更符合实际需求
- 也可以直接作为主键索引
B+树维护操作
- 插入如果导致页满,需要分裂页
- 删除如果导致页利用率低,需要合并页
索引重建问题
- 直接使用ALTER TABLE效率可能不高
- 应考虑使用线上DDL或备份还原方式
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
有没有什么场景适合用业务字段直接做主键的呢?还是有的。比如,有些业务的场景需求是这样的:
- 只有一个索引;
- 该索引必须是唯一索引。
覆盖索引
- 定义:如果一个索引包含了查询所需的所有字段,则称为覆盖索引。
- 优点:能够减少数据库访问成本,因为可以直接在索引中获取数据,无需回表到主数据文件。
- 应用:例如,在查询
SELECT ID FROM T WHERE k BETWEEN 3 AND 5
中,如果索引k
已经包括ID
字段,则此索引覆盖了查询需求。
最左前缀原则
- 定义:在使用复合索引时,查询条件可以使用索引的最左边的一个或多个字段来优化查询。
- 优点:使得数据库能够利用索引快速定位数据,减少扫描范围。
- 实例:在联合索引
(name, age)
中,查询可以利用name
或name
和age
的组合来加速,但不能只用age
。
索引下推
- 定义:MySQL 5.6及以上版本的优化技术,允许在索引层面过滤数据,而不是在找到完整记录后才进行过滤。
- 优点:减少了不必要的数据加载和回表操作,提高查询效率。
- 示例:在
name, age
索引中,查询SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10
可以直接在索引层过滤掉不符合age
条件的记录。
策略选择和权衡
- 联合索引的选择:在创建联合索引时,应考虑查询模式以及字段在查询中的频率和顺序。正确的字段顺序可以减少冗余索引,优化存储和维护成本。
- 索引的成本:尽管索引可以提高查询性能,但它们也会增加写操作的负担,并占用额外的存储空间。因此,在添加索引前需要权衡其对性能的潜在影响。
建议
- 覆盖索引使用建议:对于高频查询特别是在只需要表中少数几个字段的情况下,考虑使用覆盖索引。
- 索引审查:定期审查现有索引的效能和必要性,移除不再有效或重复的索引,优化索引配置