后端系统工程 30:关注关系与可撤销订阅
关注关系看起来只是一张两列的表,难点在于它同时是一种授权。用户 A 关注私密账号 B 并获批后,A 就能读 B 的“仅粉丝可见”内容;B 拉黑 A 之后,这项授权必须收回,而且收回之后 B 新发的内容不能再被 A 看到。关系表之外通常还有粉丝列表、粉丝计数、下一篇要讲的 Feed 收件箱等派生视图,它们靠消费关系事件异步更新。重复点击关注、关注与取消交错、事件乱序和重复投递、消费者丢失进度,都会让派生视图与真相源出现差距;差距一旦落在权限上,就是越权读取。 本章的工程放在 examples/social 的 social.rel 包中,全部用例跑在真实 PostgreSQL 16.15 事务上。验收目标有两条:重复关注、取消与事件乱序不会让已经取消的关系“复活”;内容入口与派生视图在权限判断上给出一致的结论,或者明确声明二者允许相差的窗口。 需求与范围 操作只有六个:关注、取消关注、私密账号审批(同意或拒绝)、拉黑、解除拉黑、发布带可见性的内容。可见性分三档:PUBLIC 任何未被拉黑的人可读,FOLLOWERS 只有有效粉丝可读,SELF 只有作者本人可读。 业务不变量写成可以...
后端系统工程 31:Feed 的扇出成本与热点
首页 Feed 要回答一个问题:读者当前关注的作者里,最近发了哪些读者有权看的帖子,按什么顺序排,翻到第二页时从哪里接着读。工程上的争议集中在“候选什么时候算出来”。发帖时把帖子写进每个粉丝的收件箱(推),代价落在写路径上,并且和作者粉丝数成正比;读 Feed 时现场到每个被关注作者的时间线里取(拉),代价落在读路径上,和读者关注的作者数成正比;按作者粉丝数分档(混合),则把两种代价按阈值切开。粉丝数呈重尾分布时,少数热点作者决定了推方案的写入峰值,也决定了拉方案里被反复读取的那几条时间线。 另一层要求来自第 30 篇:关注关系是一种授权。收件箱里的行是发帖时刻算出的候选,读者之后取消关注、被拉黑、帖子改成仅自己可见,这些行不会自己消失。如果读路径直接把物化的收件箱行返回给读者,撤销就被绕过了。 本章工程在 examples/social 的 social.feed 包中,复用第 30 篇 social.rel 的关系表、RelationService 与 AccessCheck,全部用例跑在真实 PostgreSQL 16.15 上。验收目标有两条:在同一张合成关注图上比较推、...
后端系统工程 32:点赞事实与计数校准
点赞数是派生数据。真相源是“谁赞了哪个条目”这组事实,计数只是对事实的汇总;为了让读取不必每次 count(*),计数被单独存放、异步更新。破坏两者一致的路径有五条:用户重复点击,赞与取消交错,事件被重复投递,消费者在提交与确认之间崩溃,以及修复用的批量重算与在线增量同时改同一行。前三条会把计数推高或拉低;消费者崩溃会让已生效的事件被再投一次;批量重算若与在线增量直接冲突,修复本身就会制造新的偏差。 本章代码在 examples/social 的 social.like 包,全部用例跑在真实 PostgreSQL 16.15 上,包括独立 JVM 的 kill -9 与数据库 immediate 停机。验收目标:点赞/取消交错与事件重复之后,计数与事实记录一致;计数允许落后于事实,落后多少有明确定义,并且在任何时刻都能算出误差上界。 需求与范围 操作有三个:点赞、取消点赞、读取条目的点赞数。业务不变量写成可以用 SQL 检查的形式: 每个 (user, item) 至多一行事实,liked 表示当前是否已赞,version 每次状态变化加一。 每次状态变化恰好写一条 Outbo...
后端系统工程 33:通知渠道的结果未知
通知服务调用外部渠道时,最难处理的结果是“没有结果”:请求发出去了,响应没回来。渠道可能根本没收到,也可能已经接受并开始外发,只是响应在路上丢了。本地数据库能把“同一事件只生成一条逻辑通知”做成硬约束,却无法替外部渠道保证“用户只收到一次”。本篇用一个最小通知服务把这两件事分开:逻辑去重由唯一约束负责,外部重复由渠道契约决定,结果未知要作为一种持久状态记录下来,并且有出口。 需求与范围 业务方提交事件,例如“订单 ord-1001 已发货”,通知服务按用户偏好为每个启用渠道生成投递任务,由工作进程领取后调用渠道。要满足的条件如下。 同一 (user_id, event_key) 无论被提交多少次、多少并发,只产生一条逻辑通知。 每条逻辑通知在每个启用渠道上最多一条任务;同一任务在同一时刻最多有一个有效令牌;被取代的持有者无法再改写任务状态,但它已发出的外部调用无法撤回。 每次调用渠道都留下一条投递记录,结果未知的调用能在表里查到,不被后续结果覆盖。 超时后的重试策略取决于渠道能力:支持幂等键就带同一键重发;不支持幂等键但可查询就先查询;两者都不具备就停止自动处理。 领取前偏好被...
后端系统工程 34:可撤销短链与访问统计
短链服务的主路径只有一次查表和一个 3xx 响应,难点在三个容易被忽略的边界:两个创建者同时申请同一个别名时谁的目标留下;所有者撤销一条短链之后,旧目标还会被返回多久;统计库变慢或不可用时,跳转是否跟着变慢。实现是一个 Java 21 + PostgreSQL 16 的最小服务,三个问题各自对应一组可执行的断言。 需求与范围 操作只有五种:创建随机码短链、创建自定义别名、撤销、按码访问、记录点击。过期时间在创建时写定,之后不改;目标地址创建后不可修改,需要改目标就撤销旧码再建新码。 业务不变量: I1 码唯一且不覆盖。一个码最多对应一个目标;任何被告知“创建成功”的调用方,其提交的目标就是库中该码的目标。 I2 撤销有界生效。撤销事务提交后,返回旧目标的 302 只会在一个可计算的窗口内出现,窗口上界由缓存参数决定,并且可以测量。 I3 统计不阻塞跳转。统计库连接失败、表锁等待或写入超时,不改变重定向的状态码,请求线程也不等待统计侧的连接或锁。 I4 点击至多计一次。同一次点击因重试被写入多次时,统计表里只有一行。点击可以因队列满而丢弃,但丢弃数必须被计数。 明确排除:账号与...
后端系统工程 35:文件上传与异步处理
一次文件上传跨越两个互不共享事务的存储:字节落在文件系统或对象存储里,文件名、归属、处理进度落在数据库里。两边各自可以做到原子,合起来却没有一次提交能同时覆盖它们。于是有三种残局必须单独处理:字节写完了、元数据没提交,字节成了没人认领的孤儿;后台处理任务因为租约过期被执行了两次,发布动作也跟着做了两次;处理还没结束,读取接口已经把文件交了出去。示例实现基于 Java 21、PostgreSQL 16 和一个本地目录替身,覆盖从分块上传到发布读取的完整链路,三种残局分别用崩溃、重复执行和提前读取的实验构造出来。 需求与范围 操作有七个:创建上传会话、上传分块、完成上传、提交元数据、中止会话、读取文件,以及后台的处理与回收。客户端在创建会话时声明分块数、总字节数和整体 SHA-256,分块可以乱序、可以重传。 业务不变量: F1 字节不丢、不错。任何进入内容地址 blobs/sha256/<hex> 的文件,其内容的 SHA-256 等于 <hex>;任何被 file_object 引用的 blob 在回收后仍然存在。 F2 孤儿可回收。字节已经写入但元数据...
后端系统工程 36:指标采集与告警生命周期——缺数据不能算恢复
本文属于《后端系统工程:从接口契约到故障恢复》系列。文中提到的演练工程、测试与实验证据位于博客源码仓库的 batch-4/ 目录(examples/、evidence/ 等子目录);业务数据均为合成数据。 告警系统一次只回答一个问题:现在是否需要叫人。一条规则从采样到通知要经过五个环节:样本是否到达、条件是否持续、多条告警怎样合并、哪些应当暂时不发、什么时候可以宣布恢复。任何一环把“没有数据”当成“数据正常”,值班人员都会在故障还在的时候收到一条恢复通知。 本篇用一个离散时钟模型实现这条链路,覆盖数据缺失、触发、重复抑制、静默、抑制、恢复和规则变更,并用负向对照证明错误做法确实会出错。模型借用 Prometheus 与 Alertmanager 的概念命名,行为以本文契约为准;凡是与官方实现有差异的地方,正文单独说明。 1. 需求与不变量 监控对象是一个合成的 checkout 服务,两个实例 c1、c2,每 15 秒采集一次 error_ratio(错误请求占比)和 up(采集是否成功)。告警规则有两条: 规则 条件 for keep_firing_for 缺数据宽...
后端系统工程 37:超时预算与负载保护——慢下游结束之后系统为什么还起不来
本文属于《后端系统工程:从接口契约到故障恢复》系列。文中提到的演练工程、测试与实验证据位于博客源码仓库的 batch-4/ 目录(examples/、evidence/ 等子目录);业务数据均为合成数据。 一个下游变慢 20 秒,上游在这 20 秒里失败,这是预期之内的损失。更难接受的是另一种结果:下游已经恢复,上游的成功率仍然是零,并且一直保持下去。本篇用同一份合成负载比较四种超时与重试策略,测量请求放大、拒绝、积压和恢复时间,并解释“慢下游结束之后系统起不来”是怎样被重试和无界队列维持住的。 实验是确定性的离散事件仿真。它能比较策略之间的相对行为,不能给出任何真实服务的吞吐或延迟。 1. 需求与规模假设 调用链为 客户端 → gateway → svc_a → svc_b。 项 假设值 说明 客户端到达 泊松,平均 400 次/秒,持续 60 秒 固定随机种子 客户端截止时间 1 秒 用户等待上限;超时后客户端最多再试 2 次,间隔 50 毫秒 svc_b 并发槽位 16 平均服务时间 20 毫秒,指数分布;理论容量约 800 次/秒 慢下游窗...
后端系统工程 38:数据备份、恢复与地域切换——恢复演练要检查数据,不只检查进程
本文属于《后端系统工程:从接口契约到故障恢复》系列。文中提到的演练工程、测试与实验证据位于博客源码仓库的 batch-4/ 目录(examples/、evidence/ 等子目录);业务数据均为合成数据。 “备份成功”是一条日志,“能恢复到指定时刻且数据正确”才是一项能力。两者之间隔着几个容易略过的问题:恢复点到底落在哪里,归档的 WAL 是否完整,恢复出来的实例里账是否平,角色和权限是否还在,切换之后旧主库会不会继续接受写入。 本篇在一台机器上启动多个隔离的 PostgreSQL 16 实例,实际执行基础备份、WAL 归档、按命名还原点的时间点恢复(PITR)、计划内切换和注入复制中断的非计划切换,每一步都用 SQL 检查数据本身。所有演练证明的是脚本与步骤在单机上可执行、检查项能发现错误;它们不证明跨地域网络、存储、DNS 或权限体系的行为。 1. 需求、目标与不变量 被保护的数据是一个合成的双边记账库:account(id, balance) 有 200 个账户,entry(request_id, account_id, amount, batch) 记录每笔转账的借...
后端系统工程 39:综合方案评审与交付——沿四条路径检查跨模块保证
本文属于《后端系统工程:从接口契约到故障恢复》系列。文中提到的演练工程、测试与实验证据位于博客源码仓库的 batch-4/ 目录(examples/、evidence/ 等子目录);业务数据均为合成数据。 单个模块的测试证明的是模块自己的承诺:消息服务保证同一个客户端请求只产生一条消息,队列保证至少投递一次,通知服务保证按幂等键推送。用户看到的结果却取决于这些承诺怎样接在一起:消息已经撤回,通知还会不会把正文推出去;队列重投一条事件,用户会不会收到两次推送;配置回退之后,正在进行的撤回按哪个窗口判断;通知服务停了十分钟,恢复后积压能否排空、告警是否如实触发和恢复。 本篇的评审对象是一个专门构造、范围受控的合成系统 Relay,有明确的接口契约和不变量,并沿发送、撤回、配置回退、积压恢复四条路径写成可执行的验收用例。Relay 不是系列 04–35 章实现的汇总。本批次没有取得那些章节的实现,本文不声称审阅或验证了它们;它们与 Relay 的对接点在第 9 节逐项列为待集成项。 1. 评审对象 模块 职责 持久状态 消息 发送、撤回、历史查询 message、c...










