diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md
index fb0e68fd..df3c73bc 100644
--- a/notes/系统设计基础.md
+++ b/notes/系统设计基础.md
@@ -10,6 +10,7 @@
* [七、海量数据处理](#七海量数据处理)
* [八、短链系统](#八短链系统)
* [九、Feed 流](#九feed-流)
+ * [附录:实测](#附录实测)
* [参考资料](#参考资料)
@@ -34,14 +35,14 @@
指系统能同时处理的并发用户请求数量。
-三者的关系为:并发数 = 吞吐量 × 响应时间。在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01s。
+三者的关系为:并发数 = 吞吐量 × 响应时间。在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01 s。
目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量,主要有两个原因:
-- 多 CPU
-- IO 等待时间
+- 多 CPU;
+- I/O 等待时间。
-使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。
+使用 I/O 多路复用等方式,系统在等待一个 I/O 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。
并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。
@@ -72,9 +73,7 @@
### 伸缩性与性能
-如果系统存在性能问题,那么单个用户的请求总是很慢的;
-
-如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。
+如果系统存在性能问题,那么单个用户的请求总是很慢的;如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。
### 实现伸缩性
@@ -117,11 +116,11 @@
漏桶以固定速率处理请求,桶满时丢弃新的请求;令牌桶以固定速率放入令牌,请求拿到令牌才能被处理,桶中积累的令牌允许一定的突发流量。
-
+
熔断器有关闭、打开和半开三种状态:失败率超过阈值时打开;打开一段时间后进入半开状态,放行少量请求试探,成功则关闭,失败则重新打开。
-
+
## 五、安全性
@@ -159,7 +158,7 @@ redis.call('SADD', KEYS[2], ARGV[1])
return 0 -- 抢购成功
```
-Redis 执行 Lua 脚本时不会穿插执行其它命令,判断是否已购、判断库存、扣减库存和记录用户这四步作为一个整体完成。如果在应用中先 GET 库存、判断大于 0 之后再 DECR,两个请求可能同时读到库存为 1,并且都扣减成功。用 64 个并发客户端实测,库存为 100、1000 个用户各请求 2 次:Lua 脚本恰好卖出 100 件;先查询后扣减的写法卖出了 104 件,库存变成了 -4。
+Redis 执行 Lua 脚本时不会穿插执行其它命令,判断是否已购、判断库存、扣减库存和记录用户这四步作为一个整体完成。如果在应用中先 GET 库存、判断大于 0 之后再 DECR,两个请求可能同时读到库存为 1,并且都扣减成功。例如库存为 100、1000 个用户各请求 2 次、64 个线程并发:Lua 脚本恰好卖出 100 件;先查询后扣减的写法运行 3 次,分别卖出了 113、106、114 件,库存变成了负数(Redis 7.4 实测,见[附录:实测](#附录实测))。
数据库也要兜底。扣减库存时把库存大于 0 作为条件,影响的行数为 0 说明库存不足:
@@ -226,11 +225,11 @@ UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
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,别人可以据此遍历出其他短链;对隐私敏感的链接可以改用随机的方法,或者加上有效期和访问权限。
+发号器通常用 [数据库号段](分布式.md#八分布式-id):每个实例一次领取 1000 个 ID 在内存中使用,用完再领。雪花算法生成的 ID 是 64 位的大整数,转成 Base62 有 10~11 位,用在短链中太长。需要注意的是,自增 ID 得到的短码是连续的,100000001 对应 6LAzf,别人可以据此遍历出其他短链;对隐私敏感的链接可以改用随机的方法,或者加上有效期和访问权限。
### 跳转用 301 还是 302
-301 表示永久跳转,响应默认可以被缓存,浏览器之后会直接跳到长链接;302 表示临时跳转,每次访问都会请求短链服务。在本机用 Edge 访问同一个短链两次:返回 301 时,第二次直接跳到了长链接,服务端只收到一次请求;返回 302 时,两次都请求了服务端。
+301 表示永久跳转,响应默认可以被缓存,浏览器之后会直接跳到长链接;302 表示临时跳转,每次访问都会请求短链服务。例如用浏览器访问同一个短链两次,返回 301 时,第二次直接跳到了长链接,服务端只收到一次请求;返回 302 时,两次都请求了服务端(Edge 156 实测,见[附录:实测](#附录实测))。
需要统计点击量、允许修改目标地址或者设置有效期时用 302;只想减轻服务端的压力时可以用 301。
@@ -261,7 +260,7 @@ Feed 流指的是把关注的人发布的内容按时间倒序合并后展示,
- 拉模式(读扩散):打开首页时,读取所有关注者的发件箱,合并后取最新的一页。发帖只写一次,但关注的人越多读得越慢;
- 推模式(写扩散):发帖时把帖子写入每个粉丝的收件箱,打开首页只需读自己的收件箱。读很快,但粉丝越多写得越多,而且每个粉丝都存一份帖子 ID。
-在本机的 Redis 7.4 上用有序集合实验(分值为发布时间):一个帖子推给 10 万个粉丝,用流水线写入 10 万个收件箱共 532 ms,已有的收件箱每多一条约占 16 字节;读自己的收件箱一页只需 0.12 ms;拉模式从 500 个发件箱各取最新 20 条再合并,一次要 24.5 ms。按这个速度,一个有 1000 万粉丝的大 V 发一条帖子,单个节点要写入约 50 秒,多占用约 150 MB 内存。
+用有序集合实现时(分值为发布时间),一个帖子推给 10 万个粉丝,用流水线写入 10 万个收件箱共约 480 ms,已有的收件箱每多一条约占 16 字节;读自己的收件箱一页只需 0.11 ms;拉模式从 500 个发件箱各取最新 20 条再合并,一次要 22.8 ms(Redis 7.4 实测,见[附录:实测](#附录实测))。按这个速度,一个有 1000 万粉丝的大 V 发一条帖子,单个节点要写入约 50 秒;粉丝的收件箱都已存在时多占用约 150 MB 内存,需要新建收件箱时约 1 GB。
所以实际的系统大多推拉结合:
@@ -283,9 +282,55 @@ p20 p19 p18 ... p11
左括号表示不包含 21 本身,这是 Redis 6.2 起的写法,相当于旧的 ZREVRANGEBYSCORE。分值相同的帖子可能被跳过,可以把分值设为帖子 ID 这类唯一且递增的值,例如雪花算法生成的 ID 本身就按时间递增,见 [分布式 ID](分布式.md#八分布式-id)。
+## 附录:实测
+
+环境为 Apple M1 Pro、macOS,Redis 7.4.1 使用默认配置,客户端为 redis-py。
+
+秒杀:库存 stock 为 100,用户 0~999 各请求 2 次,共 2000 个请求由 64 个线程并发执行。Lua 脚本与正文相同;对比的写法依次执行 SISMEMBER、GET、DECR、SADD 四条命令:
+
+```python
+def naive(u):
+ if c.sismember('buyers', u): return 2
+ if int(c.get('stock')) <= 0: return 1
+ c.decr('stock'); c.sadd('buyers', u)
+ return 0
+```
+
+```text
+Lua 脚本:成功 100,库存不足 1800,重复购买 100,剩余库存 0
+先 GET 再 DECR(第 1 次):成功 113,库存不足 1887,重复购买 0,剩余库存 -13
+先 GET 再 DECR(第 2 次):成功 106,库存不足 1894,重复购买 0,剩余库存 -6
+先 GET 再 DECR(第 3 次):成功 114,库存不足 1886,重复购买 0,剩余库存 -14
+```
+
+短链跳转:服务器对 /a 返回 301、对 /b 返回 302,Location 都是 /target。用 Edge 156 分别访问 /a 两次、/b 两次,服务器收到的请求:
+
+```text
+HIT /a
+HIT /target
+HIT /favicon.ico
+HIT /target
+HIT /favicon.ico
+HIT /b
+HIT /target
+HIT /b
+HIT /target
+```
+
+Feed 流:推模式向 10 万个收件箱各写入 1 条(流水线,每 1000 条提交一次);读一页为 ZREVRANGE 取 20 条,执行 1000 次取平均;拉模式为 500 个发件箱各有 20 条帖子,用流水线各取最新 20 条再排序,执行 20 次取平均。内存是写入前后 INFO memory 中 used_memory 的差:
+
+```text
+推:写入 100000 个收件箱 481 ms
+推模式读一页:0.110 ms(1000 次平均)
+拉模式读一页:22.8 ms(读 500 个发件箱共 10000 条再排序)
+新建 10 万个收件箱、各写 1 条:+10.2 MB
+已有的 10 万个收件箱各再写 1 条:+1.5 MB,每条约 16 字节
+OBJECT ENCODING inbox:0 -> listpack
+```
+
## 参考资料
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
-- 大型网站技术架构:核心原理与案例分析
+- 李智慧. 大型网站技术架构:核心原理与案例分析 [M]. 电子工业出版社, 2013.
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)