# 用户、角色、权限、资源缓存管理分析 ## 一、当前缓存架构概览 ### 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 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 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 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个工作日 这个方案可以显著降低缓存管理的复杂度,提高系统性能和可维护性。