中文 EN

系统设计 Lab

YouTube Like Counter 不是 blind INCR:先把 user-video reaction 保存为 source of truth,再让单行 counter、sharded counter 或异步 projection 承担不同规模下的公开计数。

调节 reaction rows、写入和 count-read QPS、热门视频流量占比、client retry、counter shard 数量、cache hit 与 freshness 预算。观察 Toggle 为什么不幂等、单行 counter 何时变成 hot row,以及什么时候应该把公开 count 拆成可重放的异步聚合。

分步讲解

一步一步把它想清楚

常规演进场景

从左到右点击,就是预设的演示路径。每张卡片都会改变 workload 输入。

Workload

这些是输入,不是预设好的架构阶段。

推荐形态

当前架构路径
YouTube Like Counter 架构图 白板风格 Like Counter 架构:Reaction Store 保存 user-video truth;低流量时同步更新 counter,高流量时通过 Outbox、Event Log 和 Aggregator 产生 sharded aggregate,Count Cache 只服务公开读取。 Clients APIs Source of truth Count projection Serving Viewer set + read reaction Reaction API idempotent set Count API serve aggregate Reaction store (video, user) state Outbox / CDC capture commits Partitioned log order by user-video Counter aggregator versioned transition Counter shards video + user hash Count cache short-lived snapshot
Clients
Viewer 用 desired state 设置自己的 reaction,并读取公开 aggregate
APIs
Reaction API 按 user-video key 串行化 transition,返回用户自己的最新 reaction
Count API 读取 cache 或聚合 counter shards,并标注 count 的 as-of 时间
Source of truth
Reaction store 保存当前 reaction、version 和 updated_at;复合主键阻止重复状态
Outbox / CDC 把已提交的 reaction transition 可靠地交给异步 projection
Count projection
Partitioned log 同一个 user-video key 保持顺序,支持 retry、replay 和 backpressure
Counter aggregator 去重 event,并用 previous -> desired transition 更新 materialized count
Counter shards 分散热门视频写入;count read 对 shards 求和或读取 snapshot
Serving
Count cache 只缓存 derived aggregate;TTL、refresh 和 pipeline lag 一起决定 freshness

瓶颈

Retry correctness

最热 Counter shard 写入压力

Reaction truth 写入分区

Count backend read fan-out

公开 Count freshness

Reaction truth storage

为什么会变

    决策权衡

    Desired-state API

    User-video reaction truth

    Atomic state transition

    Per-video counter sharding

    Outbox + async projection

    Public count cache

    Per-key ordering / region ownership

    Reconciliation

    有出处支撑的规则

    这些是模型背后那些经得起时间考验的 system design 论断。而 slider 的具体阈值,则被刻意标注为教学用的假设。

    已验证的规则

    复合 Primary Key 可以强制一个 user-video pair 只有一行

    PostgreSQL 的 primary key 可以跨多列,并自动建立 unique B-tree index;(video_id, user_id) 因此既表达 identity,也在并发下保护唯一状态。

    PostgreSQL — Constraints
    已验证的规则

    INSERT ... ON CONFLICT 提供确定的 upsert 路径

    Reaction 写入可以让复合 unique key 作为 conflict target,再结合 transaction、version 或 row lock 完成 previous -> desired transition。

    PostgreSQL — INSERT
    已验证的规则

    Row-level lock 会阻塞同一 row 的并发 writer

    这能序列化同一个 reaction 或 counter row,也解释了为什么爆款视频的单 counter row 会成为 hot lock。

    PostgreSQL — Explicit Locking
    已验证的规则

    Redis INCR 是 O(1) 原子计数 primitive,但不知道业务 previous state

    INCR 能原子增加一个整数;它不会判断请求是否 retry、用户是否已经 LIKE,或 LIKE -> DISLIKE 应该应用哪两个 delta。

    Redis — INCR
    已验证的规则

    同一个 event key 放进同一 Kafka partition 可以保序

    把 (video_id, user_id) 作为 event key,可让 consumer 按写入顺序看到该用户对该视频的 transitions;系统不需要昂贵的全局顺序。

    Apache Kafka — Introduction
    已验证的规则

    Outbox 把业务状态与待发布 event 放进同一个 transaction

    Reaction row 与 outbox row 一起 commit;CDC / Relay 之后再发布 event,避免数据库成功而消息永久丢失的同步双写。

    Debezium — Outbox Event Router

    教学用假设

    • 所有 throughput、latency 与 storage 公式只用于展示架构拐点,不是 PostgreSQL、Redis、Kafka 或任何云服务的性能承诺。
    • 单 counter row 的舒适教学预算设为 1k writes/s;counter shard 设为 2.5k writes/s。真实边界必须用 row-lock wait、p95 和 skew 压测。
    • Reaction truth 每个 active row 按约 112 bytes 估算,包含 row、primary key 和基础存储放大;实际 schema、compression 与 replica 会改变结果。
    • 异步 projection 的基础 lag 设为 500ms;3 秒 count-cache TTL 会叠加到公开可见 freshness。
    • Desired-state API 让网络 retry 成为幂等 no-op;它仍需要底层 transaction/version 来处理并发 winner。
    • 同一个 user-video key 映射到稳定 counter shard 和 event partition;公开 count 可以最终一致,用户自己的 reaction 始终读取 source of truth。
    • Reconciliation 可以从 UserVideoReaction 重算 aggregate;任何不能重建的 count 都被错误地当成了 source of truth。