因为@lru_cache是同步装饰器,无法处理async def返回的协程对象,直接使用会导致未await警告或静默错误。

为什么不能直接用 @lru_cache 装饰异步函数
因为 @lru_cache 是同步装饰器,它内部调用的是普通函数对象,而 async def 定义的函数返回的是协程对象(coroutine),不是可哈希的返回值。直接套用会导致缓存命中时返回一个未 await 的协程,后续代码报 RuntimeWarning: coroutine 'xxx' was never awaited,甚至静默出错。
aioredis 连接池必须显式配置超时与复用
默认的 aioredis.from_url() 不启用连接池复用,每次 get/set 都可能新建连接,高并发下很快耗尽文件描述符或触发 Redis 的 maxclients 限制。正确做法是:
- 用
aioredis.ConnectionPool.from_url(..., max_connections=50)显式创建带上限的连接池 - 设置
socket_connect_timeout=2和socket_keepalive=True,避免网络抖动导致连接卡死 - 在 FastAPI 的 lifespan 中初始化单例连接池,而非每次请求都 new 一个 client
漏掉 max_connections 是线上 Redis 连接数暴增的最常见原因。
本地 LRU + 远程 Redis 的两级缓存怎么协同
纯 Redis 缓存有网络延迟,纯内存 LRU 又无法跨进程共享。合理做法是:本地缓存高频、低变更的键(如配置项、枚举映射),Redis 缓存业务数据(如用户 profile、问答对)。关键点在于:
- 本地层用
functools.lru_cache(maxsize=128),仅包裹纯函数(参数全为不可变类型) - Redis 层键名必须带业务前缀和哈希(如
f"qa:{hashlib.md5(q.encode()).hexdigest()[:8]}"),防止冲突 - 两级缓存 TTL 要错开:本地设 60 秒,Redis 设 3600 秒;本地失效后仍可从 Redis 回源,避免雪崩
- 写操作必须穿透两级:先删本地缓存(
func.cache_clear()),再删/写 Redis
缓存穿透防护不能只靠“狗牌”,得结合布隆过滤器
所谓“狗牌”(cache stampede protection)指当缓存失效时,只放行一个请求去回源,其余等待——这能防雪崩,但挡不住恶意构造的不存在 key(如 /user?id=-1)。此时所有请求都会击穿到 DB。
真正有效的方案是:
- 在接入层(如 FastAPI middleware)用
pybloom_live维护一个轻量布隆过滤器,预热已存在 key 的集合 - 查询前先查布隆过滤器:
if not bloom.might_contain(key): return None - 布隆过滤器本身可缓存在 Redis 中,定期更新,误判率控制在 1% 以内即可
没有布隆过滤器的“狗牌”,只是把缓存穿透转化成了数据库慢查询。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











