Become a sponsor

本章详细描述 的缓存架构设计,包括两级缓存策略(进程内缓存 + Redis 缓存)、缓存应用场景、失效策略和代码实现。
缓存分层设计
采用两级缓存架构:第一级为进程内字典缓存(dict_util),第二级为 Redis 分布式缓存。两级缓存配合使用,在保证数据一致性的同时最大化查询性能。
┌─────────────────────────────────────────────────────────────────┐
│ 请求处理流程 │
│ │
│ ┌──────────┐ 命中 ┌──────────┐ 命中 ┌───────────┐ │
│ │ 业务代码 │ ─────────→ │ L1 缓存 │ ────────→ │ 返回结果 │ │
│ │ │ │ (进程内) │ │ │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ 命中 ┌───────────┐ │
│ │ │ L2 缓存 │ ────────→ │ 写回 L1 │ │
│ │ │ (Redis) │ │ 返回结果 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ ┌───────────┐ │
│ │ │ 数据库 │ ────────→ │ 写回 L1 │ │
│ │ │ (MySQL) │ │ 写回 L2 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │
│ ┌──────────┐ │
│ │ 缓存失效 │ → 清除 L1 + 删除 L2 │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘| 维度 | L1 进程内缓存 | L2 Redis 缓存 |
|---|---|---|
| 存储位置 | Python 进程内存 | Redis 服务器 |
| 访问速度 | 纳秒级(字典查找) | 毫秒级(网络 I/O) |
| 数据共享 | 仅当前进程 | 所有进程/实例共享 |
| 持久化 | 进程重启丢失 | 持久化可配置 |
| 容量限制 | 受进程内存限制 | 受 Redis 内存限制 |
| 适用场景 | 高频读取、低变更数据 | 跨进程共享、分布式场景 |
dict_util 模块实现了带 TTL 的进程内字典缓存。数据首次查询时从 Redis 加载到进程内存,后续查询直接从内存读取,TTL 过期后自动重新加载。
# src/utils/dict_util.py
import time
import json
from typing import Dict
# 进程内缓存:{cache_key: (data, expire_timestamp)}
_cache: Dict[str, tuple] = {}
CACHE_TTL = 300 # 默认 5 分钟
def get_dict_map(dict_code: str) -> Dict[str, str]:
"""获取数据字典的 {value: label} 映射(进程内缓存 + Redis 二级缓存)"""
cache_key = f"dict:{dict_code}"
now = time.time()
# L1: 进程内缓存命中
if cache_key in _cache:
data, expire_at = _cache[cache_key]
if now < expire_at:
return data
del _cache[cache_key]
# L2: Redis 缓存
import asyncio
from core.redis_client import get_redis
redis = get_redis()
cached = asyncio.get_event_loop().run_until_complete(redis.get(cache_key))
if cached:
data = json.loads(cached)
_cache[cache_key] = (data, now + CACHE_TTL)
return data
# 数据库查询(L3 回源)
from modules.dictionary.dict_item.repository import dict_item_repo
items = dict_item_repo.get_all(dict_code=dict_code)
data = {str(item.value): item.label for item in items}
# 写回两级缓存
_cache[cache_key] = (data, now + CACHE_TTL)
asyncio.get_event_loop().run_until_complete(
redis.set(cache_key, json.dumps(data, ensure_ascii=False), ex=CACHE_TTL)
)
return data
def invalidate_dict_cache(dict_code: str):
"""失效指定字典的缓存(L1 + L2)"""
cache_key = f"dict:{dict_code}"
# 清除 L1(进程内缓存)
_cache.pop(cache_key, None)
# 清除 L2(Redis 缓存)
from core.redis_client import get_redis
import asyncio
redis = get_redis()
asyncio.get_event_loop().run_until_complete(redis.delete(cache_key))| 场景 | 调用方式 | 说明 |
|---|---|---|
| 枚举显示名 | get_dict_map('status_type') | 序列化时将值转为显示名 |
| 表单下拉 | get_dict_map('gender') | 前端获取字典项列表 |
| 业务校验 | value in get_dict_map('xxx') | 校验值是否在字典范围内 |
字典数据变更时(新增/编辑/删除字典项),主动清除两级缓存:
# src/modules/dictionary/dict_item/service.py
class DictItemService(BaseService[DictItem]):
def add(self, request, data) -> R:
result = super().add(request, data)
if result.code == 0:
invalidate_dict_cache(data.dict_code) # 新增成功后失效缓存
return result
def update(self, request, data) -> R:
result = super().update(request, data)
if result.code == 0:
invalidate_dict_cache(data.dict_code) # 更新成功后失效缓存
return result
def delete(self, ids) -> R:
# 删除前先获取 dict_code(删除后无法从 data 中获取)
item = self.repo.get_by_id(ids)
dict_code = item.dict_code if item else None
result = super().delete(ids)
if result.code == 0 and dict_code:
invalidate_dict_cache(dict_code) # 删除成功后失效缓存
return result# src/core/redis_client.py
from contextvars import ContextVar
from redis.asyncio import Redis
_redis_ctx: ContextVar[Redis] = ContextVar('_redis_ctx')
def get_redis() -> Redis:
"""获取当前请求的 Redis 客户端"""
return _redis_ctx.get()contextvarsredis.asyncio)与 FastAPI 异步模型无缝集成| 应用场景 | 数据结构 | Key 格式 | TTL | 说明 |
|---|---|---|---|---|
| JWT 黑名单 | String | jwt:blacklist:{token_hash} | Token 剩余有效期 | 登出时将 Token 加入黑名单 |
| 登录失败锁 | String | login:lock:{username} | 600 秒 | 连续 5 次失败后锁定 |
| 登录失败计数 | String (Lua) | login:fail:{username} | 300 秒 | 原子递增 + 首次设置 TTL |
| 滑动窗口限流 | String (Lua) | rate:{ip}:{window_index} | 2 倍窗口时长 | 滑动窗口计数器 |
| 数据字典缓存 | String (JSON) | dict:{dict_code} | 300 秒 | 字典项 {value: label} 映射 |
| 权限列表缓存 | String (JSON) | perm:user:{user_id} | 变更时失效 | 用户权限字符串列表 |
| 在线用户 | String | online:user:{user_id} | Token 有效期 | 在线用户 Token 存储 |
| 验证码 | String | captcha:{uuid} | 120 秒 | 图形验证码文本 |
# src/core/redis_client.py
_INCR_EXPIRE_LUA = """
local count = redis.call('incr', KEYS[1])
if count == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
return count
"""
async def incr_with_expire(redis, key: str, seconds: int) -> int:
"""原子执行 incr,并在 key 首次创建时一并设置过期时间"""
return await redis.eval(_INCR_EXPIRE_LUA, 1, key, seconds)为什么需要 Lua 脚本?
incr 和 expire 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留,导致基于 Redis 的限流/登录锁定永久封禁。Lua 脚本保证原子性,消除此风险。
_SLIDING_WINDOW_LUA = """
local cur = redis.call('incr', KEYS[1])
if cur == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
local prev = tonumber(redis.call('get', KEYS[2]) or '0')
local elapsed = tonumber(ARGV[3]) - tonumber(ARGV[2]) * tonumber(ARGV[1])
local f = elapsed / tonumber(ARGV[1])
if f < 0 then f = 0 elseif f > 1 then f = 1 end
local effective = prev * (1 - f) + cur
return effective
"""滑动窗口限流通过当前窗口与上一窗口的计数按时间比例加权,消除固定窗口边界处的 2x 突发问题。
| 策略 | 说明 | 适用场景 | 应用 |
|---|---|---|---|
| Cache-Aside | 先查缓存,未命中查 DB,写回缓存 | 读多写少 | 数据字典、权限列表 |
| Write-Through | 写操作同时更新缓存 | 数据一致性要求高 | 字典项变更时主动失效 |
| Write-Behind | 写操作只更新缓存,异步写 DB | 写入性能要求高 | 未使用 |
| TTL 过期 | 缓存自动过期 | 允许短暂不一致 | 登录锁、限流计数 |
| 主动失效 | 数据变更时主动清除缓存 | 数据一致性要求高 | 字典缓存、权限缓存 |
数据变更时主动清除相关缓存,保证数据一致性:
# 字典项变更 → 清除字典缓存
invalidate_dict_cache(dict_code)
# 角色菜单变更 → 清除该角色下所有用户的权限缓存
invalidate_perm_cache(user_id)
# 用户角色变更 → 清除该用户的权限缓存
invalidate_perm_cache(user_id)对于允许短暂不一致的数据,使用 TTL 自动过期:
数据字典缓存:TTL 300 秒(5 分钟)
登录失败计数:TTL 300 秒(5 分钟)
滑动窗口限流:TTL 2 倍窗口时长
验证码:TTL 120 秒(2 分钟)缓存穿透
当查询一个不存在的数据时,缓存和数据库都不会命中,导致每次请求都穿透到数据库。对于字典查询,如果 dict_code 不存在,应缓存空结果(空字典),避免反复查询数据库。
def get_dict_map(dict_code: str) -> Dict[str, str]:
# ... 缓存查询逻辑 ...
items = dict_item_repo.get_all(dict_code=dict_code)
data = {str(item.value): item.label for item in items}
# 即使 data 为空字典,也写入缓存,防止穿透
_cache[cache_key] = (data, now + CACHE_TTL)
return dataRedis 缓存的运行状态可通过以下方式监控:
# 查看 Redis 缓存统计
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# 查看指定 key 的 TTL
redis-cli TTL "dict:status_type"
# 查看所有字典缓存 key
redis-cli KEYS "dict:*"采用进程内缓存(L1)+ Redis 缓存(L2)的两级缓存架构。L1 缓存提供纳秒级访问速度,适合高频读取的字典数据;L2 缓存提供跨进程数据共享,适合分布式场景。缓存失效采用主动失效 + TTL 过期的混合策略,数据变更时主动清除缓存保证一致性,允许短暂不一致的场景使用 TTL 自动过期。Lua 脚本保证 Redis 操作的原子性,防止进程崩溃导致的缓存异常。