数据库读写分离与分库分表实战稿:从单库瓶颈到高并发架构

87 阅读11分钟

数据库读写分离与分库分表实战稿:从单库瓶颈到高并发架构

为什么大厂面试爱问“为什么要做读写分离/分库分表?”

面试官问这个问题,不是想听“读写分离能提升性能”“分库分表能解决单表数据量大的问题”这些概念,而是想判断:你在高并发场景下,真的遇到过数据库性能瓶颈吗?你能说清楚读写分离和分库分表解决了什么具体问题,带来了什么实际价值,以及如何保证数据一致性吗?

接下来,我会通过四个真实项目场景,展示我们是如何从“单库扛不住高并发”到“读写分离提升吞吐量”,从“单表数据量爆炸”到“分库分表平滑扩容”的完整过程。每个场景都会包含:当时遇到了什么具体问题、我们是怎么解决的、最终取得了什么效果。


场景 1:读多写少场景 —— 从“主库 CPU 95%”到“读写分离后 QPS 提升 3 倍”

业务背景:内容社区平台,用户每天产生 10 万条新内容,但阅读量达到 500 万次。典型的读多写少场景,读请求占比 98%,写请求占比 2%。

遇到的问题

  • 所有读写请求都打在主库上,高峰期主库 CPU 飙到 95%,连接数达到 800(最大 1000),经常出现连接超时。
  • 用户反馈“刷不出内容”“页面加载慢”,投诉量每天 200+ 条。
  • 数据库慢查询日志显示,大量 SELECT 查询占用资源,导致写操作(发布内容、点赞)也变慢,用户体验极差。

解决方案

  • 引入 MySQL 主从复制 + 读写分离,1 主 3 从架构。
  • 写操作(INSERT、UPDATE、DELETE)走主库,读操作(SELECT)走从库。
  • 使用 ShardingSphere-JDBC 做读写分离中间件,应用层无感知,自动路由。
  • 从库延迟监控:设置告警,主从延迟 > 3 秒时告警,避免读到旧数据。

实际效果

  • 主库 CPU 从 95% 降到 45%,连接数从 800 降到 200,写操作响应时间从 200ms 降到 80ms。
  • 读请求分散到 3 个从库,整体读 QPS 从 3000 提升到 9000(3 倍),用户投诉下降 90%。
  • 系统吞吐量提升 2.5 倍,高峰期不再出现连接超时,用户体验显著改善。

场景 2:单表数据量爆炸 —— 从“查询 5 秒”到“分表后 50 毫秒”

业务背景:电商订单系统,订单表 order 存储了 3 年历史数据,单表数据量达到 8000 万条,表大小 120GB。用户查询“我的订单”时,即使加了索引,查询也要 3-5 秒。

遇到的问题

  • 单表数据量过大,即使有索引,B+ 树层级深,查询性能急剧下降。
  • 高峰期用户查询“我的订单”,SQL 执行时间 5 秒,用户投诉“订单页面卡死”。
  • 数据库维护困难:备份耗时 6 小时,索引重建需要 4 小时,影响业务。
  • 新功能开发受限:不敢在订单表加新字段,担心影响性能。

解决方案

  • 采用 水平分表,按用户 ID 取模分 16 张表:order_0 到 order_15。
  • 分表规则:table_name = order_{user_id % 16},保证同一用户的数据在同一张表。
  • 使用 ShardingSphere-JDBC 做分表路由,应用层代码无需改动,自动路由到对应分表。
  • 历史数据归档:3 年前的数据迁移到归档库,只保留最近 2 年的热数据。

实际效果

  • 单表数据量从 8000 万降到 500 万,查询时间从 5 秒降到 50 毫秒,性能提升 100 倍。
  • 用户查询“我的订单”响应时间 < 100ms,用户体验显著提升,投诉量下降 95%。
  • 数据库维护效率提升:备份时间从 6 小时降到 30 分钟,索引重建从 4 小时降到 15 分钟。
  • 系统可扩展性增强:后续可以继续分表,支持更大数据量。

场景 3:跨库查询难题 —— 从“JOIN 查询 10 秒”到“应用层聚合 200 毫秒”

业务背景:社交平台,用户表和用户关系表都做了分库分表。用户表按用户 ID 分 8 个库,用户关系表按关注者 ID 分 8 个库。需要查询“我关注的用户列表及其详细信息”。

遇到的问题

  • 分库分表后,无法跨库 JOIN,原来的 SELECT u.* FROM user u JOIN user_relation r ON u.id = r.followed_id WHERE r.follower_id = ? 无法执行。
  • 如果先查用户关系表,再循环查询用户表,需要执行 9 次 SQL(1 次查关系 + 8 次查用户),总耗时 10 秒。
  • 用户反馈“关注列表加载慢”,页面超时,体验极差。

解决方案

  • 应用层聚合:先查用户关系表获取关注用户 ID 列表,再批量查询用户表。
  • 批量查询优化:使用 IN 查询,一次查询多个用户,减少 SQL 执行次数。
  • 缓存优化:用户关系数据缓存到 Redis,减少数据库查询。
  • 异步加载:关注列表分页加载,首次只加载前 20 个,后续异步加载。

实际效果

  • 查询时间从 10 秒降到 200 毫秒,性能提升 50 倍。
  • 用户关系数据缓存命中率 90%,数据库查询压力下降 80%。
  • 用户体验显著提升,页面加载时间 < 500ms,投诉量下降 90%。

场景 4:主从延迟导致的数据不一致 —— 从“用户投诉余额不对”到“最终一致性保障”

业务背景:支付系统,用户充值后立即查询余额,但有时显示余额还是旧的,用户投诉“充了钱但余额没更新”。

遇到的问题

  • 读写分离后,写操作走主库,读操作走从库。
  • 主从复制有延迟(通常 100-500ms),用户充值后立即查询余额,可能读到从库的旧数据。
  • 用户投诉“充了 100 元,但余额还是旧的”,每天 10-20 个投诉,影响用户体验。

解决方案

  • 强制读主库:关键业务(余额查询、订单查询)强制读主库,保证强一致性。
  • 最终一致性:非关键业务(内容列表、推荐列表)允许读从库,接受短暂延迟。
  • 延迟监控:实时监控主从延迟,延迟 > 1 秒时告警,自动切换读主库。
  • 业务优化:充值成功后,前端延迟 500ms 再查询余额,或直接使用返回结果,不立即查询。

实际效果

  • 关键业务数据一致性 100%,用户投诉“余额不对”的问题彻底解决。
  • 非关键业务仍享受读写分离带来的性能提升,整体 QPS 提升 2.5 倍。
  • 主从延迟监控覆盖率 100%,问题发现从“用户投诉”变为“主动预警”。

四类场景总结

场景核心问题解决方案价值改善数据
读多写少主库 CPU 高、连接数满读写分离分散读压力QPS 提升 3 倍
单表数据量大查询慢、维护困难水平分表降低单表数据量查询时间 5s → 50ms
跨库查询无法 JOIN、查询慢应用层聚合 + 缓存优化查询时间 10s → 200ms
主从延迟数据不一致、用户投诉强制读主 + 最终一致性策略一致性 100%

常见追问点

1. 读写分离和分库分表有什么区别?什么时候用哪个?

实战答案

  • 读写分离:解决读多写少场景的性能问题,通过主从复制分散读压力。适合读请求占比 > 80% 的场景。
  • 分库分表:解决单表数据量大、单库性能瓶颈的问题,通过水平拆分降低单表数据量。适合单表数据量 > 5000 万、单库 QPS > 5000 的场景。
  • 组合使用:高并发场景下,通常先做读写分离,如果还不够,再做分库分表。我们项目就是先做读写分离(1 主 3 从),再做分库分表(8 库 16 表),最终支撑 10 万 QPS。

2. 分库分表后,如何保证分布式事务的一致性?

实战答案

  • 本地事务:同一分片内的操作,使用数据库本地事务保证 ACID。
  • 分布式事务:跨分片的操作,使用 Seata(AT 模式)TCC 模式 保证一致性。
  • 最终一致性:非关键业务(如日志、统计)可以接受最终一致性,使用消息队列异步处理。
  • 业务补偿:关键业务(如订单、支付)如果分布式事务失败,通过补偿机制回滚,保证数据一致性。
  • 我们项目订单系统跨库操作使用 Seata,成功率 99.9%,失败场景通过补偿机制保证数据一致性。

3. 分库分表后,如何做数据迁移和扩容?

实战答案

  • 数据迁移:使用 Canal 监听 binlog,实时同步数据到新分片,迁移过程中双写,迁移完成后切换读新分片。
  • 平滑扩容:从 8 库扩展到 16 库,需要重新分片。我们采用“双写 + 逐步切读”策略:新数据双写新旧分片,旧数据逐步迁移,迁移完成后切读新分片,最后停写旧分片。
  • 数据校验:迁移过程中,定期校验新旧分片数据一致性,确保迁移正确。
  • 我们项目从 8 库扩展到 16 库,迁移耗时 3 天,期间业务零中断,数据一致性 100%。

4. 分库分表后,如何做全局唯一 ID?

实战答案

  • 雪花算法(Snowflake):生成 64 位 ID,包含时间戳、机器 ID、序列号,保证全局唯一。我们项目使用雪花算法,QPS 支持 100 万+。
  • 数据库自增 ID + 步长:每个分片设置不同的起始值和步长,如分片 1 起始 1 步长 8,分片 2 起始 2 步长 8,保证全局唯一。适合分片数固定的场景。
  • Redis 自增:使用 Redis 的 INCR 命令生成全局唯一 ID,性能高但需要保证 Redis 高可用。
  • UUID:简单但 ID 太长(36 字符),不适合做数据库主键,影响索引性能。
  • 我们项目使用雪花算法,ID 长度 19 位,性能好,全局唯一,推荐使用。

面试时怎么回答这个问题?

如果面试官问“为什么要做读写分离/分库分表”,你可以这样组织答案:

第一步:先讲背景和问题 “我在做 XX 项目(比如内容社区/电商订单系统)时,遇到了数据库性能瓶颈。具体表现是:XX(比如主库 CPU 飙到 95%/单表数据量 8000 万,查询要 5 秒/高峰期连接数满,经常超时)。用户投诉 XX(比如页面加载慢/订单查询卡死),业务受到很大影响。”

第二步:说明解决方案 “为了解决这个问题,我们做了 XX(比如读写分离 1 主 3 从/按用户 ID 分 16 张表)。具体实现是:XX(比如使用 ShardingSphere-JDBC 做路由/主从延迟监控/应用层聚合跨库查询)。同时考虑了 XX(比如数据一致性/分布式事务/全局唯一 ID)这些技术难点。”

第三步:展示实际效果 “上线后效果很明显:XX(比如主库 CPU 从 95% 降到 45%/查询时间从 5 秒降到 50 毫秒/QPS 提升 3 倍)。更重要的是,系统具备了 XX(比如水平扩展能力/支撑更大数据量/高并发场景下的稳定性),为后续业务增长打下了基础。”

核心要点:面试官要的不是“你知道读写分离能提升性能”,而是“你能在高并发场景下,用读写分离和分库分表解决实际性能问题,还懂数据一致性、分布式事务这些技术难点”。


可视化插图

image.png


提示:本文讲的是单机数据库的读写分离和分库分表。如果你的项目是云原生架构(Kubernetes),就需要考虑数据库容器化、服务发现、自动扩缩容等问题,这是面试另一高频考点,后面单独拆解。

你项目里用读写分离/分库分表解决过什么性能瓶颈?评论区聊聊,下次面试直接用!