Java开发必知必会的MySQL核心知识点(四)-日志与高可用架构:从单机到集群 前三篇我们一直在单机 MySQL的范围内打转——单台机器上的索引怎么建、事务怎么管、锁怎么加。这已经能让你写出正确的、高效的 SQL 了。但真实的生产环境长这样吗你的项目只有一台数据库服务器几百万用户在它上面读写——如果这台机器挂了怎么办如果数据量涨到几十亿、几百亿单表根本装不下怎么办从这篇开始我们走出单机进入分布式世界。我们先从 MySQL 的三种日志说起——它们是理解主从复制、数据恢复、甚至上一篇事务实现原理的钥匙。一、日志系统数据的黑匣子MySQL 有三种核心日志redo log、binlog、undo log。它们分工不同但共同保障了数据的安全与一致性。1.1 redo log崩溃恢复的最后防线一句话概括redo log 是 InnoDB 特有的物理日志记录了数据页上做了什么修改用于保证持久性。它的工作原理基于一个经典设计——WALWrite-Ahead Logging预写日志正常流程没有 redo log 每次 UPDATE → 找到磁盘上的数据页 → 修改 → 刷回磁盘 → 每次修改都是随机磁盘 I/O极慢 WAL 流程 每次 UPDATE → 修改 Buffer Pool 中的内存数据 → 写入 redo log顺序追加写 → 后台线程定期将脏页刷回磁盘 → 顺序写比随机写快 100 倍以上为什么 redo log 是循环写的redo log 是固定大小的几个文件innodb_log_file_size写满后会从头覆盖。它只需要保证最近修改的数据不丢失之前的数据只要已刷到数据文件对应的 redo log 就可以被覆盖了。-- 查看 redo log 配置 SHOW VARIABLES LIKE innodb_log_file_size; -- 每个文件大小默认 48MB SHOW VARIABLES LIKE innodb_log_files_in_group; -- 文件数量默认 2 个 -- 总大小 48MB × 2 96MB对于高写入场景可能偏小1.2 binlog主从复制的信使一句话概括binlog 是Server 层的逻辑日志记录了所有引起数据变化的 SQL 语句或其行数据主要用于主从复制和按时间点恢复。redo log vs binlog 对比维度redo logbinlog所属层InnoDB 引擎层Server 层所有引擎共用记录内容物理日志第 10 页偏移 200 处写入 xxxx逻辑日志UPDATE user SET name张三 WHERE id1写入方式循环写固定大小满了覆盖追加写满了切新文件不覆盖用途崩溃恢复crash-safe主从复制、数据恢复可读性不可直接读可用 mysqlbinlog 工具查看binlog 的三种格式-- 查看当前格式 SHOW VARIABLES LIKE binlog_format;格式记录方式优点缺点STATEMENT记录 SQL 语句日志量小不确定函数NOW()、UUID()会导致主从不一致ROW推荐记录每行的具体变更精确主从绝对一致日志量大MIXED混合大多数用 STATEMENT折中仍是过渡方案在生产环境中推荐使用 ROW 格式——虽然日志量大一点但主从一致性是底线不能有任何妥协。数据恢复示例# 恢复到某个时间点 mysqlbinlog --start-datetime2025-06-01 10:00:00 \ --stop-datetime2025-06-01 11:00:00 \ mysql-bin.000001 | mysql -u root -p1.3 undo log回滚与 MVCC 的幕后英雄一句话概括undo log 记录了数据的反向操作用于事务回滚和 MVCC。INSERT → undo log 记录 DELETE回滚时删除这条数据即可 UPDATE → undo log 记录逆 UPDATE回滚时把值改回旧的 DELETE → undo log 记录 INSERT回滚时重新插入在 004 篇讲 MVCC 时我们提到过版本链——那个链就是通过 undo log 串起来的。每一行数据的DB_ROLL_PTR指向它在 undo log 中的上一个版本这样读事务就能回溯到适合自己的那个历史版本。三种日志各自为战还不够——当一个事务提交时redo log 和 binlog 必须同时成功才能保证数据的一致。如何做到这就是两阶段提交要解决的问题。二、两阶段提交redo log 和 binlog 的握手协议2.1 为什么需要两阶段提交假设没有协调机制场景 A先写 redo log后写 binlog → redo log 写完后宕机binlog 没写 → 主库通过 redo log 恢复了这笔数据 → 从库没收到 binlog没有这笔数据 → 主从不一致 场景 B先写 binlog后写 redo log → binlog 写完后宕机redo log 没写 → 主库崩溃恢复后没有这笔数据 → 从库通过 binlog 同步了这笔数据 → 主从不一致2.2 两阶段提交流程事务提交过程 1. 写入 redo log标记为 prepare 状态 2. 写入 binlog 3. 将 redo log 标记为 commit 状态 崩溃恢复时的判断 ┌───────────────────────────────────┬──────────┐ │ 发现情况 │ 处理方式 │ ├───────────────────────────────────┼──────────┤ │ redo log 是 prepare binlog 完整 │ 提交事务 │ │ redo log 是 prepare binlog 缺失 │ 回滚事务 │ │ redo log 是 commit │ 提交事务 │ └──────────────────────────────── ──┴──────────┘核心思想以 binlog 的完整性为准。binlog 写了说明这个事务应该被所有节点感知到binlog 没写说明这个事务还不应该出生。理解了日志和事务提交机制我们就具备了理解主从复制的基础。主从复制本质上就是 binlog 在机器间的传递。三、主从复制与读写分离3.1 主从复制的三个角色┌──────────┐ binlog 推送 ┌───────────┐ │ Master │ ─────────────────→ │ Slave │ │ (主库) │ │ (从库) │ │ 写操作 │ │ 读操作 │ └──────────┘ └───────────┘ ↑ ┌───────┴───────┐ │ IO 线程 │拉取 binlog → 写入 relay log │ SQL 线程 │执行 relay log重放 └───────────────┘三个步骤Master 将所有数据变更写入 binlog。Slave 的IO 线程连接到 Master拉取 binlog 并写入本地的 relay log中继日志。Slave 的SQL 线程读取 relay log逐条执行将 Master 上的变更重放到 Slave 上。3.2 主从延迟最常见的生产问题延迟原因Slave 机器配置低于 Master小马拉大车。Slave 承接了过多读请求CPU/IO 被打满。Master 上的大事务在 Slave 上重放需要同等的时间。Slave 只有单线程回放MySQL 5.6 之前。解决方案-- MySQL 5.7 开启基于组提交的并行复制 -- 在 Slave 上设置 STOP SLAVE SQL_THREAD; SET GLOBAL slave_parallel_type LOGICAL_CLOCK; SET GLOBAL slave_parallel_workers 4; -- 并行线程数 START SLAVE SQL_THREAD;对于延迟特别敏感的业务比如下单后立即查订单直接从Master读。对于可以接受几秒延迟的场景如列表展示走Slave。3.3 Java 中的读写分离实现// AbstractRoutingDataSource 是 Spring 提供的动态数据源路由基类 public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSource(String type) { CONTEXT_HOLDER.set(type); // master 或 slave } public static void clear() { CONTEXT_HOLDER.remove(); } Override protected Object determineCurrentLookupKey() { return CONTEXT_HOLDER.get(); // Spring 在获取连接时调用此方法 } } // 通过 AOP 自动切换标了 ReadOnly 的方法走从库 Aspect Component public class DataSourceAspect { Before(annotation(com.example.ReadOnly)) public void switchToSlave() { DynamicDataSource.setDataSource(slave); } After(annotation(com.example.ReadOnly)) public void clearDataSource() { DynamicDataSource.clear(); } } // 业务代码中使用 Service public class OrderService { ReadOnly public ListOrder listOrders(Long userId) { return orderMapper.selectByUserId(userId); // 自动走从库读 } // 不加注解默认走主库写 public void createOrder(Order order) { orderMapper.insert(order); } }3.4 几种常见的复制架构一主一从 → 最简单的结构适合小项目 一主多从 → 读多写少场景多从库分担读压力 双主互备 → 两台都是 Master高可用但需处理数据冲突 级联复制 → Master→Slave1→Slave2减轻 Master 的 IO 压力主从复制解决了读的扩展问题但如果数据量继续增长单表数据大到 BTree 层级过高、磁盘 IO 成为瓶颈时唯一的选择就是分库分表。四、分库分表当单表撑不住的时候4.1 什么时候该出手重要提醒分库分表是最后的手段不是第一个手段。在决定分库分表之前请先确认以下优化都已经做完了索引优化、SQL 优化、缓存Redis、读写分离。信号阈值参考单表数据量超过2000 万行后查询性能显著下降视硬件和索引而定磁盘空间单库接近磁盘容量上限连接数单库连接数超过 2000MySQL 单库建议上限写入瓶颈单库写入 QPS 达到磁盘瓶颈4.2 垂直拆分 vs 水平拆分垂直分库按业务 原来的大库 ──→ 用户库user 相关表 → 订单库order 相关表 → 商品库product 相关表 → 每个库的表结构不同拆的是业务维度 水平分表按数据行 user 表 5000W 行 → user_0id % 4 0 → user_1id % 4 1 → user_2id % 4 2 → user_3id % 4 3 → 每个表结构完全一样拆的是数据行维度4.3 三种分片策略// 1. 取模分片 —— 数据均匀扩容需迁移 int tableIndex userId % 4; // 0, 1, 2, 3 // 2. 范围分片 —— 扩容方便但可能有热点 if (userId 10_000_000) tableName user_0; else if (userId 20_000_000) tableName user_1; else tableName user_2; // 3. 一致性哈希 —— 扩容时迁移量最少 // 将分片节点和数据 key 都映射到 hash 环上 // 数据被顺时针方向第一个节点管理 // 扩容时只需迁移少部分数据4.4 分库分表中间件选择中间件类型选型建议ShardingSphere-JDBC客户端 SDKJava 生态首选Apache 顶级项目无独立部署ShardingSphere-Proxy代理层需要独立部署支持异构语言MyCat代理层老牌方案社区活跃度不如 ShardingSphere对于 Java 项目ShardingSphere-JDBC是最自然的选择——它是一个 jar 包直接集成在你的 Spring Boot 项目里不需要额外的服务器。4.5 ShardingSphere-JDBC 配置示例spring: shardingsphere: datasource: names: ds0, ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db_0 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db_1 rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: tbl-inline sharding-algorithms: db-inline: type: INLINE props: algorithm-expression: ds$-{user_id % 2} tbl-inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 2}4.6 分库分表后的五大新挑战分库分表不是一劳永逸的它会带来新的问题挑战解决方案分布式 ID雪花算法Snowflake不能再用自增主键跨分片查询避免跨分片 ORDER BY / GROUP BY或使用中间件聚合跨分片 JOIN字段冗余 应用层组装或换用 NoSQL分布式事务SeataAT/TCC 模式或接受最终一致性平滑扩容一致性哈希 双写 渐进式数据迁移分库分表是一把双刃剑。它能解决海量数据的存储和查询问题但也极大增加了系统复杂度。没有到真正的瓶颈之前不要为了架构好看而做分库分表。本篇回顾学完这一篇你应该能回答redo log 和 binlog 分别是什么什么用为什么需要两阶段提交怎么保证一致性主从复制是怎么实现的主从延迟怎么解决Java 项目中如何实现读写分离什么时候需要分库分表垂直拆分和水平拆分的区别ShardingSphere-JDBC 怎么配置分片策略有哪几种分库分表后会带来哪些新挑战学到这里你已经覆盖了 MySQL 从单机原理到分布式架构的完整知识链。下一篇是本系列的收官之作——我们将回到 Java 代码层面讲 MyBatis-Plus 的正确用法、慢 SQL 排查技巧、阿里巴巴开发规范以及 12 道高频面试题的精讲。【上一篇Java开发必知必会的MySQL核心知识点(三)-深入理解事务、锁与 MVCC】