mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-27 05:23:46 +00:00
Update 系统设计基础.md
补充秒杀系统的设计:层层过滤请求、Redis Lua 预扣库存、异步下单和数据库兜底;补充海量数据处理:Top K、按哈希切分、位图和外部排序,并配图。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
@@ -6,6 +6,8 @@
|
||||
* [三、扩展性](#三扩展性)
|
||||
* [四、可用性](#四可用性)
|
||||
* [五、安全性](#五安全性)
|
||||
* [六、秒杀系统](#六秒杀系统)
|
||||
* [七、海量数据处理](#七海量数据处理)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -123,7 +125,91 @@
|
||||
|
||||
要求系统在应对各种攻击手段时能够有可靠的应对措施,常见的攻击手段和防范方法见 [攻击技术](攻击技术.md)。
|
||||
|
||||
## 六、秒杀系统
|
||||
|
||||
秒杀指的是少量商品在某一时刻开售,大量用户同时抢购,例如 100 件商品在开售瞬间收到 10 万个请求。难点在于:
|
||||
|
||||
- 瞬时流量远超平时,但绝大多数请求注定失败;
|
||||
- 库存不能超卖,一个用户只能买一件;
|
||||
- 抢购不能拖垮正常的业务。
|
||||
|
||||
解决思路是层层过滤,使最终到达数据库的请求接近库存数量:
|
||||
|
||||
<div align="center"> <img src="pics/seckill.png" width="720px"/> </div><br>
|
||||
|
||||
1. 前端和网关:开售前按钮置灰,提交后禁止重复点击,网关按用户和接口限流,拦截刷单请求;
|
||||
2. 库存预扣:开售前把库存加载到 Redis 中,用 Lua 脚本在 Redis 中判断并扣减库存,没有库存的请求直接返回已抢完;
|
||||
3. 异步下单:扣减成功的请求写入消息队列,订单服务按自己的处理能力消费,扣减数据库中的库存并创建订单,用户通过轮询等方式查询结果;
|
||||
4. 隔离:秒杀服务独立部署,避免影响正常的业务。
|
||||
|
||||
预扣库存的 Lua 脚本如下:
|
||||
|
||||
```lua
|
||||
-- KEYS[1]:库存,KEYS[2]:已购用户集合,ARGV[1]:用户 ID
|
||||
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
|
||||
return 2 -- 一人一单,重复购买
|
||||
end
|
||||
if tonumber(redis.call('GET', KEYS[1])) <= 0 then
|
||||
return 1 -- 库存不足
|
||||
end
|
||||
redis.call('DECR', KEYS[1])
|
||||
redis.call('SADD', KEYS[2], ARGV[1])
|
||||
return 0 -- 抢购成功
|
||||
```
|
||||
|
||||
Redis 执行 Lua 脚本时不会穿插执行其它命令,判断是否已购、判断库存、扣减库存和记录用户这四步作为一个整体完成。如果在应用中先 GET 库存、判断大于 0 之后再 DECR,两个请求可能同时读到库存为 1,并且都扣减成功。用 64 个并发客户端实测,库存为 100、1000 个用户各请求 2 次:Lua 脚本恰好卖出 100 件;先查询后扣减的写法卖出了 104 件,库存变成了 -4。
|
||||
|
||||
数据库也要兜底。扣减库存时把库存大于 0 作为条件,影响的行数为 0 说明库存不足:
|
||||
|
||||
```sql
|
||||
UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
|
||||
```
|
||||
|
||||
一人一单可以用订单表上(用户 ID,商品 ID)的唯一索引保证,消息被重复消费时插入失败,也就实现了幂等。Redis 中扣减成功、但订单最终创建失败时,需要把 Redis 中的库存和购买记录恢复,并定期与数据库对账。
|
||||
|
||||
## 七、海量数据处理
|
||||
|
||||
这类问题的特点是数据量远大于内存,例如“100 GB 的文件,内存只有 4 GB”。常用的思路是分而治之:先把数据切成能放进内存的小块,分别处理后再合并结果。
|
||||
|
||||
### Top K
|
||||
|
||||
从 1 亿个整数中找出最大的 10 个:维护一个大小为 10 的小顶堆,堆顶是当前第 10 大的数。依次读入每个数,比堆顶大就替换堆顶并调整堆,最后堆中的 10 个数就是答案。时间复杂度为 O(NlogK),内存中只需保存 10 个数,数据可以一边读一边处理;而全部排序需要 O(NlogN) 的时间,还要把 400 MB 的数据都读入内存。Java 实现见 [Leetcode 题解 - 排序](Leetcode%20题解%20-%20排序.md#1-kth-element)。
|
||||
|
||||
### 出现次数最多的 K 个
|
||||
|
||||
100 GB 的日志文件中每行是一个 URL,内存只有 4 GB,找出出现次数最多的 10 个 URL。直接用 HashMap 统计所有 URL 会超出内存,需要先按哈希切分:
|
||||
|
||||
<div align="center"> <img src="pics/massive-topk.png" width="720px"/> </div><br>
|
||||
|
||||
1. 逐行读取,按 hash(url) % 100 写入 100 个小文件,平均每个约 1 GB;
|
||||
2. 依次把每个小文件读入内存,用 HashMap 统计每个 URL 的次数,再用小顶堆得到这个文件中出现次数最多的 10 个;
|
||||
3. 用一个小顶堆从 100 × 10 个候选中选出全局的前 10 个。
|
||||
|
||||
这样做的关键是相同的 URL 一定落在同一个小文件中,所以每个文件单独统计出的次数就是这个 URL 的总次数。HashMap 占用的内存取决于文件中不同 URL 的数量,如果某个文件中不同的 URL 仍然太多,可以换一个哈希函数对它再次切分。
|
||||
|
||||
### 两个大文件的交集
|
||||
|
||||
两个文件各有 50 亿个 URL,每个 URL 64 字节,找出两个文件共同的 URL。每个文件有 320 GB,用同一个哈希函数把两个文件各切成 1000 份,每份约 320 MB。相同的 URL 一定落在编号相同的一对小文件中,只需要依次把 a<sub>i</sub> 读入 HashSet,再逐行检查 b<sub>i</sub>。
|
||||
|
||||
如果允许少量误判,可以用 [布隆过滤器](缓存.md#缓存穿透):把 a 中的 URL 都加入布隆过滤器,再逐个检查 b 中的 URL。4 GB 内存约有 344 亿位,每个 URL 平均 6.9 位,使用 5 个哈希函数时误判率约为 3.7%;要把误判率降到 1%,每个 URL 需要约 9.6 位,共需 5.6 GB。
|
||||
|
||||
### 去重与判断是否存在
|
||||
|
||||
数据是整数时可以用位图,每个整数只用 1 位表示是否出现。int 共有 2<sup>32</sup> 个取值,位图只需要 512 MB。
|
||||
|
||||
例如在 2 亿个整数中找出只出现一次的数,可以给每个整数分配 2 位:00 表示没出现,01 表示出现一次,10 表示出现多次,共需 1 GB。读完所有数据后扫描位图,取出状态为 01 的数。
|
||||
|
||||
### 外部排序
|
||||
|
||||
100 GB 的数据排序,内存只有 1 GB:
|
||||
|
||||
1. 分段排序:每次读入 1 GB 数据,在内存中排序后写回磁盘,得到 100 个有序的文件;
|
||||
2. 多路归并:同时打开这 100 个文件,用大小为 100 的小顶堆,每次取出最小的元素写入结果文件,再从它所在的文件读入下一个元素。
|
||||
|
||||
如果数据的取值范围很小,例如 1 亿个 10000 到 20000 之间的整数,一共只有 10001 种取值,用长度为 10001 的数组统计每个值出现的次数,一次扫描就能完成排序,也就是计数排序。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
|
||||
- 大型网站技术架构:核心原理与案例分析
|
||||
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300">
|
||||
<style>
|
||||
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
|
||||
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
|
||||
.b { font-weight: bold; }
|
||||
.c { text-anchor: middle; }
|
||||
.e { text-anchor: end; }
|
||||
.note { fill: #C0605A; }
|
||||
.ok { fill: #6A9F68; }
|
||||
.sub { font-size: 11px; fill: #888; }
|
||||
.mono { font-family: Menlo, Consolas, monospace; }
|
||||
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
|
||||
.red { stroke: #C0605A; marker-end: url(#r); }
|
||||
.green { stroke: #6A9F68; marker-end: url(#g); }
|
||||
.dash { stroke-dasharray: 5 4; }
|
||||
.line { stroke: #666; stroke-width: 1.3; fill: none; }
|
||||
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
|
||||
.shadow { fill: #DADADA; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
|
||||
</marker>
|
||||
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
|
||||
</marker>
|
||||
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
|
||||
<text x="360" y="28" class="title">100 GB 日志中出现次数最多的 10 个 URL(内存 4 GB)</text>
|
||||
<rect x="22" y="112" width="110" height="70" rx="6" class="shadow"/>
|
||||
<rect x="20" y="110" width="110" height="70" rx="6" fill="#E6E6E6"/>
|
||||
<text x="75" y="143" class="b c">100 GB 日志</text>
|
||||
<text x="75" y="158" class="sub c">每行一个 URL</text>
|
||||
<text x="75" y="204" class="c sub mono">hash(url) % 100</text>
|
||||
<rect x="197" y="58" width="80" height="34" rx="6" class="shadow"/>
|
||||
<rect x="195" y="56" width="80" height="34" rx="6" fill="#B1DDF0"/>
|
||||
<text x="235" y="77.5" class="b c">文件 0</text>
|
||||
<line x1="133" y1="145" x2="191" y2="73" class="arrow "/>
|
||||
<rect x="320" y="56" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
|
||||
<text x="395" y="77.5" class="c sub">HashMap 计数</text>
|
||||
<line x1="278" y1="73" x2="316" y2="73" class="arrow "/>
|
||||
<rect x="507" y="58" width="70" height="34" rx="6" class="shadow"/>
|
||||
<rect x="505" y="56" width="70" height="34" rx="6" fill="#DDD7F6"/>
|
||||
<text x="540" y="77.5" class="b c">前 10</text>
|
||||
<line x1="473" y1="73" x2="501" y2="73" class="arrow "/>
|
||||
<line x1="578" y1="73" x2="618" y2="145" class="arrow "/>
|
||||
<rect x="197" y="110" width="80" height="34" rx="6" class="shadow"/>
|
||||
<rect x="195" y="108" width="80" height="34" rx="6" fill="#B1DDF0"/>
|
||||
<text x="235" y="129.5" class="b c">文件 1</text>
|
||||
<line x1="133" y1="145" x2="191" y2="125" class="arrow "/>
|
||||
<rect x="320" y="108" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
|
||||
<text x="395" y="129.5" class="c sub">HashMap 计数</text>
|
||||
<line x1="278" y1="125" x2="316" y2="125" class="arrow "/>
|
||||
<rect x="507" y="110" width="70" height="34" rx="6" class="shadow"/>
|
||||
<rect x="505" y="108" width="70" height="34" rx="6" fill="#DDD7F6"/>
|
||||
<text x="540" y="129.5" class="b c">前 10</text>
|
||||
<line x1="473" y1="125" x2="501" y2="125" class="arrow "/>
|
||||
<line x1="578" y1="125" x2="618" y2="145" class="arrow "/>
|
||||
<text x="235" y="182" class="c b">⋮</text>
|
||||
<rect x="197" y="214" width="80" height="34" rx="6" class="shadow"/>
|
||||
<rect x="195" y="212" width="80" height="34" rx="6" fill="#B1DDF0"/>
|
||||
<text x="235" y="233.5" class="b c">文件 99</text>
|
||||
<line x1="133" y1="145" x2="191" y2="229" class="arrow "/>
|
||||
<rect x="320" y="212" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
|
||||
<text x="395" y="233.5" class="c sub">HashMap 计数</text>
|
||||
<line x1="278" y1="229" x2="316" y2="229" class="arrow "/>
|
||||
<rect x="507" y="214" width="70" height="34" rx="6" class="shadow"/>
|
||||
<rect x="505" y="212" width="70" height="34" rx="6" fill="#DDD7F6"/>
|
||||
<text x="540" y="233.5" class="b c">前 10</text>
|
||||
<line x1="473" y1="229" x2="501" y2="229" class="arrow "/>
|
||||
<line x1="578" y1="229" x2="618" y2="145" class="arrow "/>
|
||||
<rect x="624" y="118" width="80" height="58" rx="6" class="shadow"/>
|
||||
<rect x="622" y="116" width="80" height="58" rx="6" fill="#B0E3E6"/>
|
||||
<text x="662" y="143" class="b c">小顶堆</text>
|
||||
<text x="662" y="158" class="sub c">全局前 10</text>
|
||||
<text x="235" y="262" class="c sub">① 约 1 GB / 份</text>
|
||||
<text x="395" y="262" class="c sub">② 每次只把一份读入内存</text>
|
||||
<text x="560" y="262" class="c sub">③ 合并 100 × 10 个候选</text>
|
||||
<text x="360" y="288" class="c note">相同的 URL 一定落在同一份中,所以每份单独统计的次数就是它的总次数</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="250" viewBox="0 0 720 250">
|
||||
<style>
|
||||
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
|
||||
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
|
||||
.b { font-weight: bold; }
|
||||
.c { text-anchor: middle; }
|
||||
.e { text-anchor: end; }
|
||||
.note { fill: #C0605A; }
|
||||
.ok { fill: #6A9F68; }
|
||||
.sub { font-size: 11px; fill: #888; }
|
||||
.mono { font-family: Menlo, Consolas, monospace; }
|
||||
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
|
||||
.red { stroke: #C0605A; marker-end: url(#r); }
|
||||
.green { stroke: #6A9F68; marker-end: url(#g); }
|
||||
.dash { stroke-dasharray: 5 4; }
|
||||
.line { stroke: #666; stroke-width: 1.3; fill: none; }
|
||||
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
|
||||
.shadow { fill: #DADADA; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
|
||||
</marker>
|
||||
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
|
||||
</marker>
|
||||
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
|
||||
<text x="360" y="28" class="title">秒杀:层层过滤请求</text>
|
||||
<rect x="22" y="70" width="96" height="56" rx="6" class="shadow"/>
|
||||
<rect x="20" y="68" width="96" height="56" rx="6" fill="#DDD7F6"/>
|
||||
<text x="68" y="94" class="b c">10 万个请求</text>
|
||||
<text x="68" y="109" class="sub c">同时到达</text>
|
||||
<rect x="152" y="62" width="120" height="72" rx="6" class="shadow"/>
|
||||
<rect x="150" y="60" width="120" height="72" rx="6" fill="#B1DDF0"/>
|
||||
<text x="210" y="86" class="c b">前端与网关</text>
|
||||
<text x="210" y="106" class="c sub">按钮防重复提交</text>
|
||||
<text x="210" y="122" class="c sub">按用户限流</text>
|
||||
<rect x="306" y="62" width="140" height="72" rx="6" class="shadow"/>
|
||||
<rect x="304" y="60" width="140" height="72" rx="6" fill="#B0E3E6"/>
|
||||
<text x="374" y="86" class="c b">Redis + Lua</text>
|
||||
<text x="374" y="106" class="c sub">判断库存和是否已购</text>
|
||||
<text x="374" y="122" class="c sub">原子扣减,库存 100 → 0</text>
|
||||
<rect x="482" y="70" width="90" height="56" rx="6" class="shadow"/>
|
||||
<rect x="480" y="68" width="90" height="56" rx="6" fill="#E6E6E6"/>
|
||||
<text x="525" y="94" class="b c">消息队列</text>
|
||||
<text x="525" y="109" class="sub c">100 条</text>
|
||||
<rect x="606" y="62" width="100" height="72" rx="6" class="shadow"/>
|
||||
<rect x="604" y="60" width="100" height="72" rx="6" fill="#B1DDF0"/>
|
||||
<text x="654" y="86" class="c b">订单服务</text>
|
||||
<text x="654" y="106" class="c sub">扣数据库库存</text>
|
||||
<text x="654" y="122" class="c sub">创建订单</text>
|
||||
<line x1="119" y1="96" x2="146" y2="96" class="arrow "/>
|
||||
<line x1="273" y1="96" x2="300" y2="96" class="arrow "/>
|
||||
<line x1="447" y1="96" x2="476" y2="96" class="arrow green"/>
|
||||
<text x="461" y="88" class="c ok">成功</text>
|
||||
<line x1="573" y1="96" x2="600" y2="96" class="arrow "/>
|
||||
<line x1="374" y1="136" x2="374" y2="172" class="arrow red"/>
|
||||
<text x="382" y="160" class="note">其余请求</text>
|
||||
<rect x="310" y="176" width="128" height="34" rx="6" fill="#F3C6C2"/>
|
||||
<text x="374" y="197.5" class="c sub">直接返回“已抢完”</text>
|
||||
<line x1="210" y1="136" x2="210" y2="172" class="arrow red"/>
|
||||
<rect x="150" y="176" width="120" height="34" rx="6" fill="#F3C6C2"/>
|
||||
<text x="210" y="197.5" class="c sub">拦截重复和超限请求</text>
|
||||
<text x="654" y="156" class="c sub">库存大于 0 才扣减</text>
|
||||
<text x="654" y="174" class="c sub">唯一索引保证一人一单</text>
|
||||
<text x="20" y="236" class="sub">到达数据库的请求接近库存数量;订单最终创建失败时,要把 Redis 中的库存和购买记录恢复。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
Reference in New Issue
Block a user