mirror of
https://codeup.aliyun.com/64f7d6b8ce01efaafef1e678/coal/coal.git
synced 2026-07-24 22:57:10 +08:00
445 lines
13 KiB
Markdown
445 lines
13 KiB
Markdown
# 用户、角色、权限、资源缓存管理分析
|
||
|
||
## 一、当前缓存架构概览
|
||
|
||
### 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(),容易遗漏
|
||
|
||
**原因**:
|
||
- 没有统一的缓存清除入口
|
||
- 权限更新后需要清除权限、资源、用户三个模块的缓存
|
||
- 没有事务性保证
|
||
|
||
### 问题4:RabbitMQ消息驱动的延迟清除
|
||
**代码**(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个工作日
|
||
|
||
这个方案可以显著降低缓存管理的复杂度,提高系统性能和可维护性。
|