系统设计 12:短链服务:唯一创建与可撤销重定向
短链的读取看起来只是一次键值查询,但“随时可以撤销”会改变缓存和重定向选择。假如浏览器已经记住永久重定向,服务端再删除映射也不能让这个浏览器重新询问。编码长度决定碰撞空间,数据库约束决定是否覆盖,撤销契约决定读取路径,三者不能互相替代。 候选服务支持创建、访问、到期和撤销,产品名 TinyURL 只表示题型。它不复刻任何公司的内部架构。前面的 ID、缓存和数据模型基础在这里组合成一条能明确确认点的路径。 设计目标是有效短码跳转在服务端入口计时的五分钟窗口内 p99 小于 150 ms,已提交撤销后的新读取不得重定向。性能数字和严格撤销规则均是候选 SLO,不是本地实验测得的可用性或延迟;状态库不可用时本设计宁可拒绝跳转,也不以旧缓存返回目标。 创建者与访问者有不同的接口 POST /links 接受目标 URL、有效期和可选自定义别名;调用者身份来自认证上下文,幂等键放在请求头。成功返回 201 和 code;同一所有者、同一幂等键、同一请求正文再次提交返回原记录;同键不同正文返回冲突。自定义别名已存在返回 409,而不是覆盖已有用户的链接。 GET /{code...
系统设计 11:超时、重试与降级要共享预算
一个页面调用推荐服务,推荐服务再读画像。两层都配置“最多尝试三次”,画像在故障期间可能收到九次请求。页面最后仍然失败,资源消耗却扩大了九倍。限制故障扩散需要同时约束请求的存活时间、额外尝试次数和进入下游的并发量;三个开关不能各自按正常流量独立设置。 这是第 10 篇消息重投问题在同步调用链中的延伸。场景限定为商品详情中的可选推荐卡片,商品价格与交易状态不允许用旧推荐结果替代。所有数字是教学假设,实验使用虚拟时间,不把模型的毫秒数当作机器实测延迟。 先约定失败时还能交付什么 GET /products/{id}?recommendations=true 返回商品详情和推荐列表。商品不存在返回 404;推荐不可用时主体仍返回 200,响应带 recommendation_status=unavailable 和空列表。客户端必须能区分“计算结果为空”与“未计算”,否则分析系统会把服务故障误判成用户没有兴趣。 候选目标是从入口收到合法请求到发出响应,滚动五分钟窗口内 p99 不超过 300 ms。这里先给同步链路 250 ms,预留入口解析、序列化和传输 50 m...
系统设计 10:消息与异步处理——确认、重领和背压
把“分享创建后通知关注者”改成异步,创建接口会更少等待通知渠道,但不意味着“消息只会处理一次”。消费者拿到消息后、发送通知前后都可能失效:没确认的消息应如何重领,已发出的副作用能否重复,积压能涨到多少,都要写进设计。 接在ID、幂等键与唯一约束之后,本篇以分享创建的非关键通知作异步任务。权威分享和幂等映射仍在数据库提交;通知最终送达可以延迟,分享授权与撤销不可依赖消息的消费进度。本文不设计邮件服务的真实送达承诺、跨地域队列或支付类强一致事务。 写清哪些结果可以晚到 POST /shares 用幂等键确认分享只创建一次;成功响应只承诺数据库中的分享和请求映射已提交,不承诺关注者收到通知。后续事件可建模为 ShareCreated(event_id, share_id, owner_id, version),通知处理按 (event_id, recipient) 记录处理结果和重试次数;正文、权限信息应由权威库按需读取,不能让一条旧消息重新发布已撤销内容。若以后业务要求“通知不可丢”,必须验证数据库提交与事件发布间的崩溃窗口,引入可核对的 Outbox 或等价事务边界;本篇的 Re...
系统设计 09:ID 与索引——唯一、顺序和不可猜不是同一件事
“用自增 ID 就够了”可能只是回答了如何定位一行。它不自动保证公开链接难以枚举,也不保证事务按 ID 顺序完成。系统设计里应把内部主键、外部可见编码、幂等键和列表排序键分开;每个键的唯一范围和失败处理都不同。 沿用按所有者迁移的分享服务:创建分享、用链接取分享、按所有者与创建时间翻列表;撤销后禁止公开返回目标。本文只讨论单个权威 PostgreSQL 写入域里的 ID 与索引;不声称单序列已经解决跨分片、跨地域发号或安全授权。外部别名随机性与密码学熵是待另行验证的设计要求。 先写清四把不同的钥匙 shares(id BIGINT PRIMARY KEY, public_code TEXT UNIQUE NOT NULL, owner_id, created_at, state, target):内部 id 定位一行;public_code 放在 GET /s/{public_code} 链接中,服务端读到后仍检查权限与撤销;创建接口 POST /shares 接收 (owner_id, idempotency_key),该组合另有唯一约束并与 share_id 在同一事务提交...
系统设计 08:分片与热点——数据归属变化时不能只搬快照
分片把数据分散到多台机器,不会自动把“同一个热门所有者”的请求分散掉。更难的是迁移:复制完旧分片的快照,若此时旧分片还在接更新,新分片可能永远缺少那次写入。先决定数据归属、读写路径和切换窗口,才有资格讨论加分片。 接续复制与故障切换的分享元数据场景。本篇只把 owner_id 作为候选数据归属键:POST /shares 和 DELETE /shares/{id} 找到唯一写入分片,GET /users/{owner_id}/shares?after=... 在单个所有者的有序列表上翻页。GET /shares/{id} 还需有从分享 ID 找归属的稳定映射,不能只拿一个随机 ID 猜所有者;目录映射或把可校验路由信息编码入 ID 都会新增维护成本。排除跨地域、公开搜索和双主同时写入。 量化热点,别只数机器 独立教学假设:每天 800 万次列表读取、40 万次写,读写比 20:1;峰值读 1000 次/秒、写 50 次/秒,均为日均的 10.8 倍。按 1 KiB/次读取,峰值有效载荷约 0.977 MiB/s ≈ 8...
系统设计 07:数据库、复制与一致性——提交成功意味着谁已收到
“主库已返回成功”和“备库此刻一定读得到”不是同一个承诺。加一个读副本前,要知道写请求究竟等谁确认,读请求允许多旧,主库失效后由谁确认它不会继续接写。否则把部分请求改路由到副本,可能让刚创建的分享消失在下一次读取里。 承接缓存与撤销边界:创建分享要把分享和幂等键映射原子写入,撤销确认后不能再公开返回目标。本文讨论单地域里一个权威 PostgreSQL 主库和一个只读热备库;不设计跨地域多主、自动选主或没有栅栏的“快速切换”。非关键的统计列表可以容忍短暂滞后,受保护的目标读取和撤销状态则不能偷偷改为读异步备库。 把读写语义写在接口上 POST /shares 接收幂等键,同一个事务中写 shares 与 requests;DELETE /shares/{id} 写状态并在权威库提交后才确认。GET /shares/{id} 与撤销后仍可访问的所有者列表在当前强撤销契约下都先读主库或由等价的强鉴权路径承接;例如只做非权限用途的公开统计 GET /stats?share_id=... 才可读允许滞后的副本。无视这个划分把所有 GET 都改到副本,优化的是吞吐数字,破坏的是可见性约束。...
系统设计 06:缓存与 CDN——命中、失效、热点与回源
把热门内容放进 CDN 能减少回源,但“缓存命中率很高”没有回答分享撤销之后谁还能看见旧内容。失效不只是一条删除命令:要先划清哪些读允许旧值、哪些读必须在确认撤销后立刻拒绝,再决定缓存键、TTL 和回源容量。 沿用网关与连接边界及前几篇的分享场景:GET /shares/{id} 读取目标、所有者 DELETE /shares/{id} 撤销、图片预览按版本读取。本篇只讨论分享元数据和已公开的不可变预览,不讨论私有对象的直连 CDN 方案、DRM 或 CDN 产品计费。此前约定“撤销确认后不能再公开返回目标”,这一要求高于缓存命中率。 先把可缓存和不能缓存的访问拆开 读路径分两类。分享目标在源站的权威状态为 ready/revoked,公开读取必须检查撤销;若一个共享缓存可直接返回旧目标,就算源站状态已经改变,访问者仍可能看见旧响应。受保护响应应避免进入共享缓存;对遵循 HTTP 规范的缓存,响应侧 Cache-Control: no-store 表示不应存储该响应(RFC 9111:no-store)。这不阻止读者已复制的内容,也不等于 CDN 供应商对撤销时限作了承诺。 对...
系统设计 05:负载均衡与网关——请求分配不等于连接有余量
两台后端均分请求,不能据此推断每台仍可接新连接。一次请求可以很快结束,也可以占住连接数秒;健康的实例也可能已用完连接名额。路由必须同时回答“往哪台送”和“这台现在能否接”,失败时还要说清拒绝与重试责任。 沿用先测瓶颈再扩容的分享元数据服务。本文只处理入口到无状态应用实例这一跳:POST /shares 写入、GET /shares/{id} 读取、所有者列表读取;权限和撤销仍由应用与权威库共同维护,不因为网关能缓存或改写请求而转移到入口。排除跨地域、会话黏性、TLS 证书生命周期和详细服务发现协议。 定义目标和连接负载 独立练习假设:每天 110 万次读和 10 万次写,读写比 11:1;峰值 360 次读/秒、40 次写/秒,共 400 次/秒(读、写各为日均的约 28.28 与 34.56 倍)。读响应平均有效载荷按 20 KiB,峰值约 360 × 20 KiB = 7.03 MiB/s ≈ 58.98 Mbit/s,不含 TLS、重试、连接升级和网关日志。写元数据平均 1 KiB,写入峰值约 0.039 MiB/s。 假设峰值请求中 80% 的连接平均占用 0.2 秒,...
系统设计 04:从单机到分布式——先测瓶颈,再加节点
“流量涨了,就把应用从一台扩到四台”没有回答瓶颈在哪。如果应用实例尚有余量,但每个请求都在等同一处数据库写入,加应用机器只会让更多请求同时排队。扩容先要有一份能被负载实验推翻的容量假设。 沿用访问模式与游标分页里的分享元数据服务:创建、按 ID 读取、按所有者列表读取。但以下流量是新的教学输入,不是 03 的实测流量,更不是某家服务的生产配置。 单机版本先说清楚承诺 服务包含 HTTP 应用和一个权威数据库;POST /shares 写分享与幂等键映射,GET /shares/{id} 查主键,GET /users/{owner}/shares?after=... 按 (owner_id,created_at,id) 读有序索引。应用实例可重启,不在实例内保存权限、游标或写入归属;授权检查仍发生在每次读之前。已确认的撤销请求不能再返回公开内容;这是数据不变量,不可通过增加只读节点放松。本文排除对象上传、跨地域容灾和多主写入,暂不把读副本当成默认选项。 目标先限定为合法读取服务端 p99 小于 200 ms、十秒峰值窗口的请求拒绝率小于 0.1%;写入确认还需要单独记录成功率与延...
系统设计 03:API 与数据模型——访问模式如何决定表、键和分页
先画一张“万能分享表”再补索引,常会漏掉实际最昂贵的访问:按用户和时间翻页。键、索引和游标应从查询与更新方式反推;如果列表在持续插入期间出现重复,补一台数据库机器也不会修正页面语义。 沿用前两篇的分享元数据场景:需求与撤销约束决定谁能看;容量估算给出怎么算峰值。本篇只回答“用户按时间查看自己的分享列表”,实验使用真实 SQLite,而不是用 Python 排序冒充数据库查询。 API 的三种访问,写入只有一个归属 教学范围有三条请求:POST /shares 创建分享,GET /shares/{share_id} 按标识取单条,GET /users/{owner_id}/shares?limit=20&after=... 按创建时间倒序取列表。已登录的创建者可以 DELETE /shares/{share_id} 撤销;只有本人可以列出自己的分享,列表允许展示已撤销状态但不展示给读者。排除按目标 URL 搜索、全站热度排名及跨用户浏览。对外读取仍按第 01 篇的强撤销契约判断。 读者用链接查单条,创建者按时间浏览列表,两个路径的定位方式不同。候选表 shares(sha...










