高可用与扩展
开篇:单机 MySQL 的瓶颈在哪?
假设你创业做了一个电商网站。刚上线时日活 100 人,一台 MySQL 跑得飞起。半年后做了一波推广,日活涨到 1 万,数据库开始偶尔慢查询。又过了一年,日活突破 10 万,订单表 3000 万行,高峰期 QPS 飙到 1 万 --- MySQL 连接数爆了,查询超时,用户下不了单。
这时候你面临两个瓶颈:
- 并发瓶颈:单机数据库连接数有限(通常几百到几千),扛不住大量并发读写
- 容量瓶颈:单表数据量太大,B+ 树层级增加,查询变慢
怎么办?这就是本文要解决的三大问题:主从复制解决读压力,读写分离提升并发能力,分库分表解决数据量问题。
优化的路径应该是循序渐进的:先优化 SQL 和索引 → 再加缓存(Redis) → 然后主从读写分离 → 最后才是分库分表。别一上来就分库分表,那是杀鸡用牛刀。
一、主从复制
1.1 复制原理:三个线程的接力赛
MySQL 主从复制的核心思想很简单:主库把所有写操作记录到 binlog 里,从库把 binlog 拉过来重放一遍。
整个过程涉及三个线程的协作:
三步走:
- 主库:执行写操作后,将变更记录到 binlog(二进制日志)。当从库连接时,主库会启动一个 Binlog Dump 线程,负责读取 binlog 事件并发送给从库。读取 binlog 时会加锁,读完释放
- 从库 I/O 线程:连接主库的 Dump 线程,告诉主库自己要从哪个位置开始接收 binlog,然后把拉取到的 binlog 事件写入本地的 relay log(中继日志)
- 从库 SQL 线程:不断读取本地 relay log 中的事件,解析成具体的 SQL 操作并执行,让从库的数据与主库保持一致
拉还是推?
虽然主库有 Binlog Dump 线程,但实际上是从库主动发起连接并请求 binlog,不是主库主动推送。这个设计让从库可以自行管理同步进度和处理延迟,灵活性更高。参考 MySQL 官方文档的描述也是如此。
1.2 三种复制模式:安全与性能的取舍
异步复制(默认模式)
主库执行完事务后,不等从库确认,直接返回客户端成功。
- 优点:性能最好,主库写入速度完全不受从库拖累
- 缺点:主库挂了,binlog 可能还没同步到从库,这部分数据就永久丢失了
- 类比:快递员把包裹往门口一放就走了,不管你收没收到
这是 MySQL 默认的复制模式,适合对数据安全要求不高、追求极致写入性能的场景。
半同步复制
主库执行完事务后,等至少一个从库收到 binlog 并写入 relay log,再返回客户端成功。
- 优点:数据安全性大幅提升,至少有一个从库持有完整数据副本
- 缺点:多了一次网络往返等待时间,写入延迟增加
- 类比:快递员放下包裹,等你开门说"收到了"才离开
-- 开启半同步复制(主库端)
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
-- 开启半同步复制(从库端)
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;MySQL 5.7 新增了 rpl_semi_sync_master_wait_for_slave_count 参数,可以设置需要多少个从库确认才返回,默认是 1。调大这个值可以进一步增强数据安全,但每多等一个从库就多一份延迟。
组复制(MGR --- MySQL Group Replication)
这是 MySQL 5.7.17 推出的新方案,基于 Paxos 协议实现强一致。读写事务需要经过组内**大多数节点(N/2 + 1)**同意才能提交,只读事务不需要组内同意,直接 COMMIT。
- 优点:数据一致性最强,组内各节点的数据保证一致
- 缺点:性能开销最大,配置和运维复杂度也最高
- 类比:公司决策需要董事会投票表决,超过半数同意才能执行
简单理解 MGR 的工作方式:多个节点组成一个复制组,每个节点维护自己的数据副本。写事务要提交时,通过一致性协议层广播到组内,大多数节点确认后才能提交。这样即使个别节点宕机,也不会丢数据。
| 复制模式 | 数据安全 | 写入性能 | 运维复杂度 | 适用场景 |
|---|---|---|---|---|
| 异步复制 | 弱 | 最高 | 最简单 | 允许少量数据丢失 |
| 半同步复制 | 较强 | 中等 | 中等 | 大多数互联网业务 |
| 组复制 MGR | 最强 | 较低 | 复杂 | 金融等强一致场景 |
1.3 主从延迟:绕不开的难题
不管是异步还是半同步复制,从库的数据天然会滞后于主库。这个时间差就是主从延迟。
为什么会延迟?
- 网络延迟:主从跨机房、跨地域部署时,binlog 传输需要时间
- 从库性能不足:从库硬件配置低于主库,SQL 线程重放跟不上主库的写入速度
- 单线程瓶颈:从库默认只有一个 SQL 线程做回放,而主库是多线程并发写入
- 大事务:一个事务修改了 100 万行数据,从库重放这个事务也需要同等时间
主从延迟会导致什么业务问题?
最典型的场景:用户刚下单成功(写主库),立刻刷新页面查订单(读从库)--- 数据还没同步过来,页面显示"暂无订单"。用户以为下单失败了,又下了一单,最后收到两份快递。
解决方案:
- 同城同机房部署:主从放在同一机房,网络延迟降到亚毫秒级
- 提升从库配置:从库硬件不要低于主库,尤其是磁盘 IO 和 CPU
- 并行复制:让从库用多个线程同时回放(下一节详细讲)
- 拆分大事务:一次改 100 万行不如分成 100 批,每批 1 万行
- 关键读走主库:写后立即读的场景,强制查主库而非从库
1.4 并行复制:从单车道到多车道
从库单线程回放是主从延迟的重要原因之一。试想主库 8 个线程并发写入,从库只有 1 个线程在重放,能不延迟吗?并行复制就是把回放从"单车道"变成"多车道"。
MySQL 的并行复制经历了三代进化:
第一代:MySQL 5.6 --- 库级别并行
每个库分配一个独立的回放线程。如果你有 3 个库,就能用 3 个线程并行回放不同库的事务。
问题是:绝大多数业务只有一个库,三个线程只有一个能干活,其他两个闲着。被开发者和 DBA 一致认为是"鸡肋功能"。
第二代:MySQL 5.7 --- 组提交并行(MTS)
这是真正有意义的并行复制,也叫 Enhanced Multi-Threaded Slave。
核心洞察:如果多个事务能在同一时刻进入 Prepare 阶段(2PC 的第一阶段),说明它们之间一定没有锁冲突。没有锁冲突意味着它们不修改同一行数据,谁先执行谁后执行结果都一样,所以可以在从库并行回放。
-- 从库配置开启组提交并行复制
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 8 -- 并行回放线程数,根据 CPU 核数调整但有个前提条件:这种方式依赖主库的并发度。如果主库写入不频繁,没有足够的事务可以凑成一组,并行复制就没什么用武之地。
第三代:MySQL 8.0 --- WriteSet 并行
解决了第二代依赖主库并发度的痛点。核心思路:即使主库是串行提交的事务,只要两个事务不修改同一行数据,从库就可以并行回放。
-- 开启 WriteSet 并行复制
binlog_transaction_dependency_tracking = WRITESET
transaction_write_set_extraction = XXHASH64原理:每个事务修改的行(通过主键和唯一键标识)会计算出一组哈希值,组成一个 WriteSet。MySQL 在内存中维护一个 WriteSet 集合,新事务的 WriteSet 与集合中已有事务做比对。如果没有交集,说明它们修改的不是同一行,可以分配相同的 last_committed 值,从而允许从库并行回放。
| 版本 | 并行粒度 | 是否依赖主库并发 | 实际效果 |
|---|---|---|---|
| 5.6 | 按库 | 需要多个库 | 鸡肋 |
| 5.7 | 按组提交 | 需要高并发写入 | 好用 |
| 8.0 | 按行(WriteSet) | 不依赖 | 最好 |
二、读写分离
主从搭好了,下一步就是读写分离:写操作走主库,读操作走从库,把读压力分散到多个从库上。对于读多写少的互联网应用来说,效果立竿见影。
2.1 中间件方案
实现读写分离不需要改 SQL,在应用和数据库之间加一层"智能路由"就行:
| 方案 | 类型 | 部署方式 | 特点 |
|---|---|---|---|
| ShardingSphere-JDBC | 客户端模式 | jar 包嵌入 Java 应用 | 最轻量,不需要额外部署代理,用得最多 |
| ShardingSphere-Proxy | 代理模式 | 独立部署代理服务 | 语言无关,对应用完全透明 |
| MyCat | 代理模式 | 独立部署 | 老牌中间件,功能丰富但社区活跃度逐渐下降 |
ShardingSphere-JDBC 以 jar 包形式集成到 Java 应用中,可以理解为"增强版 JDBC 驱动"。它在 JDBC 层拦截 SQL,自动判断是读还是写,然后路由到对应的主库或从库。对业务代码完全透明,不需要改一行 SQL。
2.2 强制走主库的场景
虽然读走从库可以分散压力,但有些读操作必须走主库,否则会因为主从延迟读到过期数据,引发业务问题:
- 写后立即读:用户修改了昵称,页面要立刻显示新昵称,不能还显示旧的
- 金融场景:查余额必须是实时最新的,差一分钱都不行
- 库存检查:下单前查库存要精确,读到旧库存可能导致超卖
- 业务决策依赖:如风控系统判断用户状态,必须基于最新数据
// ShardingSphere-JDBC 强制走主库的写法
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setMasterRouteOnly();
// 以下所有查询都强制路由到主库
Order order = orderMapper.selectById(orderId);
User user = userMapper.selectById(userId);
}
// try 块结束后,HintManager 自动释放,后续查询恢复走从库设计原则
不要滥用"强制走主库"。如果所有读操作都走主库,那搭从库就失去意义了。只有涉及数据一致性的关键读才走主库,其他查询走从库就好。
三、分库分表
当读写分离和缓存都用上了,但单表数据量依然在疯狂增长,查询越来越慢,这时候就该分库分表登场了。
3.1 什么时候需要分?
不要过早分库分表! 它带来的复杂度远比你想象的大。先确认其他优化手段都用尽了:
优化顺序(由简到繁、由低成本到高成本):
1. SQL 优化 + 索引优化 → 成本最低,效果显著
2. 加 Redis 缓存 → 减少数据库读压力
3. 读写分离 → 分担读压力到从库
4. 数据归档 → 历史数据迁移到归档表,给主表瘦身
5. 分库分表 → 最后的手段,成本最高经验阈值(来自阿里巴巴 Java 开发手册,偏保守):
- 分表信号:单表行数超过 500 万行,或单表容量超过 2GB
- 分库信号:数据库连接数不够,QPS 超过单库承载能力
实际经验中,InnoDB 单表扛 2000 万行数据问题不大,具体取决于行的大小、索引数量和硬件配置。从 B+ 树的角度分析,三层 B+ 树(行大小约 1KB)大概能存 2000 万行数据,超过这个量可能会增加一层,每次查询多一次磁盘 IO。
3.2 分库、分表、分库分表:三件不同的事
很多人把"分库分表"当成一个词,但其实它包含三种不同的做法,解决的问题也不同:
- 只分库不分表:解决并发问题。数据库连接数不够了,拆成多个库实例,部署在不同服务器上,增加总可用连接数。典型场景:微服务拆分时按业务边界拆库(订单库、用户库、商品库)
- 只分表不分库:解决数据量问题。单表太大查询慢,拆成多张结构相同的小表,每张表数据量可控
- 既分库又分表:同时解决并发和数据量问题。高并发 + 大数据量通常同时出现,所以这是最常见的做法
3.3 水平拆分 vs 垂直拆分
垂直拆分:按列拆,把一张"胖表"按业务维度拆成多张"瘦表"。
拆分前(一张大胖表,20 个字段):
orders: id, user_id, status, amount, address, phone,
item_list, logistics_info, remark, invoice_info, ...
拆分后(按访问频率和业务职责拆成瘦表):
orders: id, user_id, status, amount -- 高频核心字段
order_ext: id, address, phone, remark -- 低频扩展字段
logistics: id, logistics_info -- 物流信息独立
invoice: id, invoice_info -- 发票信息独立好处是减小了单行大小,一个 16KB 的数据页能存更多行,查询核心字段时 IO 更少。
水平拆分:按行拆,把一张表的数据分散到多张结构完全相同的表中。
拆分前:
orders(3000 万行)
拆分后(按 user_id 取模分成 10 张表):
orders_0000(~300 万行)
orders_0001(~300 万行)
...
orders_0009(~300 万行)好处是每张小表的数据量可控,B+ 树层级不会太深,查询性能稳定。这是我们通常说的"分表"。
实际项目中,垂直拆分和水平拆分经常配合使用:先按业务垂直拆分出独立的表,再对数据量大的表做水平拆分。
3.4 分片策略:数据去哪张表?
分片策略决定了一条数据应该路由到哪个库、哪张表。选错策略会导致数据倾斜、扩容困难等问题。
取模法(最常用)
// 分 128 张表
int tableIndex = userId % 128;
String tableName = "orders_" + String.format("%04d", tableIndex);
// userId = 12345 → 12345 % 128 = 57 → orders_0057优点:实现简单,数据分布均匀。缺点:扩容时表数量变了(128 → 256),大量数据需要重新计算路由并迁移。
范围法
按某个有序字段的范围划分,比如按时间或 ID 区间:
-- 按年份分表
orders_2024: 2024 年的订单
orders_2025: 2025 年的订单
orders_2026: 2026 年的订单
-- 按 ID 范围分表
orders_0000: id 1 ~ 500万
orders_0001: id 500万+1 ~ 1000万优点:扩容非常方便,新数据直接写新表,旧表完全不用动。缺点:容易数据倾斜和热点集中 --- 最新的表承担了绝大部分读写压力,旧表几乎闲置。
一致性哈希
取模法的升级版。把所有数据映射到一个 2^32 个节点的哈希环上,扩容时只影响相邻节点间的数据,迁移量远小于普通取模。适合需要频繁扩缩容的场景。
3.5 分布式 ID 方案
分库分表后,每张表各自自增的 ID 一定会冲突。而且即使不冲突,多张表的自增 ID 重复也会让全局查询、数据汇总、数据同步(到离线表或 ES)变得不可能。必须有全局唯一 ID 方案。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 128 位随机值 | 生成简单,全局唯一 | 36 字符太长、无序导致页分裂、索引效率低 |
| 数据库号段 | 一次从数据库取一段 ID 缓存到本地 | 简单可靠、趋势递增 | 依赖数据库、号段用完需再取 |
| Redis INCR | 利用 Redis 的原子自增命令 | 性能极高 | 依赖 Redis 可用性 |
| 雪花算法 | 时间戳 + 机器ID + 序列号 | 8 字节、趋势递增、纯内存生成 | 依赖机器时钟,时钟回拨可能重复 |
雪花算法详解:
|-- 1bit 符号位 --|-- 41bit 时间戳 --|-- 10bit 机器ID --|-- 12bit 序列号 --|
固定为 0 可用约 69 年 最多 1024 节点 每毫秒 4096 个- 41 位时间戳:精确到毫秒,可用约 69 年
- 10 位机器 ID:高 5 位数据中心 ID + 低 5 位工作节点 ID,最多 1024 个节点
- 12 位序列号:同一毫秒内的递增序号,每个节点每毫秒最多 4096 个
- 总计:每毫秒全集群可生成 1024 x 4096 = 约 420 万个唯一 ID
号段模式(Leaf / TDDL) --- 大厂高频使用的方案:
号段分配表(数据库中):
| biz_tag | max_id | step |
|---------|--------|-------|
| order | 10000 | 1000 |
应用启动 → 取走 [10001, 11000] 缓存到本地内存
本地分配 → 10001, 10002, ..., 11000
用完之后 → 再取 [11001, 12000]
每次只访问一次数据库,之后 1000 个 ID 都在本地内存中分配
还可以设置"双 buffer":剩余 20% 时异步预取下一段,业务线程永远不阻塞3.6 分表字段怎么选?
以电商订单表为例,候选字段有买家 ID、卖家 ID、订单号、时间等。分表字段的选择直接决定了数据分布是否均匀、最高频的查询是否能精确路由。
首选买家 ID。为什么?
- 数据均匀:一个买家的订单量有限,不太可能出现某个买家把数据买倾斜了
- 查询友好:买家查自己的订单是最高频场景,查询天然带分片键
- 避免热点:如果选卖家 ID,大卖家(品牌旗舰店)一天可能产生几十万笔订单,全堆在一张表里,严重倾斜
卖家查订单怎么办?
空间换时间:用 Canal 监听 binlog,准实时同步一份按卖家 ID 分片的"卖家维度表"。这张表只提供读服务,不做写操作。所有写操作还是走买家表。
大卖家的数据可以进一步按时间拆分,或者干脆用 HBase、TiDB、Lindorm 等支持海量数据查询的存储来承接卖家表,因为它只需要高性能的读能力。
按订单号查呢?
基因法:在生成订单号时,把分表路由结果编码进去。
生成时:buyer_id % 128 = 0023
订单号:2025072412345-0023
↑ 分表编号嵌入订单号
查询时:从订单号中解析出 0023 → 直接去 orders_0023 查这样不管用买家 ID 还是订单号,都能精确路由到单表。对于既没有买家 ID 也没有订单号的低频查询(如运营后台复杂报表),同步数据到 ES 或 TiDB 去查。
3.7 分表数量怎么定?
公式:
分表数量 = (存量数据 + 年增长量 x 保留年限) / 2000万 → 向上取最接近的 2 的幂这里的 2000 万是 InnoDB B+ 树在三层高度下的理论上限(行大小约 1KB 时),超过这个值 B+ 树可能增加一层,每次查询多一次磁盘 IO。
举例:存量 2000 万,年增长 500 万,保留 10 年:
(2000万 + 500万 x 10) / 2000万 = 3.5 → 向上取 2 的幂 = 4 张表为什么用 2 的幂? 三个好处:
- 位运算替代取模:
hash % 8等价于hash & 7,位运算比取模快得多 - 库表均匀分配:128 表 / 16 库 = 每库 8 表,整除无余数,不会出现有的库多有的库少
- 扩容只迁一半数据:从 4 表扩到 8 表,只需迁移 50% 的数据
4 表扩容到 8 表的迁移过程:
原来 userId % 4 == 0 的数据全在 table_0
扩容后:
userId % 8 == 0 → 留在 table_0(不用动)
userId % 8 == 4 → 迁到 table_4
每张旧表只需迁出一半数据到对应的新表
非 2 的幂(如 5 → 9)则几乎所有数据要重新分配分库数量经验公式:分库数 = 分表数 / 8
常见配置:8 库 64 表、16 库 128 表、64 库 512 表、128 库 1024 表。如果分表数量本身小于 8(如 2 或 4),建议分库数等于分表数。
3.8 分库分表的代价
分库分表不是免费的午餐,它会引入一系列新问题。上线前必须想清楚如何应对这些代价:
跨分片 JOIN
数据分散在不同库,标准 SQL JOIN 无法跨库执行。三种解决思路:
- 应用层组装:分别查询两张表,在 Java 代码中手动匹配组装结果
- 数据冗余:把高频关联字段(如用户名)冗余到主表中,避免 JOIN。这是大厂用得最多的方案
- 搜索引擎:把多张表的数据同步到 ES,构建宽表文档,在 ES 中做关联查询
分布式事务
跨库操作无法使用 MySQL 的本地事务。需要引入 2PC、TCC、Saga 等分布式事务方案,开发复杂度和运行时开销都大幅上升。
深度分页
LIMIT 100000, 10 这种查询变成了噩梦:需要从每个分片各取 100010 条,在内存中全局排序,然后丢掉前 100000 条只返回 10 条。分片越多,浪费越大。
解决方案:
- 带分片键查询,直接路由到单表做分页
- 游标分页:
WHERE id > #{lastId} ORDER BY id LIMIT 10,避免大 offset - 复杂分页需求走 ES / TiDB
聚合与排序
ORDER BY、GROUP BY、COUNT、SUM 等聚合操作需要从所有分片取出数据,在中间件或应用层做全局汇总。数据量大时内存压力和网络开销都很大。
四、ShardingSphere 实战
ShardingSphere 是 Apache 顶级项目,是目前 Java 生态最流行的分库分表中间件。
4.1 核心概念速查
逻辑表:t_order(代码中使用的表名,开发者只需关心这个)
↓ ShardingSphere 自动路由
真实表:t_order_0000, t_order_0001, ..., t_order_0127(物理表)
↓ 分布在不同的数据库实例中
数据节点:ds_0.t_order_0000, ds_1.t_order_0064, ...(库.表)| 概念 | 说明 |
|---|---|
| 分片键 | 用于路由计算的字段,如 user_id |
| 分片算法 | 根据分片键值计算目标表的逻辑(取模、范围、行表达式等) |
| 绑定表 | 分片规则一致的主子表(t_order 和 t_order_item 都按 order_id 分片) |
| 广播表 | 每个分片都持有完整副本的表(字典表、配置表等小表) |
4.2 没有分片键会怎样?
如果查询 SQL 没有带分片键,ShardingSphere 无法判断数据在哪个分片,只能走广播路由 --- 把 SQL 发到所有分片执行,然后把各分片的结果在内存中汇总返回。
-- 逻辑 SQL(没有分片键 user_id)
SELECT * FROM t_order WHERE order_status = 'PAID';
-- ShardingSphere 实际执行(广播到全部 4 个分片)
SELECT * FROM t_order_0000 WHERE order_status = 'PAID'
UNION ALL
SELECT * FROM t_order_0001 WHERE order_status = 'PAID'
UNION ALL
SELECT * FROM t_order_0002 WHERE order_status = 'PAID'
UNION ALL
SELECT * FROM t_order_0003 WHERE order_status = 'PAID'4 张表还勉强能接受。但如果是 1024 张表呢?一条查询变成 1024 条查询,性能直接崩溃。所以查询时一定要尽量带分片键。
4.3 分片策略配置示例
# ShardingSphere-JDBC 配置(YAML 格式)
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: t_order_inline
shardingAlgorithms:
t_order_inline:
type: INLINE
props:
algorithm-expression: t_order_${user_id % 4}行表达式 t_order_${user_id % 4} 是最简单的配置方式:user_id 对 4 取模,结果 0-3 对应 4 张表。适合大多数简单分片场景,不需要写 Java 代码。
复杂场景(多分片键、范围查询、热点账户特殊处理)可以实现 ComplexKeysShardingAlgorithm 接口,在 Java 代码中编写自定义路由逻辑,灵活度最高。
4.4 数据倾斜怎么办?
数据倾斜是分库分表最怕遇到的问题。举个真实案例:按付款方 ID 分表,突然接入了一个企业账户做付款方,它每天的交易量是普通用户的 1000 倍,数据全堆在同一张表里,这张表就成了整个系统的瓶颈,还会拖慢同表中其他用户的查询。
预防策略:
- 选好分片键:选分布均匀的字段。买家 ID 优于卖家 ID,因为一个买家不可能买出数据倾斜
- 复合分片:检测到热点用户后,组合"用户 ID + 时间"做二次路由,把数据打散
// 企业账户的热点处理示例
switch (customerType) {
case INSTITUTION:
// 企业账户:追加日期做二次分片,把数据打散到多张表
shardingKey = payerId + DateUtils.format(bizTime, "yyyyMMdd");
break;
default:
// 普通用户:直接按付款方 ID 分片
shardingKey = payerId;
}- 物理隔离:把超级热点商户的数据独立到专用数据库实例,配置更高的硬件,同时避免影响其他正常商户的查询性能
五、常见面试题精选
题 1:MySQL 主从复制的过程?
主从复制基于 binlog,涉及三个线程协作。主库有 Binlog Dump 线程,响应从库请求发送 binlog 事件。从库有 I/O 线程拉取 binlog 写入本地 relay log,还有 SQL 线程读取 relay log 并重放 SQL 语句。注意 binlog 是从库主动拉取的,不是主库推送。
三种复制模式:异步复制(默认,性能最好但主库宕机可能丢数据)、半同步复制(至少一个从库确认收到 binlog 才返回客户端)、组复制 MGR(基于 Paxos 协议,多数派节点同意才能提交,一致性最强但性能最低)。
题 2:什么是主从延迟?如何解决?
主从延迟是从库数据滞后于主库的时间差。主要原因包括:网络延迟、从库性能不足、从库单线程回放跟不上主库并发写入、大事务重放耗时长。
解决方案:同城部署减少网络延迟;提升从库硬件配置;开启并行复制(5.7 组提交并行、8.0 WriteSet 并行);拆分大事务避免单事务锁定太长时间。对于写后立即读等关键场景,可以强制走主库。
题 3:分库和分表分别解决什么问题?
分库解决并发瓶颈 --- 单库连接数有限,通过增加库实例提供更多可用连接。分表解决容量瓶颈 --- 单表太大导致 B+ 树层级增加、查询变慢,拆成多张小表降低单表数据量。两者是不同维度的优化,可以独立做也可以配合做。
建议在 SQL 优化、加缓存、读写分离之后仍有瓶颈时再考虑。阿里建议单表超 500 万行(保守值),实际经验单表 2000 万也可接受。
题 4:分库分表后全局 ID 怎么生成?
不能用各表自增 ID,否则 ID 重复导致全局查询无法唯一定位数据、数据同步到离线表时主键冲突。常见方案:雪花算法(8 字节、趋势递增、纯内存生成、每毫秒全集群可生成约 420 万 ID)、号段模式(批量取 ID 缓存本地,支持双 buffer 预取)、Redis INCR(高性能但依赖 Redis 可用性)。UUID 全局唯一但太长且无序,不推荐做数据库主键。
题 5:分库分表后 JOIN 怎么做?
跨库 JOIN 无法直接执行。三种主流方案:应用层分别查询后在 Java 代码中组装;对高频关联字段做数据冗余避免 JOIN(大厂最常用的方案,如冗余用户名到订单表);把数据同步到 ES 或 TiDB 构建宽表做复杂查询。
题 6:分区和分表有什么区别?
分区是 MySQL 内部机制,表面上还是一张表(一个 .frm 文件),底层数据按分区规则存在多个 .ibd 文件中,数据库自动管理路由。分表是物理上完全独立的多张表(各自有 .frm + .ibd 文件),查询需要指定具体表名或通过中间件路由。
数据量大时应先考虑分区(简单、对应用透明),分区搞不定再考虑分表。分表在缓存命中率、锁粒度、备份恢复速度、横向扩展性方面有更大优势。
小结
MySQL 高可用与扩展是一个循序渐进的过程,不要跳步:
单机 MySQL 扛不住了?
第一步:读压力大 → 主从复制 + 读写分离
├── 主从复制:binlog → relay log → SQL 线程重放
├── 复制模式:异步(默认)→ 半同步 → 组复制 MGR
├── 解决延迟:并行复制(5.6 库级 → 5.7 组提交 → 8.0 WriteSet)
└── 读写分离:ShardingSphere-JDBC / Proxy,关键读走主库
第二步:数据量大 → 分库分表
├── 垂直拆分:按列拆,胖表变瘦表
├── 水平拆分:按行拆,大表变小表
├── 分片策略:取模(最常用) / 范围 / 一致性哈希
├── 分片键选择:优先选分布均匀、查询高频的字段
├── 全局 ID:雪花算法 / 号段模式
├── 分表数量:2 的幂,便于位运算和扩容
└── 代价:跨库 JOIN、分布式事务、深度分页、数据倾斜核心原则:能不分就不分,要分就分到位。分库分表是最后的手段,但一旦走到这一步,就要提前规划好分片键、分片算法、全局 ID 方案和数据迁移策略。否则后面改起来,成本比重新做还高。