mirror of
https://codeup.aliyun.com/64f7d6b8ce01efaafef1e678/coal/coal.git
synced 2026-07-24 22:57:10 +08:00
13 KiB
13 KiB
用户、角色、权限、资源缓存管理分析
一、当前缓存架构概览
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()方法
原因:
- 权限 → 资源 → 用户 的三层关系
- 每一层的缓存都需要独立维护
- 清除时需要记住所有依赖关系
影响:
// 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() 使用缓存但依赖多个数据源
代码:
@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):
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视图集合
-- 视图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实现方案
// 简化的缓存查询,直接从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);
}
}
五、缓存清除统一方案
// 创建统一的缓存清除接口
@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)
第2步:创建新的缓存查询Service
// PermissionCacheService 使用SQL视图
第3步:创建统一的缓存清除Service
// 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 | 自动同步 |
八、建议实施方案
优先级:
- 必需:创建
v_user_resources和v_user_permissions视图 - 必需:创建
CacheInvalidationService统一清除接口 - 推荐:使用SQL直接替换复杂的权限计算逻辑
- 可选:研究是否可以移除部分RabbitMQ消息驱动的缓存更新
投入:
- SQL视图创建:2-3小时
- PermissionCacheService实现:2-3小时
- 替换调用点:4-6小时
- 测试验证:2-3小时
- 总计:约1-2个工作日
这个方案可以显著降低缓存管理的复杂度,提高系统性能和可维护性。