Skip to content

Mysql 架构

字数: 0 字 时长: 0 分钟

MySQL架构.webp

MySQL 架构主要分为 4 层:

  • 连接层:负责用户连接 MySQL 服务器,并验证用户权限,分配线程资源。
  • 服务层:负责处理用户请求,执行 SQL 语句,返回结果给用户。
  • 引擎层:可插拔的存储引擎,最常用的是 Innodb ,真正负责与文件系统交互的部分
  • 文件层:所有数据实际上都是存储在文件系统中的,与存储引擎进行交互。

连接层

客户端与 MySQL 服务器经过三次握手建立连接成功后,MYSQL 首先会对 TCP 传输过来的账号密码做身份验证,权限获取:

  • 用户名或密码错误,返回错误信息,客户端结束运行
  • 用户名密码正确,从权限表查询该用户相应权限,之后每次请求都需要判断权限是否允许

TCP 连接收到请求后,必须要分配一个独立线程与客户端进行交互。MySQL 企业版内置了线程池插件。

MySQL 社区版默认是不支持 线程池 的,每个新连接会创建独立线程,因此高并发情况下性能会严重劣化。

不过可以通过配置优化来缓解该问题:

shell
[mysqld]
thread_cache_size = 100 # 空闲线程缓存,避免频繁创建/销毁 

max_connections = 500 # 允许的最大连接数
max_user_connections = 50 # 每个用户的最大连接数

# 设置连接超时,快速回收空闲线程资源
wait_timeout = 60 # 非交互式连接空闲超时
interactive_timeout = 60 # 交互式(如 Mysql 命令行客户端)连接空闲超时

back_log = 200 # 等待队列,避免瞬间连接数爆炸

此外,客户端可以利用连接池复用连接,间接减少服务端线程数。

服务层

以一条 SQL 语句的执行流程来理解 MySQL 的逻辑架构

Mysql 执行流程.webp

1. SQL Interface 接口

用户的 SQL 语句会首先到达 SQL Interface 接口,这里会有两种情况:

  • 如果 SQL 在缓存中有,则直接在缓存中取出结果,然后直接通过该接口响应给用户 (不过 MySQL 8 已经废弃了缓存这个架构设计)
  • 如果没有缓存,则走后续流程

为什么 MySQL 8 要废除缓存?

  • 只有完全相同的查询才会被缓存,select * from userselect * from user 哪怕中间只是多了一个空格,都不会命中
  • 对同一条 SQL 语句,如果数据发生了增、删、改操作,结果也会不同,缓存失效

因此在大多数情况下,缓存命中率非常低,维护缓存的开销甚至超过了缓存带来的性能提升

2. 分析器

分析器会先做 词法分析 。 SQL 语句是由多个字符串和空格组成的,MySQL 需要识别这些字符串代表着什么,比如select 关键字代表着查询操作;字符串t_user识别为表名;字符串id识别为列名。

然后会根据词法分析的结果,进行 语法分析判断 SQL 语句是否满足 MySQL 语法

如果 SQL 语句正确,会生成语法树:

语法树.webp

3. 优化器

优化器会根据分析结果,确定 SQL 语句的执行路径,比如是 全表检索 还是 索引检索 等。

举例:

下面这两条 SQL 语句的逻辑是相同的,不过实际执行过程会有所不同,比如 张三 的重名数据概率通常大于 嘎嘎嘎 的概率

假如 test1 表里 name 为 张三 的数据有 10 条 ; test2 表里 name 为 嘎嘎嘎 的数据有 2 条,同时满足 SQL 条件的只有 一条数据

  • SQL1 :先找到一条 name 为 张三 的数据,再根据关联的 Id 找到 test2 的数据,判断 test2.name 是否为 嘎嘎嘎,概率为 0.2
  • SQL2 :先找到一条 name 为 嘎嘎嘎 的数据,再根据关联的 Id 找到 test1 的数据,判断 test1.name 是否为 张三,概率为 0.5
sql
# SQL 1
select * from test1 join test2 using(ID)
where test1.name='张三' and test2.name='嘎嘎嘎' ;

# SQL 2
select * from test1 join test2 using(ID)
where test2.name='嘎嘎嘎' and test1.name='张三' ;

总之,优化器会优化执行顺序生成执行计划,期望以最高效率查询结果,有点类似 JVM 指令重排序

4. 执行器

优化器生成执行计划交给 执行器,在执行之前需要判断该用户是否具备权限。如果有则执行 SQL 查询并返回结果,如果没有则返回错误信息。

如果开启了查询缓存,查询出结果后还会缓存起来 (MySQL 8 已经废弃了缓存这个架构设计)

引擎层

插件式存储引擎层,真正负责了 MySQL 中数据的存储和提取。

MySQL 8.0 的默认存储引擎为 InnoDB ,也是我们使用最多的存储引擎。支持事务、行锁、外键等.

存储引擎.webp

存储层

所有的数据实际上都是存储在 文件系统 上,以 文件 的方式存在的,并完成与存储引擎的交互。

MySQL 高可用

要考虑 MySQL 高可用,就要避免 MySQL 单点故障后导致整个数据库服务不可用,一般可以使用主从架构主备架构来实现。

  • 主备架构

主备架构就是主库负责读写,从库只负责默默地从主库同步数据,当主库挂了使用从库替代

主备架构.webp

  • 主从架构

主从架构涉及到读写分离,主库负责写,从库负责同步数据和响应读请求。

主从架构.webp

主从同步

MySQL 的主从同步机制是一种数据复制技术,用于将主数据库(Master)上的数据同步到一个多个从数据库(Slave)中

主要是通过 binlog 实现数据的复制,MySQL 支持异步复制、同步复制和半同步复制三种复制方式。

异步复制

MySQL 的默认复制模式:

  • 主库提交事务会生成 binlog ,会由一个 dump 线程监听 binlog 文件的变更,如果有更新则推送更新事件给从库
  • 从库接收到事件后拉取数据,会有一个 I/O 线程将其写到 relayl log 中,由 SQL 线程来重放更新数据

异步复制有数据丢失风险,比如数据还未同步到从库,主库就给客户端响应然后就挂了,此时从库晋升为主库的话数据是缺失的。

异步复制.webp

同步复制

主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,性能很差

半同步复制

MySQL 5.7 后支持半同步复制,比如有 3 个从库,可以配置只要有一个从库响应了,主库就可以响应客户端了

并行重放

从库最开始是通过一个 SQL 线程按顺序逐条执行主库的 binlog 日志指令(relay log 重放), 这种串行执行方式可能导致从库的复制速度跟不上主库的高并发写入操作,从而产生主从延迟问题。

于是 MySQL 引入了并行复制,从库由多个 SQL 线程来执行重放事件。

主从延迟

无论怎么优化,主从延迟是必然存在的,只能减少延迟的时间。这个问题再延升一下,也就是分布式系统没办法保证强一致性,只能保证最终一致性。

常见的业务上的解决方案:

  • 二次查询:如果从库查不到数据,则再去主库查询一次,由 API 封装这个逻辑,算是一个兜底策略
  • 关键业务读写都走主库:访问量不高又关键的业务可以读写都走主库
  • 使用缓存:主库写入后同步到缓存,这样查询可以先查缓存(避免了延迟但引入了缓存数据一致性的问题)

读写分离

读写分离就是主库负责写,从库负责读,这样从库就分担了主库的一部分压力。

主库不建查询的索引,从库创建查询的索引,因为索引是需要维护的,主库将索引删掉后可以减少写操作对主库的压力。

实现读写分离,主要有两种方案:

  • 代码封装:代码层面可用用个代理类对外暴露正常的读写接口,但内部写操作指向主数据库,读操作指向从数据库
  • 使用中间件:比如 MySQL-Proxy

分库分表

不到迫不得已的情况,不要采取分库分表策略,尽量先考虑使用读写分离来解决数据库压力

因为分库分表可能引发一系列问题,增大系统维护成本:

  1. 事务问题

分库之后单机事务就失效了,必须使用分布式事务来解决,分布式事务比较重,而且一般不保证强一致性,只保证最终一致性。

  1. 连表 JOIN 问题

跨库之后无法使用 JOIN 进行连表查询了,可以通过业务代码进行关联,比如先把一个表的数据查出来,再去查另一个表

  1. 全局 ID 唯一性问题

单库单表使用数据库的自增 ID 即可,分库分表后使用自增 ID 有可能导致重复主键,此时只能选择雪花算法这类全局唯一的 ID

  1. 排序问题

单表直接通过 order by 排序即可,分库分表之后要么自己在代码中排序,要么利用中间件处理,比如 ES

  1. count 问题

和排序问题类似,要么只能在代码中进行累加,要么利用 ES 这类中间件处理

如何实现数据库的不停服迁移

  • 首先需要考虑数据量级,如果数据量小(比如几十万),那么直接代码迁移,再核对下即可
  • 如果数据量大,则需要好好考虑方案,涉及到在线数据的插入和修改,需要保证数据一致性
  • 迁移还需要回滚兜底策略,一旦出现问题需要及时切回老库,防止对业务产生影响

双写方案

  1. 将新库作为从库,旧库作为主库,进行数据同步
  2. 改造业务代码,数据写操作不仅要写旧库,也要写新库,这就是所谓的双写,注意需要支持通过配置实时打开或关闭双写
  3. 在业务低峰期,也就是数据同步没有延迟的时候,关闭同步,打开双写,此时业务代码还是读的旧库
  4. 进行数据核对,如果没问题,则进行灰度切流,比如 1% 的用户切到读新数据库,如果没问题再逐步增加这个比例
  5. 继续保留双写,如果跑了几天(或者更久),用户读流量切到新库也没问题,此时就可以关闭双写了,读写流量都走新库,迁移完成

flink-cdc 方案

也可以利用第三方工具进行数据同步,比如 flink-cdc

flink-cdc.webp