深入了解Mysql

基础架构

MySql基础架构示意图如下:

20190303155160107425396.png

Server层

主要负责Mysql功能层面的事情

连接器

当你尝试连接到数据库时,接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。

连接后,如果你没有后续动作,这个连接就处于空闲状态。如果客户端太长时间没有动静,连接器就会自动将它断开,这个时间是由wait_timeout控制的,默认是8小时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> show processlist;
+----+------+-----------+------+---------+------+----------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+------+---------+------+----------+------------------+
| 3 | root | localhost | NULL | Sleep | 76 | | NULL |
| 5 | root | localhost | NULL | Query | 0 | starting | show processlist |
+----+------+-----------+------+---------+------+----------+------------------+
2 rows in set (0.00 sec)
mysql> show variables like '%wait_time%';
+--------------------------+----------+
| Variable_name | Value |
+--------------------------+----------+
| innodb_lock_wait_timeout | 50 |
| lock_wait_timeout | 31536000 |
| wait_timeout | 28800 |
+--------------------------+----------+
3 rows in set (0.00 sec)

建立连接的过程通常是比较复杂的,所以建议尽量使用长连接。但全部使用长连接后,你会发现有些时候MySQL占用内存涨得特别快,这是因为Mysql在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开时才会释放,所以长期累计下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象上来看就是Mysql异常重启了。

怎么解决这个问题呢?

  • 定期断开连接
  • 如果你用的是Mysql5.7或更新版本,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存

大多数情况下应该关闭查询缓存。因为查询缓存的失效非常频繁,只要对一个表的更新,这个表上所有的缓存都会被清空

需要注意的是,MySql8.0版本直接将查询缓存整块功能删除了。

分析器

MySQL需要知道你要做什么,因此需要对SQL语句做解析。解析分两部分:

  • 词法分析
  • 语法分析

优化器

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者是在一个语句有多表关联(join)的时候,决定各表的连接顺序。

执行器

执行语句。首先进行权限判断,然后打开表,根据表的引擎定义,调用引擎提供的接口。

1
select * from T where ID= 10;

比如这个例子,表T中ID字段没有索引,那么执行器的执行流程是这样的:

1.调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过;如果是则将这行存在结果集中;

2.调用引擎接口取”下一行”,重复相同的判断逻辑,直到取到这个表的最后一行

3.执行器将上述遍历过程中所有满足条件的行自称的记录集作为结果集返回给客户端

引擎层

主要负责存储相关的具体事宜

日志系统

与查询不同,更新流程涉及两个重要的日志模块:redo log和binlog

redo log

在MySQL中,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高。为了解决这个问题,MySQL的设计者就用类似酒店掌柜粉板的思路来提升更新效率。。

而粉板和账本配合的整个过程,其实就是Mysql中常说的WAL技术,WAL的全程是Write-Ahaed Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。具体来说,当一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log中,并更新内存,这时更新就算完成了。同时,innodb引擎会在适当的时候,将这个操作记录更新到磁盘中,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事情。

有了redo log,innoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力也成为crash-safe

为了理解crash-safe这个概念,可以想想前面赊账记录的例子。只要赊账记录记载了粉板或写在账本上,之后即使掌柜忘了,比如停业几天,恢复生意后依旧可以通过账本和粉板上的数据明确赊账账目。

bin log

redo log是innoDB特有的日志,而Server层也有自己的日志,成为binlog
binlog中记录了所有的DDL和DML语句,binlog的主要目的是复制和恢复

为什么会有两份日志呢?

最初没有innoDB引擎,Mysql自带的是myisam引擎,但是myisam没有crash-safe的能力,binlog日志只能用于归档,既然binlog没有该能力,那么innoDB就使用另外一套日志系统来实现crash-safe的能力

binlog与redolog有什么不同?

  • 1.redo log是innoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用

  • 2.redo log是物理日志,记录的是”在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如”给ID=2这一行的c字段加1”

  • 3.redo log是循环写的,空间固定会用完;binlog是可以追加写的。“追加写”指的是bin log文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

有了这两个日志的概念理解,我们再看执行器和innoDB引擎在执行update语句时的内部流程

1
mysql> update T set c=c+1 where ID=2;

1.执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读取内存,然后再返回。

2.执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

3.引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里,此时redo log处于prepare状态,然后告知执行器执行完成了,随时可以提交事务

4.执行器生成这个操作的binlog,并把binlog写入磁盘

5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成

以下是这个update语句的执行流程图,图中浅色框表示innoDB内部执行的,深色框表示是在执行器中执行的

20190303155161222740287.png

最后三部redo log的写入拆成了两个步骤:prepare和commit,这就是“两阶段提交”

两阶段提交

为什么必须有两阶段提交呢
  1. 先写redo log后写binlog,如果redo log写成功后crash,那么binlog没有这条记录,事后恢复时这条记录就会丢失,不符合预期

  2. 先写binlog在写redo log,如果binlog写成功后crash,由于redo log还没写,崩溃恢复后这个事务无效,但是binlog里面已经实际写了这一条,也就是说用binlog恢复时多了一个事务,同样不符合预期。

所以需要两阶段提交

为什么使用两阶段提交可以保障一致性呢

我们先列出两阶段提交的顺序阶段

1.prepare阶段 2写binlog 3.commit

如果在2之前崩溃,重启恢复时发现没有commit,回滚。一致

如果在3之前崩溃,重启恢复时发现虽然没有commit,但是prepare和binlog完整,所以恢复时会自动commit。一致

两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案

事务隔离

ACID

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

隔离等级

数据库上如果有多个事务同时执行,会产生脏读、幻读等问题,为解决这些问题,于是就有了隔离等级的概念

  • 未提交读,指一个事务还没提交时,它做的变更就能被其他事务看到
  • 提交读,指一个事务提交之后,它做的变更才能被其他事务看到
  • 可重复读,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
  • 串行化,顾名思义对于同一行记录,写会加写锁,读会加读锁。当出现读写锁冲突时,后访问的事务必须等前一个事务执行完成,才能继续执行。

s201903031551626257240.png

在不同隔离等级下,事务A会有那些不同的返回结果?

  • 若隔离等级是未提交读,则V1是2,V2是2,V3是2
  • 若隔离等级是提交读,则V1是1,V2是2,V3是2
  • 若隔离等级是可重复读,则V1是1,V2是1,V3是2
  • 若隔离等级是串行化,则事务B执行“将1改为2”时,会锁住。知道事务A提交后,事务B才能继续执行。V1是1,V2是1,V3是1

脏读: 一个事务读取到了另一个事务未提交的数据,提交读及以上可避免

幻读:一个事务插入了一个新行,另一个事务查不到,但是插入相同id出错,串行化可避免

事务隔离的实现

MVCC

深入浅出索引

性能提升

短连接风暴

超过Max_connection报错提示”too many connections”

在机器负载比较高时,处理现有请求的时间变长,每个连接保持的时间也更长,这时,再有新建连接的话,就可能会超过max_connections的限制。

碰到这种情况时,简单的想法是提高max_connections值,但这么做的话会让更多连接都进来,那么系统的负载会进一步加大,结果可能是适得其反,已经连接的线程拿不到CPU资源去执行业务的SQL请求

那么这种情况下有没有别的建议呢?这里有两种方法,注意这两种方法都是有损的

  • 先处理掉那些占着连接但是不工作的线程

  • 慢查询性能问题

慢查询性能问题

QPS突增问题

MySQL主备

MySQL主备基本原理

如下是基本的主备切换流程

20190312155239570193775.png

主备同步内部流程

20190312155239633082645.png

可以看到: 主库接收到客户端的请求更新后,执行内部事务的更新逻辑,同时写binlog.

备库B和主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程是这样的:

1.在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量。

2.在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread,其中io_thread负责与主库建立连接

3.主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B

4.备库B拿到binlog后,写到本地文件,成为中转日志(relay log)

5.sql_thread读取中转日志,解析出日志里的命令,并执行

后来多线程复制方案的引入,sql_thread演化成了多个线程

循环复制问题

生产环境中使用比较多的是主主结构

20190312155239744653006.png

日志执行流如下

1.从节点A更新的事务,binlog里面记得都是A的server id;

2.传到节点B执行一次以后,节点B生成额binlog的server id也是A的server id;

3.再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志,所以,死循环在这里就断掉了。

MySQL是怎么保证高可用的

主备延迟

1
2
# 查看seconds_behind_master,表示当前备库延迟了多少秒
show slave status

主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产binlog的速度要慢。

主备延迟的来源

备库物理机性能比主库机器性能差
备库压力大

由于主库直接影响业务,大家使用比较克制,反而忽略了备库的压力控制。

此时一般可以这么处理:

1.一主多从

2.通过binlog输出到外部系统,比如hadoop等,让外部系统提供统计类查询的能力。

大事务

大事务这种情况很好理解,因为主库上必须等待事务执行完成后才会写入binlog。如果事务本身耗时10分钟,那么这个事务很可能会导致从库延迟10分钟。

常有DBA说,不要一次性地用delete语句删除太多数据,其实这是一个典型的大事务

另一种大事务场景,就是大表DDL

备库的并行复制能力

可靠性优先策略

在双M结构下,从状态1到状态2切换的详细过程是这样的

  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步

  2. 把主库A改成只读状态,即把readonly设置为true;

  3. 判断备库B的seconds_behind_master的值,知道这个值变成0位置;

  4. 把备库B改成可读写状态,也就是把readonly设置为false

  5. 把业务请求切到备库B

这个切换流程,通常是由专门的HA系统来完成的,我们暂时可称之为可靠性优先流程

20190312155239885369970.png

如图可见,这个切换流程是有不可用时间的。我们可以选择可用性优先策略,来把这个不可用时间几乎降为0.

可用性优先策略

可用性优先策略有可能导致主备数据不一致

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

5.6版本之前,只支持单线程复制

多线程复制

所有的多线程复制机制,都是把图1中只有一个线程的sql_thread,拆成多个线程,也就是符合下面的这个模型

img

FAQ

1.mysqldump如何不阻塞的导出数据

--single-transaction该选项在导出数据前会开启一个事务,不会阻塞任何应用程序且能保证导出时数据库的一致性状态。
1.group_concat长度限制
group_concat长度默认限制1024,可以通过set group_concat_max_len=102400调整长度限制