Skip to content

Commit

Permalink
feat: chapter 10
Browse files Browse the repository at this point in the history
  • Loading branch information
honkinglin committed Dec 16, 2024
1 parent 2a86fe7 commit c1cf6ba
Showing 1 changed file with 25 additions and 1 deletion.
26 changes: 25 additions & 1 deletion docs/grokking/chapter-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,36 @@
1. **固定窗口的局限性**
这是一个固定窗口算法,因为我们在每分钟结束后重置了“StartTime”。这种做法可能导致每分钟允许的请求数翻倍。想象一下,如果 Kristie 在某一分钟的最后一秒内发送了 3 个请求,那么她可以在下一分钟的第一秒再次发送 3 个请求,从而在两秒内总共发出 6 个请求。对此问题的解决方案是使用滑动窗口算法,我们会在后面进行讨论。

![图10-4](/grokking/f10-4.png)

2. **原子性问题**
在分布式环境中,“先读后写”的操作模式会引发竞态条件。举例来说,如果 Kristie 当前的 `Count` 为 2,而她又同时发起两个新请求,如果这两个请求分别被两个并发进程处理,并且都在任一进程更新 `Count` 之前读取到了 `Count=2`,那么每个进程都会错误地认为 Kristie 还可以再发送一次请求,导致没有正确执行限流。

![图10-4](/grokking/f10-4.png)
![图10-5](/grokking/f10-5.png)

如果我们使用 Redis 来存储键值数据,一种解决原子性问题的方法是在读写操作期间使用 Redis 锁。然而,这会让同一用户的并发请求被顺序化处理,降低吞吐量并增加系统复杂度。我们也可以使用 Memcached,但也同样会面临类似的复杂性。

如果我们使用简单的哈希表,可以为每条记录实现自定义的加锁机制,以解决上述的原子性问题。

假设我们将所有用户数据都存储在一个哈希表中,那么需要多少内存?

假设:
- `UserID` 占用 8 字节
- `Count` 占用 2 字节(可计数到 65k,对于大多数场景足够)
- Epoch 时间需要 4 字节,但如果只存储分钟和秒的部分(合并成一个 2 字节的表示方式),那么可将时间戳占用压缩到 2 字节

因此,每位用户的数据需要总共 12 字节:
```
8 + 2 + 2 = 12 字节
```

假设哈希表每条记录有 20 字节的额外开销。如果需要在任意时刻跟踪 100 万(1 million)个用户,所需内存总量约为 32MB:
```
(12 + 20) 字节 * 1,000,000 = 32MB
```

如果我们为了处理原子性问题,还需要给每条用户记录分配一个 4 字节的“锁”,那么总内存需求就变为 36MB。
这样看来,把所有数据存放在单台服务器的内存中是可行的;但我们并不希望所有流量都路由到一台机器上。再者,如果限流器的限制是每秒允许 10 次请求,那么限流器可能需要应对 1,000,000 用户 * 10 QPS = **10,000,000 QPS**,这对于单台机器来说负载过大。

在实际中,我们可以采用 Redis 或 Memcached 等分布式方案。所有数据都会存储在远程的 Redis 服务器中,所有限流器节点在允许或拒绝请求之前,都会先去读写这些远程服务器的数据。

0 comments on commit c1cf6ba

Please sign in to comment.