Skip to content

缓存架构设计

本章详细描述 的缓存架构设计,包括两级缓存策略(进程内缓存 + Redis 缓存)、缓存应用场景、失效策略和代码实现。

两级缓存架构

缓存分层设计

采用两级缓存架构:第一级为进程内字典缓存(dict_util),第二级为 Redis 分布式缓存。两级缓存配合使用,在保证数据一致性的同时最大化查询性能。

┌─────────────────────────────────────────────────────────────────┐
│ 请求处理流程 │
│ │
│ ┌──────────┐ 命中 ┌──────────┐ 命中 ┌───────────┐ │
│ │ 业务代码 │ ─────────→ │ L1 缓存 │ ────────→ │ 返回结果 │ │
│ │ │ │ (进程内) │ │ │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ 命中 ┌───────────┐ │
│ │ │ L2 缓存 │ ────────→ │ 写回 L1 │ │
│ │ │ (Redis) │ │ 返回结果 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ ┌───────────┐ │
│ │ │ 数据库 │ ────────→ │ 写回 L1 │ │
│ │ │ (MySQL) │ │ 写回 L2 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │
│ ┌──────────┐ │
│ │ 缓存失效 │ → 清除 L1 + 删除 L2 │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘

缓存层级对比

维度L1 进程内缓存L2 Redis 缓存
存储位置Python 进程内存Redis 服务器
访问速度纳秒级(字典查找)毫秒级(网络 I/O)
数据共享仅当前进程所有进程/实例共享
持久化进程重启丢失持久化可配置
容量限制受进程内存限制受 Redis 内存限制
适用场景高频读取、低变更数据跨进程共享、分布式场景

L1 缓存:进程内字典缓存

实现原理

dict_util 模块实现了带 TTL 的进程内字典缓存。数据首次查询时从 Redis 加载到进程内存,后续查询直接从内存读取,TTL 过期后自动重新加载。

python
# 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')校验值是否在字典范围内

缓存失效

字典数据变更时(新增/编辑/删除字典项),主动清除两级缓存:

python
# 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

L2 缓存:Redis 缓存

Redis 连接管理

python
# 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()
  • 每个请求通过中间件注入独立的 Redis 客户端到 contextvars
  • 连接池在应用启动时创建,所有请求共享
  • 异步客户端(redis.asyncio)与 FastAPI 异步模型无缝集成

缓存应用场景

应用场景数据结构Key 格式TTL说明
JWT 黑名单Stringjwt:blacklist:{token_hash}Token 剩余有效期登出时将 Token 加入黑名单
登录失败锁Stringlogin: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}变更时失效用户权限字符串列表
在线用户Stringonline:user:{user_id}Token 有效期在线用户 Token 存储
验证码Stringcaptcha:{uuid}120 秒图形验证码文本

Lua 脚本

原子递增 + 过期

python
# 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 脚本?

increxpire 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留,导致基于 Redis 的限流/登录锁定永久封禁。Lua 脚本保证原子性,消除此风险。

滑动窗口限流

python
_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 过期缓存自动过期允许短暂不一致登录锁、限流计数
主动失效数据变更时主动清除缓存数据一致性要求高字典缓存、权限缓存

缓存失效策略

主动失效

数据变更时主动清除相关缓存,保证数据一致性:

python
# 字典项变更 → 清除字典缓存
invalidate_dict_cache(dict_code)

# 角色菜单变更 → 清除该角色下所有用户的权限缓存
invalidate_perm_cache(user_id)

# 用户角色变更 → 清除该用户的权限缓存
invalidate_perm_cache(user_id)

TTL 自动过期

对于允许短暂不一致的数据,使用 TTL 自动过期:

数据字典缓存:TTL 300 秒(5 分钟)
登录失败计数:TTL 300 秒(5 分钟)
滑动窗口限流:TTL 2 倍窗口时长
验证码:TTL 120 秒(2 分钟)

缓存穿透防护

缓存穿透

当查询一个不存在的数据时,缓存和数据库都不会命中,导致每次请求都穿透到数据库。对于字典查询,如果 dict_code 不存在,应缓存空结果(空字典),避免反复查询数据库。

python
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 data

缓存监控

Redis 缓存的运行状态可通过以下方式监控:

  1. Redis INFO:连接数、内存使用、命中率
  2. 应用日志:缓存命中/未命中日志(DEBUG 级别)
  3. Redis TTL:检查 key 的剩余有效期
bash
# 查看 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 操作的原子性,防止进程崩溃导致的缓存异常。

小蚂蚁云团队 · 提供技术支持