Featured image of post 数据库演化:MySQL → PostgreSQL → 分布式,从单机到 NewSQL 的全链路实战

数据库演化:MySQL → PostgreSQL → 分布式,从单机到 NewSQL 的全链路实战

数据库演化:MySQL → PostgreSQL → 分布式,从单机到 NewSQL 的全链路实战

Java Web 微服务系列 · 第 8 篇 · 数据库演化 阅读时长:约 45 分钟 本文写于 2026 年 6 月

引子:MySQL 第一次"卡死"的那个深夜

2017 年某日凌晨 3 点,我被一阵急促的电话铃声吵醒。电话那头是刚入职不到半年的运维小哥,声音发颤:"哥,订单服务挂了,DB 连接全爆,整个集群处于不可用状态。"

我一边穿衣服一边远程连上服务器,看到的景象触目惊心:

  • InnoDB buffer pool 利用率 99.8%,已经被锁页撕扯(lock wait)拖到 12 秒
  • 慢查询日志 在 5 分钟内涌进 1.2 万条 SELECT * FROM order WHERE user_id = ? AND status IN (...) 的全表扫
  • 从库延迟 飙到 30 分钟,主从复制的 relay log 把磁盘写满
  • 连接数 跑到 3500/3500,新请求全部被拒绝

事后排查,这是一张 4.2 亿行的 order 表,单表行数已经远超 InnoDB 的最优承载范围。代码里没有按 user_id 建索引,业务员按状态查询导致 90% 的查询全表扫。

那次事故直接损失 38 万元。我开复盘会时说过一句后来被团队当成内部黑话的话:"不是 MySQL 不行,是 MySQL 在裸奔"。

从那以后,我们走上了"数据库演化“这条路:

  1. 第一步:MySQL 索引优化 + 慢查询治理(4.2 亿行也能跑)
  2. 第二步:主从分离 + 读写分离(扛住 5 万 QPS)
  3. 第三步:分库分表(订单表按用户 ID 拆 64 库 × 64 表)
  4. 第四步:PostgreSQL 上位(复杂报表场景)
  5. 第五步:TiDB 试点(解决 MySQL 容量天花板)

到 2025 年回头看,数据库演化就是 Java 微服务架构演化的"地基”——没有它,上层的弹性扩容、服务治理、流量调度全是空中楼阁。

本文要回答四个问题:

  • MySQL 怎么"榨干最后一滴性能"?(索引 + 参数 + 架构)
  • 什么时候必须升级到 PostgreSQL
  • 分库分表和分布式数据库怎么选
  • 容灾备份的底线在哪里

全文 10000+ 字,一次把数据库演化的全链路讲透

一、核心概念:把"主从/集群/分库分表/分布式"掰开

谈具体技术前,先把 6 个常被混用的术语定义清楚——很多团队的"分库分表"和"分布式数据库"压根不是一回事。

1.1 单机(Standalone)

单机:所有数据 + 所有请求都在一台服务器上。

维度表现
容量上限单盘 10TB、单库行数 5 亿(B+Tree 树高 4)
QPS 上限1-3 万(写),5-10 万(读)
可用性单点,故障 = 业务停
适用场景Demo、测试、内部工具、小型 SaaS

🎯 避坑点:单机能撑多久

经验值(InnoDB + 良好索引):

  • 单表行数 < 1000 万:完全无压力
  • 1000 万 - 5000 万:需要索引优化
  • 5000 万 - 5 亿:必须考虑分表
  • > 5 亿:分库分表 or 分布式数据库

5000 万不是"硬指标",但超过了就别再用单表硬抗

1.2 主从复制(Master-Slave Replication)

主从复制:一台主库负责写,多台从库同步数据、负责读。

维度表现
容量上限单机相同,但通过分库可扩
QPS 上限读线性扩展(10 台从 = 50 万 QPS)
写能力仍是单机(最大瓶颈)
可用性主库故障需人工切换,分钟级恢复
同步延迟异步复制通常 10-100ms

💡 原理:主从复制的"三种姿势"

  • 异步复制(默认):主库写完 binlog 就返回,从库异步拉。快但不安全——主库挂掉时未同步的 binlog 会丢
  • 半同步复制(MySQL 5.5+):主库等至少 1 个从库确认收到 binlog 再返回。安全但慢(多 1 次 RTT)
  • 增强半同步(MySQL 5.7.4+):从库持久化到 relay log 后才算确认。比半同步再安全一层

金融场景必须增强半同步 + GTID;普通业务半同步够用。

1.3 集群(Cluster)

集群:多个节点共同承担同一个数据库的读写,对应用看起来像一台。

集群类型代表特点
MySQL Group Replication(MGR)MySQL 官方单主或多主,强一致,写性能差
MySQL NDB ClusterMySQL 官方内存数据库,实时性极强
Percona XtraDB Cluster(PXC)Percona基于 Galera,同步复制
Galera Cluster for MySQLCodership同步复制,多主写入
PostgreSQL Patroni + etcdZalando高可用方案

💡 原理:集群 vs 主从的本质区别

  • 主从:从库只读,主库单点写。扩读不扩写
  • 集群:所有节点都可读可写。读写都扩
  • 代价:集群的"同步复制"会拖慢写入(每写一次要等所有节点确认)

PXC 写性能约为主从的 30%-50%。如果写为主,集群不是好选择。

1.4 读写分离(Read-Write Splitting)

读写分离:在主从基础上,让写走主库、读走从库,对应用透明

常见中间件:

  • ShardingSphere-JDBC(推荐):客户端嵌入,零依赖,性能好
  • MyCat:代理模式,独立部署,跨语言
  • Atlas:奇虎 360 开源,已停更
  • MaxScale:MariaDB 出品

1.5 分库分表(Sharding)

分库分表:把一个库 / 一张表的数据,按规则拆分到多个库 / 多张表。

拆分方式说明
垂直分库按业务拆(订单库 / 用户库 / 商品库)
垂直分表把宽表按列拆(主表 + 扩展表)
水平分库同一张表的数据按规则拆到不同库
水平分表同一张表的数据按规则拆到同库不同表

1.6 分布式数据库(Distributed SQL / NewSQL)

分布式数据库:原生支持水平扩展的数据库,对应用像单机一样使用

数据库类型一致性协议特点
TiDBNewSQLRaft兼容 MySQL 协议,HTAP
OceanBase原生分布式Paxos 多副本阿里自研,2010 立项
PolarDB存储计算分离Parallel Raft阿里云原生
CockroachDBNewSQLRaft兼容 PostgreSQL 协议
YugabyteDBNewSQLRaft兼容 PostgreSQL/Cassandra
CitusPG 扩展-把 PG 变成分布式

🎯 避坑点:分库分表 ≠ 分布式数据库

维度分库分表分布式数据库
兼容性需改造 SQL兼容 MySQL/PG 协议
跨库 JOIN不支持支持(通过下推)
跨库事务需 Seata天然支持(Percolator/TCC)
运维复杂(数据迁移、扩容)较简单(自动 Rebalance)
成本中等较高(多副本)
选型时机1000 万 - 100 亿行100 亿 + 行

分库分表是"穷人版"分布式数据库——成本低但改造大。

二、选型史:MySQL → PostgreSQL → NewSQL 的三次跃迁

数据库的演化不是一夜之间完成的。从 1995 年 MySQL 1.0 发布到 2015 年 TiDB 开源,20 年时间完成了三次关键跃迁

2.1 三次跃迁时间线

2.2 第一次跃迁:从 MySQL 到 PostgreSQL

触发场景:复杂查询、GIS、JSON、CTE 等高级特性需求。

维度MySQL 8.0PostgreSQL 16
SQL 兼容性中等(最接近 SQL 标准)
JSON 支持JSON 类型JSONB(二进制索引)
GIS 支持基础PostGIS(业界标杆)
CTE / 窗口函数8.0 才开始早就支持
全文检索基础丰富(多语言、权重)
物化视图8.0 GA支持
复杂查询性能(优化器好)
写入性能
复制binlog流复制 + 逻辑复制
运维生态丰富(国内首选)中等(专业领域首选)
适合场景高并发 OLTPOLAP + 复杂业务

📌 实践:什么时候该迁到 PG

  • 业务里有 GIS(地图、附近的人、配送范围)
  • JSON 字段需要查询(半结构化数据)
  • 复杂报表(窗口函数、CTE、递归查询)
  • 全文检索要求高(多语言、模糊搜索)
  • 严格 SQL 规范(金融、政府)

不是性能问题,而是特性问题。MySQL 5.7 之后性能差距已大幅缩小。

2.3 第二次跃迁:从 MySQL 主从到分库分表

触发场景:单库行数超 5 亿,QPS 持续走高,写入瓶颈出现。

代表事件

  • 2012 年:淘宝"去 IOE",MySQL + 自主中间件取代 Oracle
  • 2014 年:阿里 OceanBase 在双 11 承担核心交易库
  • 2016 年:Sharding-JDBC(当当,后并入 ShardingSphere)成为事实标准
  • 2018 年:MyCat 流行,代理式分库分表方案成熟

2.4 第三次跃迁:从分库分表到 NewSQL

触发场景:分库分表的运维成本已经超过数据库本身的成本。

痛点表现
扩容噩梦32 库扩到 64 库要全量数据迁移
跨库 JOIN性能差,业务层要做内存 JOIN
分布式事务强一致事务需 Seata,复杂
运维复杂几百个分片,几百个 binlog 链路
研发效率SQL 受限,需求 30% 时间在做"分库适配"

应对:用 NewSQL 把分库分表的"中间件逻辑"下沉到数据库内部。

数据库立项时间开源时间关键节点
OceanBase2010 阿里2019(OB 1.0)2014 双 11 抗住 8 万笔/秒
TiDB2015 PingCAP2017(GA)2018 京东"跑赢"分库分表
CockroachDB2015 Cockroach Labs20172021 22.1 GA
YugabyteDB2016 Yugabyte20182.0 GA(PG 兼容)
PolarDB2015 阿里云闭源2018 云原生 + 存储计算分离

💡 原理:NewSQL 的核心创新

  1. 计算-存储分离:SQL 层无状态,可无限扩容
  2. 数据自动分片(Auto-Sharding):应用不感知
  3. 多副本强一致:Paxos / Raft 协议保证
  4. HTAP 混合负载:一份数据支持 OLTP + OLAP

本质是用分布式协议换"无限扩展"

三、为什么必须演化:4 大驱动力

3.1 驱动力一:数据量爆炸

来自 IDC 的预测:

  • 2010 年:全球数据量 1.2 ZB
  • 2020 年:59 ZB(5 年涨 50 倍
  • 2025 年:175 ZB
  • 2030 年:612 ZB

企业级数据库的"舒适区"是 1TB-5TB 单库。超过这个量级,单机的物理极限就开始显现

  • B+Tree 树高 > 4,索引深度查询变慢
  • 备份/恢复时间线性增长(10TB 恢复 8 小时+)
  • 主从延迟上升(大事务、批量更新)
  • 运维窗口变窄(DDL 时间从分钟级到小时级)

3.2 驱动力二:并发量陡增

业务阶段QPS数据库形态
早期< 1 万单机 MySQL
成长期1-10 万主从 + 读写分离
爆发期10-100 万分库分表 + 缓存
巨型业务> 100 万分布式数据库 + 多级缓存

🎯 避坑点:不要过早分库分表

分库分表的成本:

  • 业务改造(30% 工期)
  • 跨库 JOIN 性能损失(10-100 倍)
  • 分布式事务(20% 性能损耗)
  • 运维复杂度 ×3

过早分库分表 = 把工程能力浪费在"应对未来"。先看实际数据量:单表 > 5000 万行再考虑。

3.3 驱动力三:可用性要求

业务连续性分级:

等级业务类型RTORPO推荐方案
P0核心交易< 5 分钟< 1 分钟异地多活
P1重要业务< 30 分钟< 5 分钟同城双活 + 异地灾备
P2一般业务< 数小时< 1 小时同城灾备(热备)
P3后台系统< 1 天< 1 天主从副本即可

MySQL 主从最多撑到 P2。要做到 P0 的 RTO < 5 分钟,必须异地多活 + 分布式数据库。

3.4 驱动力四:合规与云原生

  • 强监管行业(金融、医疗):要求两地三中心 + 同城双活
  • GDPR / 等保 2.0:数据本地化 + 可审计
  • 云原生:要求弹性扩缩 + 按量计费 + 跨可用区容灾

自建 MySQL 越来越难满足这些要求,必须演进到云原生数据库(RDS、PolarDB、Aurora)。

四、MySQL 深度优化

在谈"要不要换数据库"之前,先把 MySQL 榨干——大多数团队连 MySQL 8.0 的 50% 性能都没用到。

4.1 存储引擎选择

MySQL 8.0 已经全面 InnoDB,MyISAM 只用于只读历史表。

引擎锁粒度事务崩溃恢复适用
InnoDB行锁支持Redo + Undo Log99% 场景
MyISAM表锁不支持只读历史表
Memory表锁不支持断电丢临时表
Archive行锁不支持日志归档

4.2 索引原理:B+Tree 是什么

B+Tree 是 InnoDB 的核心数据结构

B+Tree 特性影响
数据都在叶子节点查询路径长度固定
叶子节点双向链表范围查询 O(log N)
非叶子节点只存键值单页能放更多索引项
树高 3-4 层千万级数据查询 < 4 次 IO

B+Tree 的物理结构(InnoDB 16KB 页)

  • 每页 16KBinnodb_page_size,默认)
  • 每页最多 7992 行(行最小 2 字节,最大 8KB)
  • 页内查找用二分:Page Directory 槽位记录偏移

页分裂(Page Split)

随机插入 / 自增主键满时,InnoDB 会做页分裂——把一页拆成两页。页分裂 = 性能杀手

  • 拆页期间要加锁
  • 产生磁盘碎片
  • 顺序写变成随机写

对策

  • 主键用自增 ID(顺序写,几乎不分裂
  • 不要用 UUID(随机写,频繁分裂
  • 提前预留空间:innodb_fill_factor(8.0)

💡 原理:为什么 B+Tree 而不是 B-Tree 或 Hash

  • Hash 索引:等值查询 O(1),但不支持范围查询、不支持排序
  • B-Tree:非叶子节点也存数据,范围查询要回根节点
  • B+Tree:数据全在叶子,叶子双向链表,范围查询一次扫描即可

MySQL InnoDB 的自适应哈希索引(AHI)是对 B+Tree 的"局部优化"——把热点页加载到内存 Hash。

4.3 索引优化的 12 条法则

法则 1:最左前缀原则

联合索引 (a, b, c) 相当于建了 (a)(a, b)(a, b, c) 三个索引,但不能跳过 a

1
2
3
4
5
6
7
-- 走索引
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
SELECT * FROM t WHERE a = 1;

-- 不走索引(跳过了 a)
SELECT * FROM t WHERE b = 2;

法则 2:避免在索引列上做计算

1
2
3
4
5
-- 错:索引失效
SELECT * FROM t WHERE YEAR(create_time) = 2025;

-- 对:范围查询
SELECT * FROM t WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01';

法则 3:字符串前缀索引

1
2
-- 邮箱字段,前 10 个字符区分度已经够
ALTER TABLE user ADD INDEX idx_email_prefix (email(10));

法则 4:覆盖索引(Covering Index)

1
2
3
-- 表 user 有索引 (name, age),查询只要 name 和 age
SELECT name, age FROM user WHERE name = 'tom';
-- 不需要回表 → 覆盖索引

法则 5:OR 改 UNION

1
2
3
4
5
6
7
-- 错:or 可能索引失效
SELECT * FROM t WHERE a = 1 OR b = 2;

-- 对:UNION ALL 拆开
SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2;

法则 6:IN 控制在 1000 以内

1
2
3
4
-- MySQL IN 超过 1000 会拆成 OR,效率急剧下降
SELECT * FROM t WHERE id IN (1,2,3,...,999);
-- 改成 JOIN:
SELECT t.* FROM t JOIN temp_ids ON t.id = temp_ids.id;

法则 7:避免 SELECT *

1
2
3
4
5
-- 错:SELECT * 可能用不到覆盖索引
SELECT * FROM t WHERE user_id = 1;

-- 对:只查需要的列
SELECT id, name FROM t WHERE user_id = 1;

法则 8:NOT IN 改 NOT EXISTS

1
2
3
4
5
6
7
-- 错
SELECT * FROM t WHERE id NOT IN (SELECT id FROM blacklist);

-- 对
SELECT t.* FROM t WHERE NOT EXISTS (
    SELECT 1 FROM blacklist b WHERE b.id = t.id
);

法则 9:连接字段加索引

1
2
3
-- 任何 JOIN 字段必须加索引
ALTER TABLE order ADD INDEX idx_user_id (user_id);
ALTER TABLE order ADD INDEX idx_product_id (product_id);

法则 10:避免深分页

1
2
3
4
5
-- 错:limit 1000000, 20 → 扫 100 万行
SELECT * FROM t ORDER BY id LIMIT 1000000, 20;

-- 对:游标分页
SELECT * FROM t WHERE id > 1000000 ORDER BY id LIMIT 20;

法则 11:批量写入

1
2
3
4
5
6
7
8
-- 错:循环 1 万次 INSERT
for (Order o : orders) {
    insert(o);
}

-- 对:批量 INSERT
INSERT INTO order (...) VALUES (...),(...),(...) ... ;
-- 推荐每批 500-1000 行

法则 12:定期 ANALYZE TABLE

1
2
-- 重新统计索引基数(用于优化器决策)
ANALYZE TABLE order;

4.4 EXPLAIN 执行计划全解

EXPLAIN 是 MySQL 优化的"听诊器"

1
EXPLAIN SELECT * FROM order WHERE user_id = 1 AND status = 'PAID';
字段含义优化目标
idSELECT 序号越大越先执行
select_type类型SIMPLE > PRIMARY > UNION > SUBQUERY
type访问类型system > const > eq_ref > ref > range > index > ALL
possible_keys可能用的索引多个候选
key实际用的索引NULL = 全表扫
key_len索引长度越短越好
rows扫描行数(估算)越小越好
Extra额外信息关注 Using filesort / Using temporary

type 字段 7 个等级(性能从高到低):

1
2
3
system > const > eq_ref > ref > range > index > ALL
                                          ^^^^^^^^
                                          出现这个要警觉
type含义示例
system系统表SELECT * FROM mysql.proxied_user
const主键 / 唯一索引等值SELECT * FROM t WHERE id = 1
eq_refJOIN 主键 / 唯一索引t1 JOIN t2 ON t1.id = t2.id
ref非唯一索引SELECT * FROM t WHERE name = ’tom'
range索引范围WHERE id > 100 AND id < 200
index全索引扫SELECT id FROM t(只查索引列)
ALL全表扫SELECT * FROM t WHERE name LIKE ‘%tom%’

📌 实践:EXPLAIN 看什么

  1. type 是不是 ALL——出现 ALL 必须优化
  2. rows 是不是过大——> 1000 行要警觉
  3. Extra 是不是有 filesort / temporary——出现就考虑加索引
  4. key 是不是 NULL——没走索引
  5. key_len 是不是太小——联合索引没用到前列

4.5 MySQL 参数调优

不要照搬网上参数——要根据实际业务、机器配置、QPS 量级调。

内存相关(最关键)

参数默认值建议值说明
innodb_buffer_pool_size128MB物理内存 50-70%InnoDB 缓存池
innodb_log_buffer_size16MB64-256MBredo log 缓冲
key_buffer_size8MB32MBMyISAM 索引缓冲(基本无用)
tmp_table_size16MB64-256MB临时表
max_heap_table_size16MB64-256MBMEMORY 引擎表
sort_buffer_size256KB1-4MB排序缓冲
join_buffer_size256KB1-4MBJOIN 缓冲
read_buffer_size128KB1-2MB全表扫读缓冲

连接相关

参数默认值建议值说明
max_connections1511000-5000最大连接数
max_user_connections00单用户最大连接
wait_timeout28800600空闲连接超时(秒)
interactive_timeout28800600交互超时

InnoDB 写入相关

参数默认值建议值说明
innodb_flush_log_at_trx_commit11 / 21=最安全;2=折中(每 1s 刷盘)
sync_binlog11 / 01=安全;0=性能
innodb_flush_method-O_DIRECT绕过 OS Cache
innodb_io_capacity2002000(SSD)刷盘 IO 能力

🛑 误区警示:innodb_flush_log_at_trx_commit=2 是不是不安全?

不是。= 2 表示"每秒刷一次盘",最多丢 1 秒数据。 大多数业务(订单、用户)能接受 1 秒数据丢失以换取 10 倍写入性能。 金融场景必须 = 1

二进制日志

参数默认值建议值说明
binlog_formatROWROW推荐 ROW(数据一致)
binlog_row_imageFULLFULL / MINIMALMINIMAL 减小 binlog
expire_logs_days07-30自动清理
max_binlog_size1GB512MB-1GB单文件大小

💡 原理:binlog_format 怎么选

  • STATEMENT(默认):记录 SQL 语句。日志小,但有主从一致性问题(如 NOW()
  • ROW:记录行变更。日志大但绝对一致生产推荐 ROW
  • MIXED:自动判断。别用,行为不可预期

4.6 MySQL 监控指标

SRE 必看 7 大指标

指标阈值监控工具
QPS / TPS< 80% 设计上限Prometheus + mysqld_exporter
连接数使用率< 80%mysqld_exporter
InnoDB buffer pool 命中率> 99%命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)
慢查询数量< 0.1% 总查询slow_query_log + pt-query-digest
主从延迟< 1 秒Seconds_Behind_Master
锁等待0 长锁SHOW ENGINE INNODB STATUS
磁盘 IO 利用率< 70%iostat

🎯 避坑点:buffer pool 命中率

命中率 < 99% 通常意味着热点数据 > buffer pool不要急着加内存——先看:

  1. 是不是有全表扫占用了 buffer pool(SHOW ENGINE INNODB STATUS 看 LRU 列表)
  2. 是不是有 SELECT * 拉大对象
  3. 是不是表数据本身远大于内存

盲加内存是治标不治本

五、PostgreSQL 何时上位 + 杀手特性

5.1 何时该迁到 PG

MySQL 没死,但有些场景 MySQL 就是干不过 PG

场景 1:复杂 SQL 报表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- PG 窗口函数:一行 SQL 算同比环比
SELECT 
    region,
    month,
    revenue,
    revenue - LAG(revenue, 1) OVER (PARTITION BY region ORDER BY month) AS mom_diff,
    (revenue - LAG(revenue, 12) OVER (PARTITION BY region ORDER BY month)) 
        / NULLIF(LAG(revenue, 12) OVER (PARTITION BY region ORDER BY month), 0) AS yoy_rate
FROM monthly_sales
WHERE month >= '2024-01-01';

-- MySQL 8.0 才支持窗口函数,但优化器没 PG 强

场景 2:GIS 地理信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- PG + PostGIS:找用户附近 5 公里的所有商家
SELECT s.*, 
    ST_Distance(s.location, ST_MakePoint(116.4, 39.9)::geography) AS dist_m
FROM shop s
WHERE ST_DWithin(
    s.location, 
    ST_MakePoint(116.4, 39.9)::geography, 
    5000
)
ORDER BY dist_m ASC
LIMIT 20;

场景 3:半结构化数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- PG JSONB:可索引
CREATE TABLE product (
    id BIGSERIAL PRIMARY KEY,
    name TEXT,
    attrs JSONB
);
CREATE INDEX idx_attrs ON product USING gin (attrs);

-- 查询 attrs->>'brand' = 'Apple'
SELECT * FROM product WHERE attrs @> '{"brand": "Apple"}';

场景 4:递归查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- PG 递归 CTE:组织树 / 评论树
WITH RECURSIVE org_tree AS (
    SELECT id, name, parent_id, 0 AS depth
    FROM department
    WHERE parent_id IS NULL
    
    UNION ALL
    
    SELECT d.id, d.name, d.parent_id, t.depth + 1
    FROM department d
    JOIN org_tree t ON d.parent_id = t.id
)
SELECT * FROM org_tree ORDER BY depth;

场景 5:严格一致性

1
2
3
-- PG 严格遵循 SQL 标准
-- 支持 SERIALIZABLE 隔离级别(MySQL 没有真正的 SERIALIZABLE)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

5.2 PG 杀手特性详解

5.2.1 JSONB vs MySQL JSON

维度MySQL JSONPG JSONB
存储格式文本二进制(解析快)
索引虚拟列原生 GIN 索引
查询语法JSON_EXTRACT->> -> @>
性能一般快 2-10 倍

5.2.2 MVCC 的优势

MVCC(Multi-Version Concurrency Control)

  • MySQL InnoDB:通过 undo log 实现,长事务会膨胀 undo 表空间
  • PostgreSQL:每行存两个 xmin/xmax,老版本在 vacuum 时清理

📌 实践:autovacuum 调优

PG 的 MVCC 优势是"读不阻塞写、写不阻塞读"。 但老版本数据不清理会膨胀。autovacuum 必须调好:

参数默认建议说明
autovacuum_vacuum_scale_factor0.20.05-0.1触发 vacuum 的死行比例
autovacuum_analyze_scale_factor0.10.05触发 analyze 的变更比例
autovacuum_vacuum_cost_limit2001000-2000vacuum 速度
autovacuum_max_workers34-8并发 worker 数

大表要单独配 autovacuumALTER TABLE big_table SET (autovacuum_vacuum_scale_factor = 0.02)

5.2.3 复制方案

PG 有 3 种复制

复制方式说明用途
流复制(Streaming Replication)基于 WAL,类似 binlog高可用、读写分离
逻辑复制(Logical Replication)基于发布订阅,跨版本数据迁移、聚合
级联复制(Cascading Replication)多级从库异地灾备
1
2
3
4
5
-- 创建发布
CREATE PUBLICATION pub_orders FOR TABLE orders, order_items;

-- 订阅端
CREATE SUBSCRIPTION sub_orders CONNECTION 'host=primary port=5432 user=repl password=xxx dbname=app' PUBLICATION pub_orders;

5.2.4 分区表

PG 10+ 原生分区表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 范围分区(按时间)
CREATE TABLE order (
    id BIGSERIAL,
    user_id BIGINT,
    amount DECIMAL(10,2),
    created_at TIMESTAMPTZ
) PARTITION BY RANGE (created_at);

CREATE TABLE order_2025 PARTITION OF order
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE order_2026 PARTITION OF order
    FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

5.3 PG 参数调优

内存相关

参数默认值建议值说明
shared_buffers128MB物理内存 25%PG 缓存池
work_mem4MB32-256MB排序/哈希缓冲
maintenance_work_mem64MB512MB-2GBvacuum / CREATE INDEX
effective_cache_size4GB物理内存 50-75%给优化器的"提示"
huge_pagestrytry大内存机器开启

WAL 相关

参数默认值建议值说明
wal_levelreplicareplica / logicallogical 启用逻辑复制
max_wal_size1GB4-16GBWAL 总大小
min_wal_size80MB1-2GBWAL 最小保留
checkpoint_completion_target0.50.9减少 checkpoint 抖动

🎯 避坑点:work_mem 调多大

  • work_mem每个操作的内存,不是全局
  • 一个复杂查询可能有 10 个 sort 节点 → 实际占用 10 × work_mem
  • 1000 个并发连接 × work_mem 256MB = 256GB 内存需求
  • 生产建议 32-64MB,复杂查询用 SET LOCAL work_mem = '1GB'; 临时调大

六、主从复制 + 读写分离

6.1 复制原理

三步流程

  1. 主库:所有数据变更写 binlog(ROW 模式)
  2. 从库 IO 线程:拉主库 binlog,写入本地 relay log
  3. 从库 SQL 线程:重放 relay log 到从库

6.2 复制模式

模式延迟安全性适用
异步(默认)10-100ms主库挂丢数据普通业务
半同步+1 RTT主库挂最多丢 1 秒重要业务
增强半同步+1-2 RTT主库挂不丢金融

SHOW SLAVE STATUS 关键指标(MySQL 8.0 后是 SHOW REPLICA STATUS):

字段期望异常处理
Slave_IO_Running: Yes必须 YesNo → 网络/账号问题
Slave_SQL_Running: Yes必须 YesNo → SQL 线程挂了
Seconds_Behind_Master: 00> 0 → 主从延迟
Master_Log_File / Read_Master_Log_Pos持续增长不动 → IO 线程有问题
Relay_Master_Log_File / Exec_Master_Log_Pos持续增长不动 → SQL 线程卡住
Last_Error非空 → 看错误码

GTID 模式(强烈推荐)

GTID = Global Transaction Identifier,每个事务全局唯一。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 主库配置
gtid_mode = ON
enforce_gtid_consistency = ON

-- 从库 CHANGE MASTER
CHANGE MASTER TO
    MASTER_HOST='master.host',
    MASTER_USER='repl',
    MASTER_PASSWORD='xxx',
    MASTER_AUTO_POSITION=1;  -- 用 GTID 自动定位

START SLAVE;

💡 原理:GTID 解决什么

不开启 GTID 的痛点

  • 主从切换需要找 binlog file + position(容易错)
  • 级联复制链路复杂(A → B → C,B 故障时 C 找不到位点)
  • 跳过一个错误要手动算位点

开启 GTID 的好处

  • 自动定位,无需 file + position
  • 跳过错误简单:SET GTID_NEXT='xxx'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC';
  • 故障切换几乎是零配置

6.3 主从延迟的 7 种解决方案

主从延迟是读写分离最大的坑。常见解法:

方案原理适用
强制读主写后立即读主库强一致场景
Session 标记同一个 session 写后短时间走主简单业务
缓存标记写时在缓存记录 N 秒"读主"标记通用
查主库判断读从前先查"是否最近写过"极端场景
GTID 等待从库等到指定 GTID 应用完MySQL 5.7+
延迟监控 + 告警延迟 > 阈值告警必须有
业务回避不做读写分离一致性要求极高

最推荐"缓存标记法"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void write(Order order) {
    // 写主库
    orderRepository.save(order);
    
    // 写后 1 秒内强制读主
    redisTemplate.opsForValue().set(
        "force-master:" + order.getUserId(),
        "1",
        1, TimeUnit.SECONDS
    );
}

public Order read(Long userId, Long orderId) {
    // 查从库前看是否要强制主
    Boolean force = redisTemplate.hasKey("force-master:" + userId);
    if (force) {
        return orderRepository.findMasterById(orderId);
    }
    return orderRepository.findSlaveById(orderId);
}

6.4 读写分离中间件对比

中间件模式性能复杂度适用
ShardingSphere-JDBC客户端 jar最优(无网络跳)Java 应用首选
ShardingSphere-Proxy代理跨语言
MyCat代理复杂分库分表
MaxScale代理MariaDB
ProxySQL代理MySQL

📌 实践:选型建议

  • 纯 Java 应用:ShardingSphere-JDBC(性能最好)
  • 多语言应用:ShardingSphere-Proxy
  • 只用读写分离,不分库分表:ProxySQL / 自研 AOP

七、分库分表

7.1 何时分库分表

经验阈值(InnoDB + 良好索引):

指标阈值风险
单表行数5000 万查询性能下降
单表容量50GBDDL 变慢
单库 QPS1-3 万CPU 打满
写并发5000 TPS主从延迟

🎯 避坑点:分库分表不是"应不应该",是"什么时候"

  • < 1000 万行:坚决不分
  • 1000 万 - 5000 万:加索引 + 优化 SQL 还能撑
  • 5000 万 - 5 亿:分库分表
  • > 5 亿:直接上分布式数据库

提前分库分表是过早优化,推迟分库分表是踩坑

7.2 拆分维度

垂直拆分

垂直分库:按业务边界拆。最简单、最有用,应该第一个做。

垂直分表:宽表拆主表 + 扩展表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- 主表(常用字段)
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    nickname VARCHAR(50),
    avatar VARCHAR(200),
    created_at TIMESTAMP
);

-- 扩展表(不常用字段)
CREATE TABLE user_ext (
    user_id BIGINT PRIMARY KEY,
    bio TEXT,
    address TEXT,
    extra JSON
);

水平拆分

水平分库:同一张表的数据按规则分到不同库。 水平分表:同一张表的数据按规则分到同库不同表。

7.3 Sharding 策略

策略优点缺点适用
Hash 取模分布均匀扩容要数据迁移通用
Range 范围不迁移老数据热点(最新数据访问多)时序数据
List 列表业务清晰数据不均业务隔离
地理分片用户延迟低跨区数据不一致O2O
一致性 Hash扩容只影响 1/N复杂度高大规模

Hash 取模最常用:

1
2
3
4
5
// 64 库 × 64 表 = 4096 分片
int dbIndex = (int)(userId % 64);
int tableIndex = (int)((userId / 64) % 64);
String tableName = "order_" + tableIndex;
// 实际 SQL: SELECT * FROM db_0.order_15 WHERE user_id = ?;

7.4 分布式主键

分库分表后,自增 ID 失效。常见方案:

方案性能全局唯一推荐度
UUID缺点:占空间、无序
Snowflake(雪花算法)推荐
美团 Leaf推荐
数据库号段中小厂
Redis incr中小厂

Snowflake 64 位结构

1
2
3
4
0 | 0000000... | 0000000000 0000000000 0000000000 0000000000 0000 | 00000 | 00000
  ^    41 位时间戳(毫秒)                          ^^^^^^^^^^^^    ^^^^^    ^^^^^
  符号    基准 2020-01-01                            12 位序列       5 位     5 位
                                                   (每毫秒 4096)  数据中心  机器

每毫秒可生成 4096 个 ID,每秒 409.6 万

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Snowflake {
    private final long datacenterId;
    private final long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                timestamp = waitNextMillis();
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - 2020_01_01_EPOCH) << 22)
            | (datacenterId << 17)
            | (workerId << 12)
            | sequence;
    }
}

7.5 跨库查询 + 跨库事务

跨库查询

分库分表最大的代价

1
2
3
4
5
-- 这条 SQL 走不了分库分表
SELECT o.*, u.username 
FROM order o JOIN user u ON o.user_id = u.id
WHERE o.id = 12345;
-- order 在分库 0 的 order_15,user 在分库 32

解决方案

方案思路适用
业务层 JOIN先查 order 拿到 user_id,再查 user简单但要 2 次查询
宽表冗余order 表冗余 username 字段适合读多写少
搜索引擎全量同步到 ES,在 ES 查适合复杂查询
数据仓库T+1 同步到 Hive / ClickHouse适合 OLAP
广播表小表(字典)全库冗余字典类

跨库事务

分布式事务的 4 种方案(已在前作展开):

方案一致性性能复杂度
2PC
TCC
Saga最终
本地消息表最终

Seata AT 模式:业界主流

Seata = Simple Extensible Autonomous Transaction Architecture,阿里开源。AT 模式是其默认工作模式:

AT 模式的"魔法"

  1. 第一阶段:执行业务 SQL,生成 undo log(前镜像),不提交
  2. 第二阶段:TC 看所有分支都成功 → 全局提交;任一失败 → 用 undo log 回滚
  3. 应用层无感知:基于数据源代理(DataSourceProxy)自动接管

AT 模式的限制

  • 必须是关系型数据库
  • SQL 必须是单条(不支持跨 SQL 事务)
  • 全局锁依赖 TC,TC 单点问题

🛑 误区警示:尽量不用分布式事务

能用业务回避就别用事务

  • 把"扣库存 + 下订单"拆成"先预占库存再下单 + 异步扣库存"
  • 用最终一致性 + 对账保证正确性
  • 99% 的分布式事务场景可以用业务设计绕开

7.6 ShardingSphere 实战

ShardingSphere 5.x 是当前 Java 生态首选

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://10.0.0.1:3306/db_order_0
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://10.0.0.2:3306/db_order_1
    rules:
      sharding:
        tables:
          order:
            actual-data-nodes: ds$->{0..1}.order_$->{0..15}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: tbl-mod
        sharding-algorithms:
          db-mod:
            type: MOD
            props:
              sharding-count: 2
          tbl-mod:
            type: MOD
            props:
              sharding-count: 16

八、NewSQL 三剑客

8.1 TiDB 详解

TiDB = MySQL 协议 + 分布式架构

组件作用
TiDB ServerSQL 层,无状态,可横向扩
PD(Placement Driver)集群调度 + TSO 时间戳
TiKV存储层,基于 RocksDB + Raft
优势说明
MySQL 兼容95% 场景无需改代码
水平扩展加 TiKV 节点即扩
强一致Raft + Percolator 事务
HTAPTiFlash 列存引擎支持 OLAP
自动 Rebalance不停服迁移数据
劣势说明
运维复杂至少 6 个组件(3 TiDB + 3 PD + 3 TiKV + TiFlash)
大事务支持差单事务 < 100MB 限制
二级索引全局索引有性能开销
成本3 副本起步,资源消耗 3 倍

8.2 OceanBase 详解

OceanBase = 原生分布式 + 金融级

优势说明
Paxos 多副本强一致 + 高可用
Oracle 兼容替代 Oracle
HTAP同一份数据 OLTP + OLAP
多模兼容 MySQL、Oracle、PG

关键事件

  • 2010:阿里立项
  • 2014:双 11 抗住 8 万笔/秒
  • 2019:OceanBase 1.0 开源
  • 2020:TPC-C 基准测试 7.07 亿 tpmC(世界第一)
  • 2023:OceanBase 4.0 GA

8.3 PolarDB 详解

PolarDB = 存储计算分离

核心创新

  • 共享存储:所有节点挂同一份数据,读节点无延迟
  • RDMA 网络:节点间通信 < 100us
  • 并行回放:从库并行重放 redo log

8.4 选型决策树

8.5 一致性协议速览

协议特点代表
Paxos经典,难懂OceanBase、Chubby
Raft易懂的 PaxosTiKV、etcd、CockroachDB
ZABZK 专属ZooKeeper
EPaxosPaxos 改进学术
Parallel RaftRaft 并行PolarDB

💡 原理:Raft 怎么保证一致

  1. Leader 选举:多数派投票选主
  2. 日志复制:Leader 接收写,复制到多数 Follower
  3. 提交时机:多数派落盘才提交
  4. 脑裂解决:通过 Term(任期号)防旧 Leader 复活

九、容灾 + 备份

9.1 容灾等级

等级描述RTORPO成本
L0无备份--0
L1本地备份数小时数小时1x
L2本地 + 异地备份数小时数小时1.2x
L3热备(同城)分钟02x
L4双活(同城)03-4x
L5双活(异地)5-8x
L6多活(异地)010-15x

9.2 备份策略

3-2-1 备份铁律

  • 3 份数据副本
  • 2 种存储介质(如本地 + 异地)
  • 1 份离线(防勒索病毒)

物理备份 vs 逻辑备份

维度物理备份逻辑备份
工具xtrabackup / pg_basebackupmysqldump / pg_dump
速度(直接拷文件)慢(逐行转 SQL)
压缩
恢复速度
跨版本不支持支持
跨平台受限不受限
适用大库(> 100GB)小库、结构迁移

9.3 xtrabackup 实战

xtrabackup 是 MySQL 物理备份的首选工具(Percona 开源)。

xtrabackup 工作原理(关键 4 步):

xtrabackup 的"魔法"是:备份期间不锁库——通过持续跟踪 redo log 拿到一致性快照。这比 mysqldump 在线备份强 10 倍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 全量备份
xtrabackup --backup \
    --target-dir=/data/backup/full/2026-06-09 \
    --user=backup --password=xxx

# 准备(apply-log)
xtrabackup --prepare \
    --target-dir=/data/backup/full/2026-06-09

# 恢复
xtrabackup --copy-back \
    --target-dir=/data/backup/full/2026-06-09 \
    --datadir=/var/lib/mysql

# 增量备份
xtrabackup --backup \
    --target-dir=/data/backup/inc/2026-06-09 \
    --incremental-basedir=/data/backup/full/2026-06-08 \
    --user=backup

📌 实践:xtrabackup 备份策略

  • 全量备份:每周一次(业务低峰期)
  • 增量备份:每天一次
  • binlog 备份:实时(用 mysqlbinlog 或复制到远程)
  • 保留期:全量 30 天,增量 7 天,binlog 14 天

9.4 PITR(Point-in-Time Recovery)

PITR = 时间点恢复。从全量备份 + binlog 恢复到任意时间点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 假设误操作发生在 14:32:00,要恢复到 14:31:00

# 1. 恢复全量备份
xtrabackup --prepare --target-dir=/data/backup/full/2026-06-09
xtrabackup --copy-back --target-dir=/data/backup/full/2026-06-09

# 2. 启动 MySQL,配置只应用到 14:31:00
# 启动后用 mysqlbinlog 过滤
mysqlbinlog --stop-datetime="2026-06-09 14:31:00" \
    /var/lib/mysql/binlog.000123 \
    | mysql -u root

# 3. 验证数据 OK 后,重启 MySQL

9.5 PostgreSQL 备份

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 逻辑备份
pg_dump -h localhost -U postgres -F c -b -v -f "backup_$(date +%Y%m%d).dump" mydb

# 恢复
pg_restore -h localhost -U postgres -d mydb_new backup.dump

# 物理备份(PG 17+ pg_basebackup 增量)
pg_basebackup -D /data/backup/base -Fp -Xs -P

# 持续归档(用于 PITR)
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'cp %p /data/wal_archive/%f'

9.6 备份验证:恢复演练

最关键的步骤——备份没演练 = 不存在

演练清单

  • 每月一次:全量备份恢复演练(人工)
  • 每季度一次:PITR 演练(恢复到指定时间点)
  • 每年一次:跨机房灾备切换演练
  • 每季度一次:核对备份文件大小、时间、压缩率
  • 每月一次:检查备份脚本日志,告警是否正常

🛑 误区警示:这些坑别踩

  1. 备份完从不验证——某公司 3 年备份全部损坏
  2. 备份和数据库在同一块盘——磁盘坏了备份也没了
  3. 没做异地备份——机房故障 = 全丢
  4. 没加密——备份文件泄露 = 数据泄露
  5. 没做定期恢复演练——真出事时手忙脚乱

十、真实案例

10.1 某电商:MySQL → TiDB 的迁移史

背景:订单库单表 8 亿行,主从延迟 30 秒,分库分表 128 库 × 16 表。

痛点

  • 跨库 JOIN 几乎不可用
  • 扩容要全量迁移,耗时 72 小时
  • 分布式事务(Seata)性能损失 30%
  • DBA 维护 2000+ 分片

方案

  1. 第 1 期:核心订单库迁 TiDB(2023)
  2. 第 2 期:迁移商品库(2024)
  3. 第 3 期:迁移用户库(2025)

收益

  • 单库扩到 30 个 TiKV 节点
  • SQL 改造 5%(MySQL 协议)
  • 跨表 JOIN 性能提升 10 倍
  • 运维复杂度下降 50%

教训

  • 不要一次性迁全量:分库分批迁移
  • 写好回滚预案:每次迁移都要有回滚方案
  • 性能基线对比:迁移前后必须做压测对比

10.2 某金融:从 Oracle 到 OceanBase

背景:传统银行核心系统,Oracle RAC + 小型机。

痛点

  • Oracle 授权费高(年 1000 万+)
  • RAC 扩展性差,最多 8 节点
  • 国产化合规要求

方案

  • 数据库国产化:OceanBase 替代 Oracle
  • 应用改造:10% SQL 调整(兼容 Oracle 语法)
  • 灾备:3 地 5 中心部署

收益

  • 授权费归零
  • 节点扩到 30+
  • 性能持平或略优
  • 通过等保 2.0

10.3 某 SaaS:MySQL → PostgreSQL

背景:CRM 系统,复杂查询多(多表 JOIN + 窗口函数)。

痛点

  • MySQL 5.7 优化器对复杂 SQL 支持差
  • 一个 10 表 JOIN 查询要 30 秒
  • GIS 功能缺失(附近客户查询)

方案

  • 业务层改造:用 PG 的 CTE、窗口函数重写
  • 数据迁移:logical replication(MySQL → PG)
  • 工具链:MyBatis 适配 PG dialect

收益

  • 复杂查询从 30 秒降到 1.5 秒
  • PostGIS 替代 MongoDB(数据一致性更好)
  • 报表 SQL 简化 60%

十一、总结

11.1 数据库演化的 3 大核心要素

  1. 业务匹配:根据数据量、QPS、一致性要求选型
  2. 渐进演化:单机 → 主从 → 分库分表 → 分布式,不要一步到位
  3. 容灾底线:3-2-1 备份铁律 + 异地灾备 + 恢复演练

11.2 何时该上分布式数据库

业务特征推荐方案
数据量 < 5000 万单机 MySQL/PG
数据量 5000 万 - 10 亿分库分表 + 缓存
写为主(> 5000 TPS)分库分表优于 NewSQL
强 GIS / 复杂查询PG 优先
强 SQL 规范PG 优先

分布式数据库的 ROI 拐点在"单表 10 亿 + 跨库 JOIN 多"。低于这个规模做了也是浪费。

11.3 常见陷阱

🛑 误区警示:这些坑别踩

  1. 盲目上 NewSQL:业务量没到,成本先到
  2. 忽视 SQL 优化:先 EXPLAIN 再谈架构
  3. 不验证备份:3 年没演练过
  4. 盲目分库分表:500 万行就分,运维成本爆炸
  5. 忽视 PG 优势:复杂查询死磕 MySQL
  6. 不监控:buffer pool 命中率、慢查询、主从延迟
  7. 不升级 MySQL:还在用 5.6 / 5.7

11.4 系列预告

这是 Java Web 微服务系列 的第 8 篇。后续计划:

  • 服务治理:服务发现、配置中心、熔断降级
  • 网关与限流:Spring Cloud Gateway、Sentinel
  • 分布式事务:Seata、Saga、TCC 模式对比
  • 链路追踪:SkyWalking、Jaeger
  • Spring Cloud Alibaba 实战:Nacos + Sentinel + Seata 三件套
  • 可观测性体系:Metrics + Logging + Tracing
  • Kubernetes 落地:从 Docker 到 K8s 的完整路径
  • 多活架构:单元化、流量调度、灰度发布

十二、数据库实施清单

12.1 选型层 Checklist

  • 数据量评估:当前 + 3 年 + 5 年
  • QPS 评估:日常 + 大促 + 业务峰值
  • 一致性要求:强一致 / 最终一致
  • 特性需求:GIS / JSON / 复杂查询
  • 生态评估:团队熟悉度 + 招聘难度
  • 成本评估:授权费 + 机器 + 运维人力

12.2 性能层 Checklist

  • 索引设计:联合索引遵循最左前缀
  • 慢查询治理:每月清理 top 10 慢 SQL
  • EXPLAIN 检查:关键 SQL 都跑过 EXPLAIN
  • 参数调优:buffer pool、work_mem、shared_buffers
  • 监控告警:7 大指标全埋点
  • 批量写入:避免循环单条 INSERT
  • 深分页改造:游标分页替代 OFFSET

12.3 高可用层 Checklist

  • 主从复制:GTID + 增强半同步
  • 读写分离:ShardingSphere-JDBC
  • 自动故障切换:MHA / Orchestrator
  • 主从延迟监控:> 1 秒告警
  • 连接数管理:池化 + 限流
  • 熔断降级:Sentinel / Resilience4j

12.4 容灾层 Checklist

  • 3-2-1 备份:3 份、2 种介质、1 离线
  • 定期演练:每月恢复演练
  • 异地备份:至少 100 公里外
  • PITR 验证:时间点恢复演练
  • binlog 归档:保留 14 天+
  • 灾备切换 Runbook:写好且演练过
  • 数据校验:备份完成后自动校验

12.5 团队层 Checklist

  • DBA 能力:能讲清楚 EXPLAIN + binlog 原理
  • SRE 能力:能做故障切换演练
  • 研发能力:懂索引优化 + 分库分表
  • 值班制度:7×24 数据库故障响应
  • 复盘机制:每次故障 24 小时内复盘

十三、常见问题 FAQ

Q1:MySQL 单表 1000 万行就一定要分表吗?

A不是

单表 1000 万行在 InnoDB + 良好索引下毫无压力。要看的不是行数,而是:

  • 实际查询响应时间(> 100ms 要优化)
  • 索引深度(B+Tree 树高 > 4 才考虑)
  • DDL 时间(> 1 小时考虑分表)

不要为了分表而分表

Q2:分库分表后 JOIN 怎么办?

A尽量避免跨库 JOIN

替代方案:

  • 业务层 JOIN:先查 A 拿 ID 列表,再查 B(接受 N+1)
  • 宽表冗余:A 表冗余 B 的常用字段
  • 搜索引擎:全量同步到 ES
  • 数据仓库:T+1 同步到 OLAP

90% 的跨库 JOIN 可以通过业务改造绕开

Q3:TiDB 和 OceanBase 怎么选?

A:看场景。

维度TiDBOceanBase
协议MySQLMySQL / Oracle / PG
一致性RaftPaxos
HTAPTiFlashOB 自带
生态开源、社区阿里主导 + 开源
Oracle 兼容不支持支持
适合互联网、MySQL 替代金融、Oracle 替代

MySQL 替代选 TiDB,Oracle 替代选 OceanBase

Q4:MySQL 主从切换怎么做?

A:用 MHA / Orchestrator / MGR。

工具特点
MHA老牌,脚本方式,10 秒切换
OrchestratorWeb 界面,MySQL 官方推荐
MGRMySQL 官方方案,但写性能差
ProxySQL + MHA代理 + 自动切换

生产推荐 Orchestrator——可视化、自动选主、告警齐全。

Q5:xtrabackup 备份期间影响业务吗?

A会有一些影响,但可控。

  • 影响

    • 备份期间磁盘 IO 升高 10-20%
    • 长事务的 undo log 备份会锁
    • 大库(> 1TB)备份时长 4-8 小时
  • 优化

    • 业务低峰期备份
    • --parallel=8 并行
    • --throttle=200 限速
    • --rsync 减少锁

核心库建议凌晨 2-6 点备份

Q6:PostgreSQL 的 autovacuum 怎么调?

A分表配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 大表单独配(普通表可以保留默认)
ALTER TABLE big_order SET (
    autovacuum_vacuum_scale_factor = 0.02,  -- 2% 死行触发
    autovacuum_analyze_scale_factor = 0.01, -- 1% 触发 analyze
    autovacuum_vacuum_cost_limit = 2000
);

-- 看 autovacuum 状态
SELECT relname, 
    n_live_tup, n_dead_tup, 
    last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

Q7:分布式数据库还要不要分库分表?

A不要

TiDB / OceanBase / CockroachDB 已经做了分片,再分库分表是重复建设。

  • 应用层只看到一个逻辑库
  • 数据库自己分片、扩缩、Rebalance
  • 跨节点 JOIN / 事务由数据库自己处理

迁到 NewSQL 后,把分库分表中间件全下掉

Q8:数据库故障时怎么快速止血?

A预案 + 演练

标准止血动作(5 分钟内):

  1. KILL 长事务SHOW PROCESSLIST; KILL <id>;
  2. 切只读SET GLOBAL read_only = 1;(让写流量失败而非拖死)
  3. 降级非核心业务:关掉报表、推荐、对账
  4. 重启主库:最后手段,先备份数据再重启
  5. 切主库:主库不可用时切到从库

平时就要演练——真出事时手忙脚乱 = 二次事故。

十四、推荐阅读

官方文档

书籍

  • 《高性能 MySQL》(Baron Schwartz)—— MySQL 优化的必读圣经
  • 《MySQL 8.0 高级应用与调优》(张甦)—— 国产佳作
  • 《PostgreSQL 实战》(唐成)—— 中文 PG 实战
  • 《数据密集型应用系统设计》(DDIA, Martin Kleppmann)—— 分布式系统圣经
  • 《大型网站技术架构:核心原理与案例分析》(李智慧)—— 阿里专家作品

技术博客

  • 阿里云数据库团队博客:RDS、PolarDB、OceanBase 实战
  • PingCAP 技术博客:TiDB 原理与最佳实践
  • Percona Database Performance Blog:MySQL 优化全球权威
  • 美团技术团队:数据库运维与分库分表
  • 字节跳动技术博客:分布式数据库落地

开源项目

💡 学习路径建议

  1. 入门:本文 + MySQL 8.0 官方文档,2 周能上手
  2. 进阶:阿里 / PingCAP / Percona 博客,理解工业实践
  3. 实战:用 ShardingSphere 搭一个分库分表 demo
  4. 深潜:读 TiDB 源码(建议从 tidb/server 入口),理解分布式 SQL 引擎

理论 + 实践 + 源码,缺一不可。

参考文章

本系列共 16 篇,本文为第 10 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计