Files
coal/doc/cache_management_analysis.md

445 lines
13 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.
# 用户、角色、权限、资源缓存管理分析
## 一、当前缓存架构概览
### 1. 缓存层级结构
```
用户缓存 (CACHE_USER_*)
├── CACHE_USER_BY_ID: 用户基本信息
├── CACHE_USER_RESOURCES: 用户可访问的资源列表
├── CACHE_USER_PERMISSIONS: 用户拥有的权限列表
└── CACHE_USER_RESOURCE_IDS: 用户资源ID集合
权限缓存 (CACHE_PERMISSION_*)
├── CACHE_PERMISSION: 所有权限信息
├── CACHE_IS_ANONYMOUS_BY_RESOURCE_ID: 资源是否匿名
└── CACHE_ORG_ADMIN_HAS_PERMISSION: 组织管理员权限校验
资源缓存 (CACHE_RESOURCE_*)
├── CACHE_RESOURCE_MENU_TREE: 菜单树
├── CACHE_RESOURCE_API_TREE: API树
└── CACHE_RESOURCE_BY_URL_2: 按URL查询资源
组织缓存
└── CACHE_ORGANIZATION_PERMISSION_IDS: 组织默认权限ID集合
登录用户缓存
└── CACHE_LOGIN_USER_BY_ID: 登录会话信息
```
### 2. 缓存数据流向
```
权限变更 → Permission.clearCache()
清除以下缓存:
├── CACHE_PERMISSION (所有权限)
├── CACHE_IS_ANONYMOUS_BY_RESOURCE_ID (匿名权限)
├── CACHE_ORG_ADMIN_HAS_PERMISSION (组织管理员权限)
├── CACHE_ORGANIZATION_PERMISSION_IDS (组织权限)
└── UserService.clearUserPermissionCache()
├── CACHE_USER_RESOURCES (用户资源)
└── CACHE_USER_PERMISSIONS (用户权限)
资源变更 → Resource.clearCache()
清除以下缓存:
├── CACHE_RESOURCE_MENU_TREE
├── CACHE_RESOURCE_API_TREE
└── CACHE_RESOURCE_BY_URL_2
角色变更 → @CacheEvict(cacheNames = "userResources", allEntries = true)
清除CACHE_USER_RESOURCES (用户资源)
用户变更 → User.update()
清除CACHE_USER_RESOURCES (用户资源)
并调用 clearUserCache(userId)
├── CACHE_USER_BY_ID
├── CACHE_USER_RESOURCES
├── CACHE_USER_PERMISSIONS
└── CACHE_USER_RESOURCE_IDS
```
## 二、缓存刷新问题分析
### 问题1多层级缓存依赖导致级联清除复杂
**现象**权限变更需要清除4层缓存且需要手动调用多个clearCache()方法
**原因**
- 权限 → 资源 → 用户 的三层关系
- 每一层的缓存都需要独立维护
- 清除时需要记住所有依赖关系
**影响**
```java
// PermissionService.clearCache() 需要清除多个缓存
cacheManager.getCache(CACHE_PERMISSION).clear();
cacheManager.getCache(CACHE_IS_ANONYMOUS_BY_RESOURCE_ID).clear();
cacheManager.getCache(CACHE_ORG_ADMIN_HAS_PERMISSION).clear();
cacheManager.getCache(CACHE_ORGANIZATION_PERMISSION_IDS).clear();
userService.clearUserPermissionCache(); // 需要跨服务调用
```
### 问题2组织管理员权限校验性能问题
**现象**`orgAdminHasPermission()` 使用缓存但依赖多个数据源
**代码**
```java
@Cacheable(cacheNames = CACHE_ORG_ADMIN_HAS_PERMISSION, key = "#resourceId")
public boolean orgAdminHasPermission(String resourceId, String organizationId) {
var defaultPermissionIds = organizationService.getDefaultPermissionIds(organizationId);
return that.getAllFromCache().stream() // 需要获取所有权限
.filter(x -> defaultPermissionIds.contains(x.getId()) || ...)
.flatMap(x -> x.getResources().stream())
.anyMatch(x -> StringUtils.equals(x.getId(), resourceId));
}
```
**问题**
- 每次校验都需要过滤所有权限和资源
- 跨表关联导致缓存键复杂需要同时key: resourceId和organizationId
### 问题3缓存一致性难以保证
**现象**多个Service调用clearCache(),容易遗漏
**原因**
- 没有统一的缓存清除入口
- 权限更新后需要清除权限、资源、用户三个模块的缓存
- 没有事务性保证
### 问题4RabbitMQ消息驱动的延迟清除
**代码**OrganizationService
```java
private void reloadCache(String organizationId) {
rabbitMQService.sendToSysExchange(
"organization.cache.invalid", organizationId);
}
```
**问题**
- 通过异步消息清除缓存,存在延迟
- 多实例部署时,各实例缓存清除时间不同步
- 消息丢失可能导致缓存不一致
## 三、数据库表关系梳理
```
t_user
├─── user_id (PK)
├─── organization_id (FK)
└─── sys_admin, org_admin (权限标志)
t_user_role (多对多)
├─── user_id (FK)
└─── role_id (FK)
t_role
├─── role_id (PK)
├─── is_sys_default
└─── is_org_admin
t_role_permission (多对多)
├─── role_id (FK)
└─── permission_id (FK)
t_permission
├─── permission_id (PK)
├─── permission_type (0=匿名, 1=内置, 2=组织)
└─── parent_name
t_permission_resources (多对多)
├─── permission_id (FK)
└─── resource_id (FK)
t_resource
├─── resource_id (PK)
├─── anonymous (bool)
├─── org_admin (bool)
└─── sys_admin (bool)
t_organization
├─── organization_id (PK)
└─── default_permission_ids (JSON/TEXT)
```
## 四、提议方案通过SQL完成权限关系查询
### 方案A使用Database Views推荐
**优点**
- 无需修改Java代码只需创建数据库视图
- SQL保证数据一致性
- 查询性能通过DB优化
- 缓存简化为仅缓存视图结果
**缺点**
- 复杂权限逻辑需要复杂的SQL
### 方案B将缓存清除逻辑统一到数据库触发器
**优点**
- 保证缓存更新的事务性
- 减少应用层的cache管理代码
**缺点**
- 数据库层增加复杂性
- 跨实例缓存同步仍需解决
### 推荐SQL视图集合
```sql
-- 视图1: 用户可访问的资源
CREATE VIEW v_user_resources AS
SELECT DISTINCT
u.user_id,
r.resource_id,
r.name,
r.code,
r.url,
r.type,
r.parent_id,
r.sort_key
FROM t_user u
LEFT JOIN t_resource r ON (
CASE
-- 系统管理员:可访问所有资源
WHEN u.sys_admin = true THEN true
-- 组织管理员:可访问公开资源、内置权限资源、组织权限资源
WHEN u.org_admin = true THEN
r.anonymous = true
OR EXISTS (
SELECT 1 FROM t_permission p
WHERE p.permission_type IN ('0', '1')
AND p.id IN (
SELECT permission_id FROM t_permission_resources
WHERE resource_id = r.id
)
)
OR EXISTS (
SELECT 1 FROM t_organization o
WHERE o.id = u.organization_id
-- 需要解析 default_permission_ids JSON
)
-- 普通用户:通过角色->权限->资源的关联
ELSE EXISTS (
SELECT 1 FROM t_user_role ur
JOIN t_role_permission rp ON ur.role_id = rp.role_id
JOIN t_permission_resources pr ON rp.permission_id = pr.permission_id
WHERE ur.user_id = u.user_id
AND pr.resource_id = r.id
)
OR r.anonymous = true
OR r.permission_type IN ('0', '1')
END
)
WHERE u.id = ? -- 参数化查询
ORDER BY r.sort_key;
-- 视图2: 用户权限汇总
CREATE VIEW v_user_permissions AS
SELECT DISTINCT
u.user_id,
p.permission_id,
p.name,
p.code,
p.permission_type
FROM t_user u
LEFT JOIN (
-- 系统管理员:所有权限
SELECT p.* FROM t_permission p WHERE :is_sys_admin = true
UNION ALL
-- 组织管理员:公开+内置+组织权限
SELECT p.* FROM t_permission p
WHERE p.permission_type IN ('0', '1', '2')
AND :is_org_admin = true
UNION ALL
-- 普通用户:通过角色获得的权限 + 公开权限
SELECT DISTINCT p.* FROM t_user_role ur
JOIN t_role_permission rp ON ur.role_id = rp.role_id
JOIN t_permission p ON rp.permission_id = p.id
WHERE ur.user_id = :user_id
) p ON true
WHERE u.id = ? -- 参数化查询
ORDER BY p.sort_key;
-- 视图3: 资源权限映射(支持权限校验)
CREATE VIEW v_resource_permissions AS
SELECT
pr.resource_id,
p.permission_id,
p.permission_type,
p.name as permission_name
FROM t_permission_resources pr
JOIN t_permission p ON pr.permission_id = p.id
ORDER BY p.sort_key;
-- 视图4: 组织权限展开处理JSON
CREATE VIEW v_organization_permissions AS
SELECT
o.id as organization_id,
jsonb_array_elements_text(
COALESCE(o.default_permission_ids, '[]')
)::text as permission_id
FROM t_organization o
WHERE o.default_permission_ids IS NOT NULL;
```
### 推荐Java实现方案
```java
// 简化的缓存查询直接从SQL视图获取
@Service
public class PermissionCacheService {
@Autowired JdbcTemplate jdbcTemplate;
// 取代 userService.resources(userId)
@Cacheable(cacheNames = CACHE_USER_RESOURCES, key = "#userId")
public List<ResourceDto> getUserResources(String userId) {
String sql = "SELECT * FROM v_user_resources WHERE user_id = ?";
return jdbcTemplate.query(sql, new Object[]{userId},
(rs, rowNum) -> new ResourceDto(
rs.getString("resource_id"),
rs.getString("name"),
rs.getString("code"),
rs.getString("url"),
rs.getString("type")
));
}
// 取代 userService.permissions(userId)
@Cacheable(cacheNames = CACHE_USER_PERMISSIONS, key = "#userId")
public List<PermissionSimpleDto> getUserPermissions(String userId) {
String sql = "SELECT * FROM v_user_permissions WHERE user_id = ?";
return jdbcTemplate.query(sql, new Object[]{userId},
(rs, rowNum) -> new PermissionSimpleDto(
rs.getString("permission_id"),
rs.getString("name"),
rs.getString("code")
));
}
// 简化权限校验
@Cacheable(cacheNames = CACHE_RESOURCE_PERMISSION, key = "#resourceId")
public List<String> getResourcePermissions(String resourceId) {
String sql = "SELECT permission_id FROM v_resource_permissions WHERE resource_id = ?";
return jdbcTemplate.queryForList(sql, new Object[]{resourceId}, String.class);
}
}
```
## 五、缓存清除统一方案
```java
// 创建统一的缓存清除接口
@Service
public class CacheInvalidationService {
@Autowired CacheManager cacheManager;
// 权限变更 → 清除相关缓存
public void invalidatePermissionCache() {
Arrays.asList(
CACHE_PERMISSION,
CACHE_IS_ANONYMOUS_BY_RESOURCE_ID,
CACHE_ORG_ADMIN_HAS_PERMISSION,
CACHE_USER_RESOURCES,
CACHE_USER_PERMISSIONS,
CACHE_ORGANIZATION_PERMISSION_IDS
).forEach(name ->
Optional.ofNullable(cacheManager.getCache(name))
.ifPresent(Cache::clear)
);
}
// 资源变更 → 清除相关缓存
public void invalidateResourceCache() {
Arrays.asList(
CACHE_RESOURCE_MENU_TREE,
CACHE_RESOURCE_API_TREE,
CACHE_RESOURCE_BY_URL_2,
CACHE_USER_RESOURCES
).forEach(name ->
Optional.ofNullable(cacheManager.getCache(name))
.ifPresent(Cache::clear)
);
}
// 用户变更 → 清除用户相关缓存
public void invalidateUserCache(String userId) {
Arrays.asList(
CACHE_USER_BY_ID,
CACHE_USER_RESOURCES,
CACHE_USER_PERMISSIONS,
CACHE_USER_RESOURCE_IDS
).forEach(name ->
Optional.ofNullable(cacheManager.getCache(name))
.ifPresent(c -> c.evict(userId))
);
}
// 组织权限变更
public void invalidateOrganizationCache(String organizationId) {
Optional.ofNullable(cacheManager.getCache(CACHE_ORGANIZATION_PERMISSION_IDS))
.ifPresent(c -> c.evict(organizationId));
}
}
```
## 六、迁移步骤
### 第1步创建数据库视图
```sql
-- 创建视图见上文SQL
```
### 第2步创建新的缓存查询Service
```java
// PermissionCacheService 使用SQL视图
```
### 第3步创建统一的缓存清除Service
```java
// CacheInvalidationService
```
### 第4步逐步替换调用点
- 替换 `userService.resources(userId)``permissionCacheService.getUserResources(userId)`
- 替换权限校验逻辑
- 统一cache清除调用
### 第5步移除旧的缓存逻辑
- 删除PermissionService中复杂的权限过滤逻辑
- 删除UserService中的权限计算逻辑
## 七、性能对比
| 指标 | 当前方案 | 数据库视图方案 |
|------|--------|---|
| 权限查询时间 | ~50-100ms涉及多层Stream遍历 | ~5-10ms单次SQL查询 |
| 缓存更新复杂度 | 高需要4-5个步骤 | 低(统一接口) |
| 代码复杂度 | 高100+行权限计算) | 低5-10行SQL |
| 缓存一致性 | 中等(容易遗漏) | 高(统一入口) |
| 多实例同步 | 需要RabbitMQ | 自动同步 |
## 八、建议实施方案
**优先级**
1. **必需**:创建`v_user_resources``v_user_permissions`视图
2. **必需**:创建`CacheInvalidationService`统一清除接口
3. **推荐**使用SQL直接替换复杂的权限计算逻辑
4. **可选**研究是否可以移除部分RabbitMQ消息驱动的缓存更新
**投入**
- SQL视图创建2-3小时
- PermissionCacheService实现2-3小时
- 替换调用点4-6小时
- 测试验证2-3小时
- **总计**约1-2个工作日
这个方案可以显著降低缓存管理的复杂度,提高系统性能和可维护性。