Files
coal/doc/cache_management_analysis.md

13 KiB
Raw Permalink Blame History

用户、角色、权限、资源缓存管理分析

一、当前缓存架构概览

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(),容易遗漏

原因

  • 没有统一的缓存清除入口
  • 权限更新后需要清除权限、资源、用户三个模块的缓存
  • 没有事务性保证

问题4RabbitMQ消息驱动的延迟清除

代码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 自动同步

八、建议实施方案

优先级

  1. 必需:创建v_user_resourcesv_user_permissions视图
  2. 必需:创建CacheInvalidationService统一清除接口
  3. 推荐使用SQL直接替换复杂的权限计算逻辑
  4. 可选研究是否可以移除部分RabbitMQ消息驱动的缓存更新

投入

  • SQL视图创建2-3小时
  • PermissionCacheService实现2-3小时
  • 替换调用点4-6小时
  • 测试验证2-3小时
  • 总计约1-2个工作日

这个方案可以显著降低缓存管理的复杂度,提高系统性能和可维护性。