topic: backend
Redis 缓存实战
2021 年初,公司项目遇到了性能瓶颈。数据库被查询打爆了,老大让我研究一下 Redis 缓存。
问题背景
接口响应时间越来越慢,数据库 CPU 经常性 100%。
分析了一下,90% 的请求都是查询:
这些数据的特点:变动不频繁,但被频繁读取。
解决方案:Redis 缓存
第一次尝试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import redis import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_user(user_id): cache_key = f"user:{user_id}" cached = r.get(cache_key) if cached: return json.loads(cached) user = db.query(f"SELECT * FROM users WHERE id = {user_id}") r.setex(cache_key, 3600, json.dumps(user)) return user
|
效果:
爽!但随之而来的是各种问题。
遇到的问题
问题1:缓存穿透
大量请求查询不存在的数据,直接打到数据库。
比如:黑客用不存在的 user_id 疯狂请求。
解决:布隆过滤器 + 空值缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def get_user(user_id): if not bf.exists(f"user:{user_id}"): return None cache_key = f"user:{user_id}" cached = r.get(cache_key) if cached: return json.loads(cached) user = db.query(f"SELECT * FROM users WHERE id = {user_id}") if user: r.setex(cache_key, 3600, json.dumps(user)) else: r.setex(cache_key, 60, "") return user
|
问题2:缓存雪崩
大量缓存同时过期,请求全部打到数据库。
比如:高峰期大量缓存同时过期。
解决:随机过期时间 + 永不过期
1 2 3 4 5
| import random expire_time = random.randint(3600, 7200)
r.setex(cache_key, expire_time, json.dumps(user))
|
问题3:数据一致性
数据库更新了,缓存还是旧的。
解决:
- 先更新数据库,再删缓存(延迟删)
- 延迟双删
1 2 3 4 5 6 7 8 9 10
| def update_user(user_id, data): db.execute(f"UPDATE users SET ... WHERE id = {user_id}") r.delete(f"user:{user_id}") time.sleep(0.1) r.delete(f"user:{user_id}")
|
Redis 数据结构
Redis 不只是缓存,还有很多强大的数据结构。
String
最常用,存储字符串、JSON。
1 2 3 4 5 6
| r.set("key", "value") r.get("key")
r.incr("counter") r.decr("counter")
|
Hash
存储对象,类似于 Python 的 dict。
1 2 3 4 5 6
| r.hset("user:1", "name", "张三") r.hset("user:1", "age", "25") r.hgetall("user:1")
r.hmset("user:1", {"name": "张三", "age": "25"})
|
List
队列、列表。
1 2 3 4 5
| r.lpush("tasks", "task1") r.lpop("tasks")
r.lrange("tasks", 0, 10)
|
Set
去重、交集、并集。
1 2 3 4 5
| r.sadd("tags:python", "t1", "t2", "t3") r.smembers("tags:python")
r.sinter("tags:python", "tags:ai")
|
Sorted Set
排行榜、权重队列。
1 2 3 4 5
| r.zadd("leaderboard", {"user1": 100, "user2": 90})
r.zrevrange("leaderboard", 0, 10, withscores=True)
|
Redis 集群
单节点 Redis 不够用?考虑集群方案:
1. 主从复制
一主多从,读写分离。
1 2 3 4 5
| redis-server --port 6379
redis-server --port 6380 --replicaof 127.0.0.1 6379
|
2. Redis Sentinel
自动故障转移。
3. Redis Cluster
数据分片,自动路由。
总结
Redis 是后端开发的必备技能。
缓存用得好,接口快到飞起。但也要注意:
- 缓存不是银弹
- 一致性问题是难点
- 内存有限,合理规划
从那以后,我养成了习惯:任何频繁查询的数据,先考虑缓存。