diff --git a/notes/pics/feed-push-pull.png b/notes/pics/feed-push-pull.png
new file mode 100644
index 00000000..54a8cc73
Binary files /dev/null and b/notes/pics/feed-push-pull.png differ
diff --git a/notes/pics/short-url.png b/notes/pics/short-url.png
new file mode 100644
index 00000000..3fe81ca3
Binary files /dev/null and b/notes/pics/short-url.png differ
diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md
index 4e6cfce1..fb0e68fd 100644
--- a/notes/系统设计基础.md
+++ b/notes/系统设计基础.md
@@ -8,6 +8,8 @@
* [五、安全性](#五安全性)
* [六、秒杀系统](#六秒杀系统)
* [七、海量数据处理](#七海量数据处理)
+ * [八、短链系统](#八短链系统)
+ * [九、Feed 流](#九feed-流)
* [参考资料](#参考资料)
@@ -208,8 +210,82 @@ UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
如果数据的取值范围很小,例如 1 亿个 10000 到 20000 之间的整数,一共只有 10001 种取值,用长度为 10001 的数组统计每个值出现的次数,一次扫描就能完成排序,也就是计数排序。
+## 八、短链系统
+
+短链系统把很长的网址换成很短的网址,例如把 https://shop.example.com/item/1024?from=campaign 换成 https://s.example/6LAze,常用于短信和社交媒体。用户访问短链时,服务返回一个跳转,浏览器再去访问原来的长链接。
+
+
+
+### 短码的生成
+
+常见的方法有三种:
+
+- 哈希:对长链接计算哈希(例如 MurmurHash),再把结果转成 Base62。同一个长链接总是得到同一个短码,但不同的长链接也可能得到同一个短码,冲突时要加上后缀重新计算;
+- 随机:随机生成 6 位字符,由唯一索引检查是否重复,重复就重新生成。已经用掉的短码越多越容易重复,例如已有 10 亿个短链时,新生成的短码约有 1.8% 的概率重复;
+- 发号器:先取一个全局唯一的自增 ID,再转成 Base62。不会冲突,短码也最短。
+
+Base62 指的是用 0~9、a~z、A~Z 共 62 个字符表示一个数。例如 ID 100000000 不断除以 62,依次得到余数 14、35、36、47、6,倒过来对应字符 6、L、A、z、e,短码就是 6LAze。6 位 Base62 能表示 626 ≈ 568 亿个短码,每天新增 1 亿个也能用一年半。
+
+发号器通常用 [数据库号段](分布式.md#八分布式-id):每个实例一次领取 1000 个 ID 在内存中使用,用完再领。雪花算法生成的 ID 是 64 位的大整数,转成 Base62 也有 11 位,用在短链中太长。需要注意的是,自增 ID 得到的短码是连续的,100000001 对应 6LAzf,别人可以据此遍历出其他短链;对隐私敏感的链接可以改用随机的方法,或者加上有效期和访问权限。
+
+### 跳转用 301 还是 302
+
+301 表示永久跳转,响应默认可以被缓存,浏览器之后会直接跳到长链接;302 表示临时跳转,每次访问都会请求短链服务。在本机用 Edge 访问同一个短链两次:返回 301 时,第二次直接跳到了长链接,服务端只收到一次请求;返回 302 时,两次都请求了服务端。
+
+需要统计点击量、允许修改目标地址或者设置有效期时用 302;只想减轻服务端的压力时可以用 301。
+
+### 存储与缓存
+
+```sql
+CREATE TABLE short_url (
+ code VARCHAR(8) PRIMARY KEY,
+ long_url VARCHAR(2048) NOT NULL,
+ url_hash BINARY(16) NOT NULL, -- 长链接的 MD5,用于查找同一个长链接是否已经生成过
+ expire_at DATETIME,
+ KEY idx_url_hash (url_hash)
+);
+```
+
+长链接最长可能有几千个字符,直接在 long_url 上建唯一索引会报错 Specified key was too long(InnoDB 索引最长 3072 字节),所以另存一列 16 字节的 MD5 建索引,查到后再比较完整的长链接。
+
+短链读多写少,热门链接的访问又很集中,适合用 Redis 缓存短码到长链接的映射。不存在的短码会穿透到数据库,可以缓存空值或者使用布隆过滤器,见 [缓存穿透](缓存.md#缓存穿透)。点击统计不要在跳转时同步写数据库,而是把点击事件写入消息队列,异步汇总。
+
+## 九、Feed 流
+
+Feed 流指的是把关注的人发布的内容按时间倒序合并后展示,例如朋友圈和微博的首页。每个用户有一个发件箱,保存自己发的帖子;难点在于怎样把几百个人的发件箱合成一个首页。
+
+
+
+### 拉模式与推模式
+
+- 拉模式(读扩散):打开首页时,读取所有关注者的发件箱,合并后取最新的一页。发帖只写一次,但关注的人越多读得越慢;
+- 推模式(写扩散):发帖时把帖子写入每个粉丝的收件箱,打开首页只需读自己的收件箱。读很快,但粉丝越多写得越多,而且每个粉丝都存一份帖子 ID。
+
+在本机的 Redis 7.4 上用有序集合实验(分值为发布时间):一个帖子推给 10 万个粉丝,用流水线写入 10 万个收件箱共 532 ms,已有的收件箱每多一条约占 16 字节;读自己的收件箱一页只需 0.12 ms;拉模式从 500 个发件箱各取最新 20 条再合并,一次要 24.5 ms。按这个速度,一个有 1000 万粉丝的大 V 发一条帖子,单个节点要写入约 50 秒,多占用约 150 MB 内存。
+
+所以实际的系统大多推拉结合:
+
+- 粉丝少的普通用户使用推模式;
+- 粉丝很多的大 V 不推,粉丝打开首页时,从自己的收件箱和所关注的少数几个大 V 的发件箱中拉取,再合并;
+- 只推给最近活跃的粉丝,长期不登录的用户回来时再用拉模式重建收件箱;
+- 收件箱只保留最近的若干条,例如每次写入后执行 ZREMRANGEBYRANK inbox:1001 0 -1001,只留下分值最大的 1000 条,更早的内容到数据库中查。
+
+### 分页
+
+Feed 流的分页不能用偏移量。例如收件箱中有 p1~p30 共 30 条帖子,第 1 页取到 p30~p21;用户翻页之前又有 3 条新帖子进来,这时按偏移量取第 11~20 条,结果是 p23~p14,p23、p22、p21 重复出现了。
+
+应该用上一页最后一条的分值作为游标,取比它更早的帖子:
+
+```text
+> ZRANGE inbox:1001 (21 -inf BYSCORE REV LIMIT 0 10
+p20 p19 p18 ... p11
+```
+
+左括号表示不包含 21 本身,这是 Redis 6.2 起的写法,相当于旧的 ZREVRANGEBYSCORE。分值相同的帖子可能被跳过,可以把分值设为帖子 ID 这类唯一且递增的值,例如雪花算法生成的 ID 本身就按时间递增,见 [分布式 ID](分布式.md#八分布式-id)。
+
## 参考资料
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
- 大型网站技术架构:核心原理与案例分析
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
+- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)