From 40067d8ef91c423ad3d0fba622abd0c23c1e0f83 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Wed, 28 Oct 2026 03:19:07 +0800 Subject: [PATCH] =?UTF-8?q?Update=20=E7=B3=BB=E7=BB=9F=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=9F=BA=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重新绘制限流和熔断的配图,更新短链长度的描述,增加实测附录。 --- notes/系统设计基础.md | 73 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 14 deletions(-) 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)