Files
2026-09-12 23:12:15 +08:00

155 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 权限缓存
:::tip 说明
通过 Redis 实现用户权限和菜单的缓存,采用用户级缓存 + 全局版本号双层策略。源码位于 `src/utils/perm_cache.py`,供 `src/modules/system/menu/service.py` 中的权限查询函数调用。
:::
## 缓存策略
采用「用户级缓存 + 全局版本号」双层策略:
```
1. 每个用户的缓存 Key 带版本号后缀
2. 版本号存全局 Key(perm:version)
3. 菜单变更时递增全局版本号,所有用户缓存自然失效
4. 用户角色变更时,直接删除该用户缓存
5. 角色菜单变更时,查询该角色下所有用户 ID,批量删除缓存
```
## 缓存 Key 结构
```python
_VERSION_KEY = 'perm:version' # 全局版本号
_PERM_PREFIX = 'perm:list:' # 用户权限列表
_MENU_PREFIX = 'perm:menu:' # 用户菜单树
_USER_ACTIVE_PREFIX = 'user:active:' # 用户有效状态
```
Key 格式示例:
```
perm:version → 5
perm:list:1001:5 → ["sys:user:page", "sys:user:add", ...]
perm:menu:1001:5 → [{id: 1, name: "系统管理", children: [...]}]
user:active:1001 → "1"
```
## 权限缓存读写
### get_cached_permissions()
```python
async def get_cached_permissions(user_id: int):
"""获取缓存的用户权限列表,未命中返回 None"""
redis = get_redis()
ver = await _get_version()
key = f'{_PERM_PREFIX}{user_id}:{ver}'
data = await redis.get(key)
if data is not None:
return json.loads(data)
return None
```
### set_cached_permissions()
```python
async def set_cached_permissions(user_id: int, perm_list: list):
"""缓存用户权限列表(空列表不缓存,避免缓存空壳后无法感知后续数据变更)"""
if not perm_list:
return
redis = get_redis()
ver = await _get_version()
key = f'{_PERM_PREFIX}{user_id}:{ver}'
await redis.set(key, json.dumps(perm_list), ex=_CACHE_TTL)
```
## 缓存失效
### 菜单变更
```python
async def invalidate_menu_change():
"""菜单增删改时调用,递增全局版本号使所有用户缓存失效"""
await _incr_version()
```
递增版本号后,旧版本的缓存 Key 不再被访问,自然过期(TTL 2小时)。
### 用户角色变更
```python
async def invalidate_user_perms(user_id: int):
"""用户角色变更时调用,删除该用户的权限和菜单缓存"""
redis = get_redis()
ver = await _get_version()
keys = [
f'{_PERM_PREFIX}{user_id}:{ver}',
f'{_MENU_PREFIX}{user_id}:{ver}',
]
await redis.delete(*keys)
```
### 角色菜单变更
```python
async def invalidate_role_perms(role_id: int):
"""角色菜单变更时调用,清理该角色下所有用户的权限和菜单缓存"""
# 查询该角色下所有用户 ID
user_ids = await asyncio.to_thread(_query_user_ids)
# 批量删除缓存
redis = get_redis()
ver = await _get_version()
keys = []
for uid in user_ids:
keys.append(f'{_PERM_PREFIX}{uid}:{ver}')
keys.append(f'{_MENU_PREFIX}{uid}:{ver}')
await redis.delete(*keys)
```
## 用户有效状态缓存
用于 `login_required` 每请求校验,避免每次请求都查库:
```python
_USER_ACTIVE_TTL = 300 # 5分钟
async def get_cached_user_active(user_id: int):
"""读取用户有效状态缓存;未命中返回 None(调用方需回源 DB 校验)"""
async def set_cached_user_active(user_id: int, active: bool):
"""写入用户有效状态缓存"""
async def invalidate_user_active(user_id: int):
"""用户状态/删除变更时调用,删除有效状态缓存"""
```
## 异常降级
所有缓存操作都包裹 try/except,Redis 不可用时降级直查 DB:
```python
except Exception as e:
logger.warning(f"读取权限缓存异常: {e}")
return None # 调用方回源 DB
```
删除失败时递增全局版本号孤立旧缓存(fail-closed):
```python
except Exception as e:
logger.error(f"清理用户权限缓存异常,降级递增全局版本号: {e}")
try:
await _incr_version()
except Exception as e2:
logger.error(f"降级递增版本号失败: {e2}")
```
:::tip 温馨提示
权限缓存的 TTL 为 2小时,用户有效状态缓存 TTL 为 5分钟。正常情况下缓存由业务操作即时失效,TTL 仅作为兜底防止 Redis 故障时缓存永久残留。
:::
## 总结
权限缓存采用用户级缓存 + 全局版本号双层策略,菜单变更通过递增版本号使所有缓存自然失效,无需遍历删除。所有缓存操作都有异常降级机制,Redis 不可用时自动回源数据库查询。