作为传统关系型数据库,MySQL 的分布式倾向于存储方面:将数据分散存储到各个节点,减少单个节点的读写压力、存储压力

同时,MySQL 原生支持了主从复制,实现了数据的冗余存储,保障了可用性

结构

image

  • 数据分散存储到各个主节点,减少单个节点的读写压力、存储压力
  • 从节点冗余存储主节点数据,保障了可用性,还可以实现读写分离,进一步减少主节点读压力
  • 子集群之间没有直接关联,需要应用层实现

如何实现服务注册、服务发现

假设有一个主节点,我们要想为这个主节点添加从节点,需要在从节点上 手动 指定主节点的位置:

CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1  -- 指定使用 GTID 确定位点

指定以后,从节点就知道了主节点的位置,同样的,主节点也会知道从节点的位置

但是主节点之间是无法发现彼此的,此外,对于应用层来说,MySQL 原生并不支持服务注册、服务发现,通常需要手动实现,或者引入一个 Proxy 层

如何将请求路由到正确的节点

我们将请求分为两类:

  • 读请求
  • 写请求

假设我们做了数据分片(分库分表),那么在执行读写请求前,就要先确定数据存储的位置

实现方式有两种:

  • 应用层代码手动实现
  • 引入 Proxy 层

应用层代码手动实现

应用程序主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说,由客户端来选择后端数据库进行查询。

这种方式的性能较好,缺点就是需要对已有代码进行修改

基于 Proxy(代理层)实现

这个方式就是在 Client 到 Server 之间引入一个代理,自动分发客户端的请求:

image

Proxy 会负责请求的分发与负载均衡

这种方式的优点就是不用修改已有代码,但是有一定的性能损耗

如果要实现读写分离,同样的,也是上面两种方式

C、A 如何做权衡(trade-off)

这里讨论的主要是读写分离的情况

由于主从复制会存在延迟,在读写分离的情况下,我们就需要在 C、A 之间做 trade-off,通常有以下几种实现方式

强制走主库

一些对数据一致性要求很高的业务(如金融),可以采取这种方式,保证绝对的数据一致性,纯粹的 C

缺点就是:如果很多业务都有很高实时性要求,都走主库,会 对主库造成很大压力

强制走从库

一些对数据一致性要求不高的业务(如互联网应用),可以采取这种方式,保证可用性,纯粹的 A

Sleep

既然同步需要时间,那我们可以「sleep」一下,等它同步完了再去访问不就好了吗

例如,一个用户发布了一个商品,前端可以基于本地已有信息进行展示,就好像获取到了最新的数据一样

用户待会刷新页面,其实已经过了一段时间,也就达到了 sleep 的目的,只要等待时间内同步完成,访问的就是最新的数据

这种方式适用于对数据实时性要求低的业务,就算访问到过期数据也影响不大

缺点就是:我们无法保证在规定时间内主从同步完毕,还是 有访问到过期数据的可能性

判断主从无延迟方案

这种方式比较折中

  1. 在访问数据之前,判断主从是否同步完毕
  2. 如果同步完毕,那么走从库,获取的是最新数据
  3. 如果没有,可以等待一段时间,重复第一个步骤
  4. 如果等待时间 超过一定阈值,那么根据实际需求,看看是走主库,获取最新数据;或者走从库,获取过期数据

如何做数据分片

我们可以将数据分散存储到不同的主节点上

通常的实现方式有:

  • 哈希分片
  • 一致性哈希
  • 范围分片

哈希分片

根据指定 key(比如 id) 的哈希值,算出这个数据在哪个库(表)中

哈希分片算法比较适合 随机 查询的场景,并能 一定程度避免单个库上的热点问题不适合范围查询

范围分片

按照指定范围区间来分配数据,例如,将 id 为 1 ~ 10000 的存在第一个 DB,id 为 10001 ~ 20000 的存在第二个 DB

范围分片算法比较适合 范围 查询的场景,但有可能 存在 单个库上的 热点问题

数据如何同步(复制)到各个节点

这里讨论的是 MySQL 的主从复制

image

简单来说,就是从库的 IO_Thread 将主库发来的 binlog 写到本地的 relay log,然后由 SQL_Thread 负责拉取 relay log 完成主从复制

binlog 是主库主动推送,还是从库主动拉取?

一开始创建主备关系的时候,是由备库指定的。

比如基于位点的主备关系,备库说“我要从 binlog 文件 A 的位置 P”开始同步, 主库就从这个指定的位置开始往后发。

而主备复制关系搭建 完成以后,是 主库决定“要发数据给备库” 的。

所以主库有生成新的日志,就会发给备库。

如何实现故障转移

主节点挂了,从节点怎么办?

MySQL 没有自动的故障转移,需要手动转移

大致步骤如下:

  • 选择一个从库
  • 将其设置为新的主库
  • 修改剩余从库的 master 为新的主库

image

但问题是:其它从节点应该怎么去同步新的主节点的数据呢?

难点:从库去同步新的主库的数据,不是全量同步,而是增量同步,如何寻找这个同步的「起始点」?

MySQL 5.6 引入了 全局事务 ID(GTID),是一个事务的唯一标识

在 GTID 模式下,从库获取新的主库的位点这件事情就在 MySQL 内部做好了:

CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1  -- 指定使用 GTID 确定位点

想要更深入的了解 GTID 的实现,可以看看这篇文章:27 | 主库出问题了,从库怎么办?

如何实现分布式事务

MySQL 使用两阶段提交(2PC)实现分布式事务:

两阶段提交将 事务提交过程 分为两个阶段,保证主从节点间数据的一致性:

  • 准备阶段(prepare)
  • 提交阶段(commit)

使用两阶段提交时,MySQL 内部开启一个 XA 事务:

在 prepare 阶段,将 XID 写入 redo log,并 将 redo log 状态设置为 prepare,然后调用 write、fsync(类似于 innodb_flush_log_at_trx_commit = 1),将 redo log 持久化到磁盘

在 commit 阶段,将 XID 写入 binlog,然后调用 write、fsync(类似于 sync_binlog = 1),将 binlog 持久化到磁盘,并 将 redo log 状态设置为 commit

当然,上面所指的事务,涉及到的数据应该仅在同一个主库中,分布式体现在 一主多从

如果涉及到多个主库(多主多从),MySQL 原生就无能为力了,可以使用:

  • 基于消息队列的异步处理:将事务操作写入消息队列,由消费者进行处理。这种方式可以利用 MQ 的事务机制实现跨多个数据库的分布式事务,但可能会引入一定程度的异步性和延迟。
  • 分布式事务中间件:使用第三方的分布式事务中间件(如 Seata)来实现分布式事务。这些中间件通常提供了对分布式事务的原生支持,能够简化开发和管理复杂的分布式事务场景。
  • 应用程序层面的解决方案:在应用程序层面实现分布式事务逻辑。例如,通过在应用程序中使用 分布式锁、分布式协调服务(如 ZooKeeper)、分布式一致性算法(如 Paxos 或 Raft)等技术来保证事务的一致性。

总结

MySQL 作为传统关系型数据库的代表,其在分布式上的实现 并不完善,原生仅支持主从节点的复制