• mysql 执行步骤

图片

show processlist//显示所有进程

过程讲解:

图片 总结

执行一条 SQL 查询语句,期间发生了什么?

  • 连接器:建立连接,管理连接、校验用户身份;

  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;

  • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;

  • 执行 SQL:执行 SQL 共有三个阶段:

    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。

    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;

    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

  • redo log 及 binlog

redo:

图片 redo属于存储引擎层(如InnoDB),记录了数据的偏移,适当时间会更新到硬盘。环形结构。

redo log 是物理日志,记录的是“在某个数据页上做了什么修改”

binlog:

binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”

更新时逻辑,两阶段提交。

先redo log(prepare阶段),再binlog,最后commit 图片

  • 隔离级别

图片

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

尽量不要使用长事务

当没有比回滚日志更早的读视图(读视图在事务开启时创建)的时候,这个数据不会再有谁驱使它回滚了,这个回滚日志也就失去了用武之地,可以删除了

-- 查询超过60s的长事务
select * from information_schema.innodb_trx 
where TIME_TO_SEC(timediff(now(),trx_started))>60

MySQL 的事务启动方式有以下几种:

  1. 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。

  2. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

建议使用 set autocommit=1, 通过显式语句的方式来启动事务

如何避免长事务对业务的影响?

首先,从应用开发端来看:

  1. 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。

  2. 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。

  3. 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

其次,从数据库端来看:

  1. 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;

  2. Percona 的 pt-kill 这个工具不错,推荐使用;

  3. 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;如果使用的是 MySQL  5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

  • MVCC

图片

基于以上,在可重复读读已提交两个事务隔离级别中,在执行普通 SELECT 语句时都会获取 Read View,并针对行记录的版本链进行遍历,并遵循如下规则:

1)如果遇到某个版本的事务 id 等于当前事务 id,直接选取该版本(同一个事务修改的内容,哪怕是草稿态也要读取到)

2)遍历找到首个事务 id 不在 trx_list 的事务(代表该版本是已提交的最新版数据)作为选取的版本

 

而可重复读和读已提交的区别就在于:

  • • 【读已提交】 会在每次执行 SELECT 查询时,实时获取最新的 Read View 视图

  • • 【可重复读】 只在事务开启时获取一次 Read View并在整个生命周期进行复用. 同时针对上述第 2)条,还需要保证选取版本的事务 id < low_limit_id. 这样就能屏蔽事务开启后其它并发事务的一切修改行为,进而保证当前事务视角的一致性

  • 索引(上)

N叉树:

 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了

每个节点存放索引个数:

MySql默认一个节点的长度为16K,一个整数(bigint)字段索引的长度为 8B,另外每个索引还跟着6B的指向其子树的指针;所以16K/14B ≈ 1170

基于非主键索引的查询需要多扫描一棵索引树,即回表。因此,我们在应用中应该尽量使用主键查询

页分裂与合并:

  1. 新增数据可能页分裂,需申请新数据页,挪动部分数据过去,性能会受影响,空间利用率也会降低

  2. 删除数据可能页合并, 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程

尽量使用自增主键索引:

因为普通索引的叶子节点存放的是主键,所以主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小,尽量使用自增主键

重建主键索引是否合理?

重建二级索引 k 的做法是合理的,可以达到省空间的目的。

但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,你可以用这个语句代替 :

alter table T engine=InnoDB
  • 索引(下)

覆盖索引

是否联合索引,都可覆盖

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段

联合索引- 最左前缀原则

最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符

第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的

当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引

联合索引-索引下推

我以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”

select * from tuser where name like '张%' and age=10 and ismale=1;

MySQL 5.6 之前,name满足后,回表,查询age、ismale是否满足。

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。 也就是name满足后,判断age是否满足,满足后再回表,减少回表次数

  • 全局锁 和 表锁

全局读锁:

全局读锁①,会话结束时,锁会失效

Flush tables with read lock (FTWRL)

全局读锁②,可用来做主从库判断

-- 如果客户端发生异常,则数据库就会一直保持 readonly 状态,风险较大
set global readonly=true

mysqldump 数据备份

当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的

表锁:

锁1:

与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放.表锁的语法是:lock tables … read/write 锁2:

增删改查:加 MDL 读锁

结构变更操作:加MDL 写锁

另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性

名称解释:

DML:增删改数据

DDL:修改表结构的操作

MDL(meta data lock) :元数据锁

如何安全地给小表加字段?

首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务

要是请求很频繁:

这时候 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程

MariaDB 已经合并了 AliSQL 的这个功能:

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...