CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
系统设计入门/课时/数据架构
RawBlame
课时目录12 节

系统设计入门

  1. 01系统设计是什么
  2. 02性能与扩展基础
  3. 03一致性与 CAP
  4. 04负载均衡
  5. 05缓存模式
  6. 06异步与消息
  7. 07数据架构
  8. 08微服务与网关
  9. 09云设计模式
  10. 10可靠性模式
  11. 11性能反模式
  12. 12综合练习

数据架构

分片、复制、CQRS、Event Sourcing、索引表。

35 分钟

数据架构

这一课你将能做到

  1. 为提交记录表选择分片键并推导路由,说清为什么用 userId 而不是 problemId
  2. 区分复制、读写分离、分片三种手段各自解决的问题与触发时机
  3. 为排行榜实现「写规范化表 + 读预聚合」的 CQRS 简化版,并写出投影逻辑

为什么要学

流量靠加机器能解决,数据量靠加机器解决不了——单表 5 亿行、单库磁盘 2TB 时,再贵的机器也只是延后崩溃。数据架构回答的是:数据怎么拆、怎么放、怎么读,它是系统设计题里区分「背题」和「做过」的部分。

面试高频:「什么时候分库分表?」「分片键怎么选?」「分片后跨片查询怎么办?」「CQRS 是什么、什么时候用?」——本课全部用本站的提交表和排行榜落地。

人话:复制是多印几本一样的书分给不同人读(读扩展);分片是把一套百科全书按卷拆开放不同书架(写扩展+容量);CQRS 是写的时候按规矩记(规范化),读的时候提前把摘要做好(反规范化)——记账和看账用的是两套视图。

先建立图像

三种手段解决的问题完全不同:

手段解决什么触发信号本站映射
主从复制读扩展、灾备读 QPS 超主库承受题目浏览走从库
读写分离读流量分流读远大于写(9:1)列表/详情读从库
分片 Sharding写扩展、单机容量单表过大、写 QPS 超限提交表 10 亿行

先估算触发点:提交 50 万条/天,单表假设 5000 万行开始明显变慢 → 约 100 天就触顶。索引膨胀(每条 2KB + 索引 ~500B)、备份窗口拉长、DDL 锁表风险全部随之而来。分片不是「以后再说」,对本站是年度内必做。

分片键的取舍对比(面试核心):

按 user_id 分片(hash(userId) % 4):
  ✅ 「我的提交记录」一次路由命中 —— 最高频查询
  ✅ 写压力均匀(活跃用户分布均匀)
  ❌ 「某题目的全部提交」要扫 4 个分片 —— 但该需求是运营场景,低频可接受

按 problem_id 分片:
  ✅ 题目维度查询快
  ❌ 热门题目所在分片写压力倾斜(明星 key)
  ❌ C 端最高频的「我的记录」要扫全分片 —— 反了

结论一句话:分片键选「最高频查询的过滤条件 + 高基数 + 写入均匀」。高频 > 一切。

跟着做

步骤 1:提交表分片的 ShardingSphere-JDBC 配置

rules:
  sharding:
    tables:
      submission:
        actual-data-nodes: ds_${0..3}.submission     # 4 库 × 1 表
        database-strategy:
          standard:
            sharding-column: user_id
            sharding-algorithm-name: user-mod
    sharding-algorithms:
      user-mod:
        type: MOD
        props:
          sharding-count: 4

应用代码完全无感知——submissionMapper.selectByUser(userId) 自动路由到正确分片,这是分片中间件最大的价值:路由逻辑下沉,业务只管写 SQL。

步骤 2:跨片查询的两条出路

「某题目最近提交列表」要跨 4 个分片,直接聚合代价高:

出路 A 广播聚合:SQL 发到 4 片,各自 LIMIT 20,内存归并排序取 Top20
        —— 中间件支持,适合低频运营查询
出路 B 索引表(异构读模型):写入时同步维护 submission_feed 表
        (按 problem_id 组织,只存 feed 需要的字段,单表可再分片)
        —— 空间换时间,适合高频公开列表页

本站题目页的「最新提交」标签访问量大,选 B;运营后台统计选 A。跨片不是禁区,是把高频跨片需求提前物化。

步骤 3:排行榜的 CQRS 简化版

写模型(规范化,保事务正确):

-- 提交判定完成时写入(唯一事实来源)
INSERT INTO judge_result(submission_id, user_id, problem_id, passed, score, ...)
VALUES (...);

读模型(预聚合,优化展示):

// Kafka 消费 judge-result 事件,更新投影(第 6 课的消费者顺带做)
@KafkaListener(topics = "judge-result")
public void project(JudgeResultEvent e) {
    if (e.isPassed()) {
        // ZSet 分数累加 —— 读模型只需支持「TopN」和「我的排名」两种查询
        redis.opsForZSet().incrementScore("rank:weekly", e.getUserId(), e.getScore());
    }
    // 题目通过率:INCR pass:{problemId} —— 原子计数,避免每次 COUNT(*)
    redis.opsForValue().increment("pass:" + e.getProblemId());
}

一致性保障(衔接第 3 课):投影是异步的,排行榜有秒级延迟——可接受;每天凌晨对账任务比对 SUM(score) 与 ZSet 总分,漂移则重建。

步骤 4:冷热分离与归档

分片之外的第二把刀。提交记录的访问规律:90% 的查询集中在最近 30 天(用户看自己的近期记录),历史记录只是偶尔翻。

-- 热表:近 90 天,分片,配齐索引
-- 冷表:历史归档,按月分表,只保留主键+摘要索引
INSERT INTO submission_archive_202510 SELECT ... FROM submission
   WHERE create_time < '2025-07-01' AND create_time < '2025-08-01';
DELETE FROM submission WHERE create_time < '2025-07-01' LIMIT 5000;  -- 分批删

效果:热表数据量砍掉 60%+,索引更瘦、buffer pool 命中率更高——比多分一片便宜得多,且常常被忘记做。

讲清楚要点

  1. 复制、读写分离、分片是三件事:复制解决可用性和读冗余,读写分离是把复制用于流量,分片解决写与容量。它们常组合出现(分片内主从),但解决的问题互不替代。
  2. 分片键的错误代价极高:选错后所有高频查询跨片,等于白分。选片三问:最高频查询带什么条件?基数够不够高?写入是否均匀?判题平台的答案都是 userId。
  3. 分布式 ID 必须提前设计:分库后自增主键会冲突。方案:雪花算法(Snowflake,趋势递增、机器号+时间戳)或号段模式(DB 批量取号)。提交 id 全局唯一是幂等和追踪的前提。
  4. CQRS = 写模型保正确 + 读模型保速度,用事件(Kafka)连接两者。它不是新数据库,是「承认读写形态不同、分别优化」的意识,本站排行榜就是最小实现。
  5. 先冷热分离,再考虑加分片:归档让表变小,很多「分片需求」其实消失。成本顺序:索引优化 < 冷热分离 < 读写分离 < 分片。

你可能会懵的地方

  • 分库和分表什么关系? 分表解决单表索引树过深、锁竞争;分库解决单机磁盘/CPU/连接数上限。只分表不分库,数据还在一台机器上,容量天花板没动。本站先分库(4 实例),每库内 1 表即可,将来再细分表。

  • 分片之后还能 JOIN 吗? 同片内(同 userId 的 user 和 submission)可以;跨片 JOIN 中间件支持有限且性能差。正确姿势是广播聚合(低频)或冗余字段/索引表(高频)——「反规范化」是分片世界的常态。

  • 数据倾斜(某分片特别大)怎么办? 先确认倾斜原因:分片键不均匀(用 province 分片必然倾斜)→ 换键迁移;自然明星用户(一个工作室灌几千万提交)→ 单独给它切「热点分片」或限流。扩容翻倍(4→8 片)配合一致性哈希式迁移可控制在 1/4 数据量搬迁。

  • Event Sourcing 是什么,本站要用吗? 存「事件流」而非「当前状态」:不存「分数=350」,而存「AC(+100)、AC(+80)、TLE(0)」全序列。好处是可审计、可重放、天然知道历史;代价是查询要投影。本站只在「学习轨迹回放」子域有价值,全站 ES 是自我伤害。

  • 什么时候直接换分布式数据库(TiDB 之类)? 当分片带来的跨片事务、迁移、运维成本 > 换库成本时。判据:频繁需要跨片强一致事务、团队无力维护 ShardingSphere。传统分库分表成熟稳定,TiDB 换来弹性但要接受不同性能模型——先算账再动。

错法对照

错法问题正解
按 problem_id 分片提交表C 端高频「我的提交」全跨片分片键跟最高频查询走(userId)
用自增主键 + 分库各库 id 冲突,幂等失效雪花 ID / 号段模式
一上来分 64 片「一步到位」运维复杂度爆炸,数据量根本用不满从 4 片起步,留平滑扩容方案
每次读排行榜实时 COUNT/ORDER BY大表聚合拖死主库ZSet 预聚合 + 异步投影 + 对账
冷数据一直留在热表索引膨胀,buffer pool 命中率下降90 天归档,分批迁移删除

卡点急救

  1. 分片后部分接口变慢:先查是否意外跨片——SQL 里没带分片键会广播全部分片(看中间件 SQL 日志);补上路由条件,或为该查询建索引表。
  2. 分片扩容要迁移历史数据:双写方案——新分片规则生效后旧数据异步迁移(binlog 订阅或扫描任务),读先切「读新读旧兜底」,确认干净后下线旧路由。禁止停机全量搬迁。
  3. 排行榜数据与 MySQL 对不上:查投影消费者的重复/丢失(衔接第 6 课幂等),再跑对账任务重建 ZSet(DEL rank:weekly 后从 judge_result 全量重算,几十万行秒级完成——读模型可重建正是 CQRS 的底气)。

面试常问

  • 什么时候需要分库分表?怎么选分片键? 触发:单表行数使索引深度/DDL/备份成本失控(经验值千万级起步观察)、写 QPS 超单库极限、磁盘容量逼近。选片键三标准:最高频查询的过滤条件、高基数、写入均匀,并以本站提交表为例讲 userId vs problemId 的取舍。

  • 分片后「全局排行榜」「跨片分页」怎么做? 两条路:禁止跨片实时聚合,改为读模型预聚合(CQRS,ZSet/ES);确需实时则各片取 TopN 内存归并(并发 4 路 LIMIT 20 → 归并 80 条取 20),并说明翻页深的代价。落点落在「高频查询必须物化」。

  • CQRS 是什么?代价是什么? 命令(写)与查询(读)用不同模型:写侧规范化保事务,读侧反规范化/预聚合保速度,事件驱动同步,读模型可随时重建。代价:最终一致(产品要接受延迟)、投影链路多一层要监控对账。举排行榜实例收尾。

自测三问

  1. 提交表分片为什么选 userId?如果运营强需求是「题目维度统计」怎么办?
  2. 复制、读写分离、分片分别解决什么问题?正确的实施顺序是什么?
  3. 排行榜的 CQRS 实现里,读模型坏了怎么恢复?为什么说「可重建」是读模型的关键性质?

小结一句话

数据架构在复制、读写分离、分片之间按「索引优化 < 冷热分离 < 读写分离 < 分片」的代价顺序演进;CQRS 的本质是把高频跨形态查询提前物化成可重建的读模型。

延伸

  • ShardingSphere-JDBC 分片文档:本课配置与广播路由说明
  • 《Designing Data-Intensive Applications》第 6 章(分区):分片与再平衡的权威论述
  • CQRS Pattern(Microsoft Azure 架构模式):模式定义与优劣分析
  • 下一课:微服务与网关——单体什么时候该拆、按什么拆、Gateway 与 BFF 怎么分工。
加载本节练习…
← 上一课异步与消息下一课 →微服务与网关