系统设计 02:容量估算——QPS、存储、带宽、并发与成本
容量估算不是猜一台服务器能承受多少 QPS。先把业务动作拆成次数、字节、持续时间和保留期,再看哪一项随假设变化最快。算完只能得到待测的负载,不会凭空得到组件容量。 以图片分享的元数据、原图和预览图为教学场景。本篇重点是估算如何改变“上传流量是否穿过应用”这一设计决定,不讨论完整图片社交产品。此前的需求澄清讨论的是分享撤销契约;本篇使用独立的图片负载,不能把两个场景的指标拼成真实业务画像。 先把单位写在每个假设旁边 以下均为教学假设:5 万上传者每天各上传 4 张,得 20 万张/日;每张图片在 90 天保留期内累计产生平均 20 次预览读取,每次读取 256 KiB。按上传速率和各年龄图片的读取分布稳定、系统已进入稳态估算,日均预览读取量为 200000 × 20 = 4000000,读写比 20:1;这不是每张存量图片每天读取 20 次,后者在保留 90 天的稳态下将产生 200000 × 90 × 20 = 360000000 次读取/日。全天峰值按日均速率的 8 倍计算。每张原图 4 MiB,另外生成 2 张各 256 KiB 的预览;原图及预览保留 90 天、两份。每张...
系统设计 01:需求澄清——功能、SLO、数据不变量与范围
“做一个分享服务”不能直接变成组件清单。它还没有说明:分享给谁,链接可以撤销吗,读者缓存了旧内容怎么办,创建响应丢了还能不能重试。这些问题的答案会直接改变读写路径。 延续第 00 篇:系统设计题究竟交付什么的教学场景,本篇只处理一件事:把含混需求转成可验证的契约,不借课程题解推测真实产品架构。 先问会改变架构的问题 产品希望用户创建一条分享,接收方打开链接,创建者以后可以撤销。选择以下教学假设,而不是默许它们:只有登录的创建者能撤销;现在分享目标是 URL 元数据,非文件正文;读者无需登录,但链接转发即可能被别人读取;撤销确认之后才开始的新读取,不得返回目标。撤销确认之前已发出响应的读取,不承诺能在网络中撤回。 排除公开搜索、按创建者列举、链接到期、屏蔽恶意 URL 和隐私内容的接收者鉴权。这些是后续产品决策,不是“先不画出来就自动解决”。若用户问“这条链接必须仅让指定同事读取吗”,应先改变接口和访问控制模型,而不是把随机分享 ID 当成授权。若用户问“网页已被别人保存怎么办”,服务端撤销不能抹去接收方保存的副本;必须缩小承诺。 问题 本题确认 可观察验收 创建超...
系统设计 00:系统设计题究竟交付什么
系统设计题的交付物不是一张画满组件的架构图,而是一份能被反驳的决策记录:要求是什么,数字如何得到,写入何时算成功,故障时哪些承诺还能成立。图只负责让这份记录里的数据路径清楚可见。 以“用户保存一个链接,取得分享地址,后来可以撤销”为贯穿练习。它不是任何产品的内部实现。这里先确定交付物,后续章节再分别研究需求、规模和组件。 先定下要交付的答案 教学场景只支持创建、查看、撤销;用户登录已有,接收方凭分享地址访问。不做推荐、搜索、离线同步和全站爬取。不可违反的约束是:创建接口超时后重试不得产生两份不同的分享;撤销确认后,新的读取不能再返回内容。地址可能被转发,不能把“知道链接”称作身份认证;如果内容有隐私要求,必须增加接收者鉴权,这会改变读路径。 用于练习的服务目标是:按 30 天滚动窗口,成功读取占有效读取请求的比例不低于 99.9%,读取 p99 小于 250 ms,创建 p99 小于 500 ms。可用性目标需要说明是否剔除非法请求;这里按鉴权通过、格式正确且尚未撤销的请求定义分母。p99 是成功请求样本分布的分位值,不是平均值;即使写下目标,也不意味着设计已达到目标。撤销一致...
设计模式 E08:遗留代码的小步重构
没有测试的遗留代码,第一步不是套模式,而是把现有行为钉住。特征测试不代表旧行为都正确,它只说明这些输出不能在重构时无声改变。接缝建立以后,再把兼容行为、已知缺陷和确认后的变化分开。 特征测试先冻结可观察行为 labs/E08 的 LegacyCheckout.checkout 解析 sku:cents:state。开放订单返回九折收据,被封禁返回 DENIED,解析失败返回 ERROR。特征测试覆盖成功、封禁、未知状态、负金额、最大 int 金额、空 SKU、字段缺失、金额解析失败和溢出解析失败。测试还记录一个已知缺陷:空 SKU :100:open 返回 OK::90。记录它的目的,是防止重构时顺手改变外部可见行为。 123456assertEquals("OK:paper:90", legacy.checkout("paper:100:open"));assertEquals("DENIED", legacy.checkout("paper:100:blocked"));assertEquals(...
设计模式 E07:真实库里能看到哪些模式
看到 ArrayList.Itr、BufferedInputStream、PropertyChangeSupport 这类 JDK 类,很容易把它们直接贴上 Iterator、Decorator、Observer 的标签。标签本身并不是证据。真实库里的类经常同时承担兼容性、性能、序列化、线程安全和历史 API 约束;只看类名、继承关系或“持有另一个对象”,会把结构相似误读成模式意图。 本章固定到 OpenJDK jdk-21+35 对应提交 890adb6410dab4606a4f26a942aed02fb2f55387。本机运行环境是 Homebrew OpenJDK 21.0.10;它只用于验证 Java 21 标准库行为和 src.zip 路径存在,不声明本机发行包源码与上游 GA 提交逐字一致。 Iterator 要看调用者拿到什么遍历协议 OpenJDK 的 ArrayList.iterator() 在固定提交里返回内部 Itr;Itr 持有 cursor、lastRet 和 expectedModCount,next() 先检查结构性修改,再按当前位置返回元素。源码路...
设计模式 E06:从 DIP 到 Ports and Adapters
DIP 说高层策略不应依赖低层细节。Ports and Adapters 把这个方向放到系统边界上:应用层面向端口,数据库、消息、支付、外部服务都是适配器。DDD 战术设计再补一个约束:领域不变量不应丢给适配器或应用脚本去补。 Repository 是端口,不是 GoF 名称复用 labs/E06 的 OrderRepository 只有 save 和 find,InMemoryOrderRepository 是内存适配器。PlaceOrder 应用服务依赖接口,测试把订单 o-1 放进去,再从 repository 找回同一个对象。应用层不知道底层是 Map,也不需要知道未来会不会换成数据库。 12Scenario.PlaceOrder useCase = new Scenario.PlaceOrder( repository, new Scenario.OrderFactory(), cents -> cents <= 1_000); Repository 不是 GoF 23 种模式之一。Factory 在这里也不是 Abstract Factory...
设计模式 E05:依赖注入容器、动态代理与 AOP
依赖注入容器能把对象创建从业务代码里移出去,动态代理能在入口外包一层横切逻辑。但这两个机制都不是设计原则本身。DIP 仍然看依赖方向,生命周期仍然看作用域,AOP 仍然只拦截真实经过代理的调用。 先对照手工组装 labs/E05 的 ManualGraph 接收一个 Counter,测试断言图里的对象和调用者传入的是同一个引用。随后用 Spring Framework 6.2.11 的 AnnotationConfigApplicationContext 加载 SpringGraph:singletonCounter 两次获取是同一对象,prototypeCounter 两次获取不是同一对象。 1234try (var context = Scenario.context()) { assertSame(context.getBean("singletonCounter"), context.getBean("singletonCounter")); assertNotSame(context.getBean(&quo...
设计模式 E04:模式不证明线程安全
Singleton、Flyweight、Observer、State 都可能和共享状态放在一起。模式名称只能说明对象如何协作,不能说明多个线程同时进入时会发生什么。并发正确性要看发布、原子性、可见性和生命周期,而不是看类名里有没有 Registry、Cache 或 Bus。 用屏障制造可复现交错 JDK 21 的 CyclicBarrier 文档说明,它允许一组线程互相等待,直到所有线程都到达公共屏障点。labs/E04 用两个屏障控制两条线程:先一起进入 registry.get("shop"),再一起停在工厂内部。这样未同步 HashMap 注册表会看到两次“缓存为空”,从而为同一个租户创建两个对象。 12CyclicBarrier start = new CyclicBarrier(2);CyclicBarrier insideFactory = new CyclicBarrier(2); unsynchronizedRegistryCanCreateTwoObjectsForTheSameKey 断言创建次数为 2,并且两个返回对象不是同一身份。这里没...
设计模式 E03:默认行为和规则组合
Null Object 和 Specification 都不在 GoF 23 种模式里,但它们常出现在业务代码里。前者把“没有可选协作者”变成一个安全对象,后者把规则变成可组合对象。真正危险的地方也在这里:默认对象不能吞掉关键失败,规则组合不能把失败原因洗掉。 Null Object 只替代可选副作用 labs/E03 的 NullNotifier 什么都不做,用来替代“没有通知渠道”。这类协作者失败不会改变支付是否成功,默认对象是合理的。nullObjectCanOnlyReplaceAnOptionalSideEffect 用 Checkout 提交 100 分订单,通知器为空行为也返回通过。 关键依赖不能这么处理。支付网关、库存锁、风控结果这些东西缺失时,系统需要失败,而不是装作成功。Checkout 构造器对规则使用 Objects.requireNonNull,missingCriticalDependencyIsARealFailure 断言关键规则为 null 会抛 NullPointerException。 123456public record Checkout...
设计模式 E02:record、sealed 类型与模式匹配
第 37 篇用 Visitor 处理“节点类型稳定、操作会增加”的场景。Java 21 的 record、sealed 和 switch 模式匹配把这类模型的另一种写法放进语言里:节点可以是数据载体,类型集合可以被封闭,switch 可以在编译期检查是否覆盖所有已知类型。 record 和 sealed 改变的是数据边界 Oracle JDK 21 文档说明,record 会为组件生成字段、访问器、规范构造器、equals、hashCode 和 toString;record 字段是 final,适合作为简单数据载体。sealed class/interface 则限制哪些类可以继承或实现它。labs/E02 里,Line 只允许 Product 和 Bundle 两种实现,Product 校验 SKU 和金额,Bundle 保存子节点副本。 12345678910111213141516public sealed interface Line permits Product, Bundle { <R> R accept(Visitor<R>...










