cswork / 系统设计

第 42 章 / 共 57 章 · 约 3 分钟

设计模式:扩展写

本页目录
  1. 1. 先确认瓶颈
  2. 2. 方案
  3. 3. 怎么选
  4. 4. 常见追问
  5. 5. 本站用到这个模式的案例
  6. 章节测验

读可以靠缓存和副本层层挡住,写却不行:每一次写都必须真正落到存储上,而且通常只有一个地方能接收它(主库、分区的 leader)。位置上报、聊天消息、点击日志、计数器这类写多的系统,扩展的办法是把写分散开、攒起来、或者换一种更擅长写的存储。

1. 先确认瓶颈

  • 写入量到底有多大?每秒几千次写,一台调优过的 PostgreSQL 就能承受,不要过早分片。
  • 瓶颈在哪:磁盘 fsync、锁竞争(热点行)、索引维护、还是网络?不同的瓶颈对应不同的解法。

2. 方案

2.1 垂直扩展和减少每次写的成本

  • 更快的磁盘、更大的机器;去掉不必要的索引(每个索引都让写入多一份工作);批量提交。
  • 这一步便宜,但有上限。

2.2 分片(水平分区)

  • 按分片键把数据分到多台机器,每台只承担一部分写入。写吞吐随分片数近似线性增长。
  • 分片键是关键:要让写入均匀分布(避免所有新数据都落在同一个分片),同时让最常见的查询只命中一个分片。
    • 按时间范围分片:所有新写入集中在最新的分片上,形成热点。
    • 按 user_id、conversation_id 哈希:分布均匀,同一实体的数据在一起。
  • 用一致性哈希或者“很多逻辑分片映射到少量物理节点”,以后加机器只需迁移一部分数据。
  • 代价:跨分片查询和事务变难,重新分片是大工程。

2.3 换成擅长写的存储

  • LSM 树存储(Cassandra、DynamoDB、RocksDB)把随机写变成顺序写,单机写吞吐高,并且天生按分区键分布。
  • 时序数据库适合指标和监控数据;追加写的日志(Kafka)适合先把事件记下来。

2.4 用队列削峰

  • 突发的写入先进入队列或 Kafka,后端按自己能承受的速度消费写入。用户拿到的是“已接收”,而不是“已写入”。
  • 适合可以异步的写(日志、分析事件、通知)。代价是延迟和最终一致,以及需要处理积压。

2.5 批量和预聚合

  • 批量写:攒够 N 条或 T 毫秒再写一次,大幅减少每条写入的开销。
  • 预聚合:点赞数、浏览量不需要每次 +1 都写库。先在内存、Redis 或流处理里按 key 聚合,每隔几秒写一次总数。一百万次 +1 可以变成一次 +1000000。
  • 只保留最新值:位置上报这类数据,很多时候只需要最新位置,旧值覆盖即可;完整轨迹另外以追加的方式归档。

2.6 打散热点

  • 热点 key(一个超大群聊、一个爆款商品的计数)会压垮单个分片。办法:把一个逻辑计数拆成 N 个子计数(likes#0..likes#N-1),写时随机选一个,读时求和;或者给 key 加后缀分散,读时合并。

3. 怎么选

情况推荐
每秒几千次写、需要事务单个关系数据库,优化索引和批量提交
写入持续增长、按实体访问按实体 id 哈希分片
海量追加写、按 key 加时间读宽列存储(LSM)
突发流量、可以异步队列削峰
高频计数预聚合、分片计数器
高频位置 / 状态更新只存最新值,轨迹另外追加归档

4. 常见追问

  • 按 created_at 分片有什么问题? 所有写入都打到最新的分片上,其他分片闲着。
  • 分片之后怎么做跨分片查询? 尽量让查询带上分片键;必须跨分片的查询走异步构建的读模型(搜索索引、数仓)。
  • 队列积压越来越多怎么办? 扩消费者(受分区数限制)、批量写入下游、降级非关键写入,并对上游限流。
  • 预聚合会不会丢数据? 内存里的增量在崩溃时会丢;可以先写 Kafka 再聚合,或者接受计数类指标的少量误差。

5. 本站用到这个模式的案例

章节测验

5 道选择题
  1. 一个日志表按 created_at 的时间范围分片,结果写入性能没有随分片数提升。最可能的原因是?
  2. 视频的播放量计数每秒有几十万次 +1。哪种写法最合理?
  3. 写入量每秒只有 2000 次,需要事务和唯一约束。团队提议马上分片。你的建议是?
  4. 网约车每个司机每 4 秒上报一次位置,几百万司机在线。派单只关心最新位置。怎样处理这些写入?
  5. 一个超大群聊的“消息总数”计数器成了热点,压垮了它所在的分片。怎么打散?

章节讨论

正在加载讨论…