mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-30 18:23:52 +00:00
Update 缓存.md
补充缓存击穿的互斥锁和逻辑过期、布隆过滤器的误判率与参数、LRU 的线程安全,增加实测附录。
This commit is contained in:
+74
-2
@@ -129,14 +129,26 @@ CDN 主要有以下优点:
|
||||
|
||||
<div align="center"> <img src="pics/bloom-filter.png" width="620px"/> </div><br>
|
||||
|
||||
误判是因为这些位置可能都被其它 Key 置为 1。插入 n 个 Key、位数组为 m 位、使用 k 个哈希函数时,误判率约为 (1 - e<sup>-kn/m</sup>)<sup>k</sup>,k 取 (m/n) ln 2 时最小。每个 Key 占 10 位、k = 7 时,100 万个 Key 只需要 1.2 MB,按公式误判率为 0.82%;实际插入 100 万个 Key,再查询 100 万个不存在的 Key,误判率也是 0.82%(见[附录 A6](#a6-布隆过滤器的误判率))。目标误判率为 1% 时,每个 Key 约需要 9.6 位,1 亿个 Key 约 114 MB。
|
||||
|
||||
使用时需要注意:
|
||||
|
||||
- 容量要预估准确:插入的 Key 超过预期后误判率迅速上升,例如按 100 万个 Key 配置的过滤器插入了 200 万个 Key,按公式误判率会升到约 14%;
|
||||
- 不能删除:一个位可能同时属于多个 Key,清零会让其它 Key 被误判为不存在,需要删除时可以定期重建,或者使用计数布隆过滤器、布谷鸟过滤器;
|
||||
- 新增的数据要同时写入布隆过滤器,否则请求会被误判为不存在而拒绝。
|
||||
|
||||
Redis 8 起内置了布隆过滤器(BF.RESERVE、BF.ADD、BF.EXISTS),Redis 7 需要加载 RedisBloom 模块。
|
||||
|
||||
### 缓存击穿
|
||||
|
||||
指的是某个热点数据的缓存过期时,大量对它的请求同时到达数据库。
|
||||
|
||||
解决方案:
|
||||
|
||||
- 使用互斥锁,只允许一个请求去数据库加载数据并写回缓存,其它请求等待后直接读缓存;
|
||||
- 热点数据不设置过期时间,或者由后台线程异步更新。
|
||||
- 互斥锁:缓存未命中时用 SET lock NX EX 3 抢锁,抢到的请求先再查一次缓存(可能别人已经写回),仍然没有才查询数据库并写回缓存,最后释放锁(释放时要确认锁仍是自己的,见 [分布式锁](分布式.md#redis-的-set-nx-指令));没抢到的请求等待几十毫秒后重新读缓存;
|
||||
- 逻辑过期:键不设置过期时间,而是在值中保存一个过期时间。读到已经过期的值时,抢到锁的请求启动后台线程重建缓存,所有请求都直接返回旧值,不等待。
|
||||
|
||||
两者都只让一个请求查询数据库,区别在于等待重建还是返回旧值。热点键刚过期时 100 个请求同时到达,数据库查询耗时 50 毫秒:不加保护时查询了数据库 68~100 次;互斥锁只查询 1 次,请求耗时的中位数约 90~100 毫秒;逻辑过期也只查询 1 次,耗时中位数约 12 毫秒,但 100 个请求拿到的都是旧值(Redis 7.4、MySQL 8.0 实测,见[附录 A5](#a5-缓存击穿的三种做法))。要求数据准确时用互斥锁,能接受短暂的旧值、更看重响应时间时用逻辑过期。
|
||||
|
||||
### 缓存雪崩
|
||||
|
||||
@@ -397,6 +409,8 @@ public class LRU<K, V> implements Iterable<K> {
|
||||
}
|
||||
```
|
||||
|
||||
上面的实现不是线程安全的:get() 也会移动链表中的节点,多个线程同时访问会破坏链表,所以读操作也要加锁。简单的做法是 get() 和 put() 使用同一把锁,或者用 Collections.synchronizedMap() 包装上面的 LinkedHashMap。并发高时用 Caffeine,它不在每次读取时调整顺序,而是把访问记录写入缓冲区,再批量调整,读操作不需要获取全局锁。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
以下实验的环境均为 Apple M1 Pro(10 核)、macOS,Redis 7.4.1 使用默认配置,客户端和服务器在同一台机器上。
|
||||
@@ -522,6 +536,62 @@ update-then-delete stale 542/1000 (54.2%)
|
||||
update-then-delete + double delete stale 0/1000 (0.0%)
|
||||
```
|
||||
|
||||
### A5 缓存击穿的三种做法
|
||||
|
||||
MySQL 8.0.44,Redis 7.4.1,Python 3.13(pymysql、redis-py)。数据库查询为 SELECT price, SLEEP(0.05) FROM cache_demo WHERE id = 1,热点键刚过期时 100 个线程同时发起请求,统计查询数据库的次数和每个请求的耗时,逻辑过期的旧价格为 99。每种做法运行两轮:
|
||||
|
||||
```python
|
||||
def mutex():
|
||||
while True:
|
||||
v = r.get(KEY)
|
||||
if v is not None:
|
||||
return int(v)
|
||||
if r.set(LOCK, 1, nx=True, ex=3): # 只有一个请求拿到锁
|
||||
try:
|
||||
v = r.get(KEY) # 再查一次,可能别人已经写回
|
||||
if v is None:
|
||||
v = query_db(); r.set(KEY, v, ex=60)
|
||||
return int(v)
|
||||
finally:
|
||||
r.delete(LOCK)
|
||||
time.sleep(0.01) # 没拿到锁,稍后重新读缓存
|
||||
|
||||
def logical():
|
||||
d = json.loads(r.get(KEY)) # 值中带逻辑过期时间
|
||||
if d['expire_at'] < time.time() and r.set(LOCK, 1, nx=True, ex=3):
|
||||
threading.Thread(target=rebuild).start() # 后台重建,完成后删除锁
|
||||
return d['price'] # 不等待,直接返回
|
||||
```
|
||||
|
||||
```text
|
||||
不加保护 查询数据库 68 次,耗时中位数 157.8 ms,最大 181.4 ms,返回旧值 0 次
|
||||
互斥锁 查询数据库 1 次,耗时中位数 88.2 ms,最大 95.8 ms,返回旧值 0 次
|
||||
逻辑过期 查询数据库 1 次,耗时中位数 13.8 ms,最大 19.8 ms,返回旧值 100 次
|
||||
不加保护 查询数据库 100 次,耗时中位数 408.4 ms,最大 444.2 ms,返回旧值 0 次
|
||||
互斥锁 查询数据库 1 次,耗时中位数 102.0 ms,最大 118.5 ms,返回旧值 0 次
|
||||
逻辑过期 查询数据库 1 次,耗时中位数 11.5 ms,最大 18.6 ms,返回旧值 100 次
|
||||
```
|
||||
|
||||
### A6 布隆过滤器的误判率
|
||||
|
||||
Python 3.13 模拟:插入 user:0~user:999999,再查询 user:1000000~user:1999999 这 100 万个不存在的 Key。哈希值取 MD5 的两个 64 位整数 h1、h2,第 i 个位置为 (h1 + i × h2) mod m:
|
||||
|
||||
```python
|
||||
def pos(key):
|
||||
d = hashlib.md5(key.encode()).digest()
|
||||
h1, h2 = int.from_bytes(d[:8], 'little'), int.from_bytes(d[8:], 'little')
|
||||
return [(h1 + i * h2) % m for i in range(k)]
|
||||
```
|
||||
|
||||
```text
|
||||
n=1000000 m/n=10 k=7 位数组 1.19 MiB:实测误判率 0.8199%,公式 0.8194%
|
||||
n=1000000 m/n=10 k=3 位数组 1.19 MiB:实测误判率 1.7588%,公式 1.7411%
|
||||
n=1000000 m/n=5 k=3 位数组 0.60 MiB:实测误判率 9.2104%,公式 9.1849%
|
||||
n=1000000 m/n=20 k=14 位数组 2.38 MiB:实测误判率 0.0065%,公式 0.0067%
|
||||
目标误判率 1.0%:每个 Key 9.59 位,k = 6.6,1 亿个 Key 约 114 MiB
|
||||
目标误判率 0.1%:每个 Key 14.38 位,k = 10.0,1 亿个 Key 约 171 MiB
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 杨传辉. 大规模分布式存储系统:原理解析与架构实战 [M]. 机械工业出版社, 2013.
|
||||
@@ -533,5 +603,7 @@ update-then-delete + double delete stale 0/1000 (0.0%)
|
||||
- [Redis: UNLINK](https://redis.io/docs/latest/commands/unlink/)
|
||||
- [Redis CLI](https://redis.io/docs/latest/develop/tools/cli/)
|
||||
- [Scaling Memcache at Facebook](https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf)
|
||||
- [Redis: Bloom filter](https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/)
|
||||
- [Caffeine: Design](https://github.com/ben-manes/caffeine/wiki/Design)
|
||||
- [Caffeine Wiki](https://github.com/ben-manes/caffeine/wiki)
|
||||
- [Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web](https://dl.acm.org/doi/10.1145/258533.258660)
|
||||
|
||||
Reference in New Issue
Block a user