Files
coal/JAVA_SERVICE_IMPLEMENTATION.md

17 KiB

Java Service 实现指南

基于性能验证结果,以下是推荐的 Java Service 实现方案。


1. PermissionCacheService (新建)

package cn.lihongjie.coal.permission.service;

import cn.lihongjie.coal.permission.dto.PermissionDto;
import cn.lihongjie.coal.resource.dto.ResourceDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 使用数据库视图优化的权限缓存服务
 * 
 * 性能指标 (经验证):
 * - sys_admin 权限: 0.77 ms
 * - sys_admin 资源: 39 ms  
 * - org_admin 资源: 70 ms
 * 
 * 缓存策略:
 * - 用户权限/资源缓存: TTL 5-30 分钟 (权限变更不频繁)
 * - 缓存失效: 权限/角色/资源变更时手动清除
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class PermissionCacheService {

    private final JdbcTemplate jdbcTemplate;
    private final PermissionMapper permissionMapper;
    private final ResourceMapper resourceMapper;

    /**
     * 获取用户的所有权限 (从 v_user_permissions 视图)
     * 
     * 执行时间: 0.77-15 ms (取决于权限量)
     * 缓存命中时: < 1 ms
     * 
     * @param userId 用户ID
     * @return 该用户可访问的所有权限列表
     */
    @Cacheable(
        value = "permission:user",  // 缓存名
        key = "#userId",             // 缓存 key: "user_id"
        cacheManager = "redisCacheManager"  // 使用 Redis
    )
    public List<PermissionDto> getUserPermissions(String userId) {
        log.debug("查询用户权限 [userId={}]", userId);
        
        String sql = "SELECT DISTINCT user_id, permission_id, name, code, permission_type, "
                   + "parent_name, status, sort_key "
                   + "FROM v_user_permissions "
                   + "WHERE user_id = ? "
                   + "ORDER BY sort_key, id";
        
        long startTime = System.currentTimeMillis();
        List<PermissionDto> result = jdbcTemplate.query(
            sql, 
            new Object[]{userId},
            permissionMapper::mapRow
        );
        
        long duration = System.currentTimeMillis() - startTime;
        log.debug("权限查询完成 [userId={}, records={}, duration={}ms]", 
                  userId, result.size(), duration);
        
        return result;
    }

    /**
     * 获取用户的所有资源 (从 v_user_resources 视图)
     * 
     * 执行时间: 39-70 ms (取决于权限类型和资源量)
     * 缓存命中时: < 1 ms
     * 
     * @param userId 用户ID
     * @return 该用户可访问的所有资源列表
     */
    @Cacheable(
        value = "permission:resources",  // 缓存名
        key = "#userId",                 // 缓存 key
        cacheManager = "redisCacheManager"
    )
    public List<ResourceDto> getUserResources(String userId) {
        log.debug("查询用户资源 [userId={}]", userId);
        
        String sql = "SELECT DISTINCT user_id, resource_id, code, name, url, type, "
                   + "parent_id, visible, icon, sort_key, status "
                   + "FROM v_user_resources "
                   + "WHERE user_id = ? "
                   + "ORDER BY sort_key, id";
        
        long startTime = System.currentTimeMillis();
        List<ResourceDto> result = jdbcTemplate.query(
            sql, 
            new Object[]{userId},
            resourceMapper::mapRow
        );
        
        long duration = System.currentTimeMillis() - startTime;
        log.debug("资源查询完成 [userId={}, records={}, duration={}ms]", 
                  userId, result.size(), duration);
        
        return result;
    }

    /**
     * 批量查询用户权限 (优于循环单个查询)
     * 性能: 1次数据库查询替代 N 次查询
     * 
     * @param userIds 用户ID列表
     * @return Map<userId, List<PermissionDto>>
     */
    public Map<String, List<PermissionDto>> getUserPermissionsBatch(List<String> userIds) {
        if (userIds.isEmpty()) {
            return new HashMap<>();
        }
        
        String placeholders = userIds.stream()
            .map(id -> "?")
            .collect(Collectors.joining(","));
        
        String sql = "SELECT user_id, permission_id, name, code, permission_type, "
                   + "parent_name, status, sort_key "
                   + "FROM v_user_permissions "
                   + "WHERE user_id IN (" + placeholders + ") "
                   + "ORDER BY user_id, sort_key";
        
        return jdbcTemplate.query(
            sql,
            userIds.toArray(),
            rs -> {
                Map<String, List<PermissionDto>> result = new HashMap<>();
                while (rs.next()) {
                    String userId = rs.getString("user_id");
                    PermissionDto perm = permissionMapper.mapRow(rs, rs.getRow());
                    result.computeIfAbsent(userId, k -> new ArrayList<>()).add(perm);
                }
                return result;
            }
        );
    }

    /**
     * 批量查询用户资源
     * 
     * @param userIds 用户ID列表
     * @return Map<userId, List<ResourceDto>>
     */
    public Map<String, List<ResourceDto>> getUserResourcesBatch(List<String> userIds) {
        if (userIds.isEmpty()) {
            return new HashMap<>();
        }
        
        String placeholders = userIds.stream()
            .map(id -> "?")
            .collect(Collectors.joining(","));
        
        String sql = "SELECT user_id, resource_id, code, name, url, type, "
                   + "parent_id, visible, icon, sort_key, status "
                   + "FROM v_user_resources "
                   + "WHERE user_id IN (" + placeholders + ") "
                   + "ORDER BY user_id, sort_key";
        
        return jdbcTemplate.query(
            sql,
            userIds.toArray(),
            rs -> {
                Map<String, List<ResourceDto>> result = new HashMap<>();
                while (rs.next()) {
                    String userId = rs.getString("user_id");
                    ResourceDto res = resourceMapper.mapRow(rs, rs.getRow());
                    result.computeIfAbsent(userId, k -> new ArrayList<>()).add(res);
                }
                return result;
            }
        );
    }
}

2. 修改 UserService

原代码 (50+ 行, 复杂)

// UserService.java (原始)
public List<ResourceDto> resources(String id) {
    UserEntity user = get(id);
    if (BooleanUtils.isTrue(user.getSysAdmin())) {
        return resourceService.allResources();  // 方案A: 所有资源
    }
    if (BooleanUtils.isTrue(user.getOrgAdmin())) {
        var defaultPermissionIds = organizationService.getDefaultPermissionIds(...)
        List<PermissionDto> allPermissions = permissionService.getAllFromCache();
        return Stream.ofAll(allPermissions)
            .filter(...)  // 过滤权限类型 0,1,2
            .flatMap(x -> x.getResources())
            .appendAll(Stream.ofAll(permissionService.getByTypes(["0","1"]))
            .flatMap(x -> x.getResources()))
            .distinctBy(BaseEntity::getId)
            .map(resourceMapper::toDto)
            .collect(Collectors.toList());
    }
    // 方案C: 常规用户角色权限
    return Stream.ofAll(user.allRoles())
        .flatMap(x -> x.getPermissions() == null ? Stream.empty() : x.getPermissions())
        .flatMap(x -> x.getResources() == null ? Stream.empty() : x.getResources())
        .appendAll(Stream.ofAll(permissionService.getByTypes(["0","1"]))
        .flatMap(...))
        .distinctBy(BaseEntity::getId)
        .map(resourceMapper::toDto)
        .collect(Collectors.toList());
}

新代码 (3 行, 清晰)

// UserService.java (优化后)
@Autowired
private PermissionCacheService permissionCacheService;

/**
 * 获取用户可访问的资源列表
 * 
 * 性能改进:
 * - 原始: 50+ 行 Stream 操作 + 多个缓存查询
 * - 优化: 单个数据库视图查询 + Redis 缓存
 * 
 * 执行时间:
 * - 首次: 39-70 ms (视图查询)
 * - 缓存命中: < 1 ms
 */
public List<ResourceDto> resources(String id) {
    return permissionCacheService.getUserResources(id);
}

/**
 * 获取用户可访问的权限列表
 */
public List<PermissionDto> permissions(String id) {
    return permissionCacheService.getUserPermissions(id);
}

3. 统一缓存失效服务

新建 CacheInvalidationService

package cn.lihongjie.coal.permission.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

/**
 * 统一的缓存失效管理服务
 * 
 * 解决的问题:
 * - 原始: PermissionService.clearCache() 需要清 5+ 个缓存
 * - 优化: 单个方法清所有相关缓存,易于维护
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class CacheInvalidationService {

    private final CacheManager cacheManager;

    /**
     * 清除指定用户的权限缓存
     * 
     * @param userId 用户ID
     */
    public void invalidateUserPermissionCache(String userId) {
        String[] cacheNames = {
            "permission:user",          // v_user_permissions 缓存
            "permission:resources",     // v_user_resources 缓存
        };
        
        for (String cacheName : cacheNames) {
            try {
                cacheManager.getCache(cacheName).evict(userId);
                log.debug("已清除用户权限缓存 [userId={}, cache={}]", userId, cacheName);
            } catch (Exception e) {
                log.warn("缓存清除失败 [userId={}, cache={}]", userId, cacheName, e);
            }
        }
    }

    /**
     * 清除所有权限缓存 (权限表变更)
     */
    public void invalidateAllPermissionCache() {
        String[] cacheNames = {
            "permission:user",
            "permission:resources",
        };
        
        for (String cacheName : cacheNames) {
            try {
                cacheManager.getCache(cacheName).clear();
                log.info("已清除所有权限缓存 [cache={}]", cacheName);
            } catch (Exception e) {
                log.warn("全量缓存清除失败 [cache={}]", cacheName, e);
            }
        }
    }

    /**
     * 清除组织权限缓存 (组织权限变更)
     * 
     * @param organizationId 组织ID
     */
    public void invalidateOrgPermissionCache(String organizationId) {
        // 获取该组织的所有用户
        List<String> userIds = getUserIdsByOrganization(organizationId);
        for (String userId : userIds) {
            invalidateUserPermissionCache(userId);
        }
        log.info("已清除组织权限缓存 [orgId={}, users={}]", organizationId, userIds.size());
    }

    /**
     * 清除角色权限缓存 (角色权限变更)
     * 
     * @param roleId 角色ID
     */
    public void invalidateRolePermissionCache(String roleId) {
        // 获取该角色的所有用户
        List<String> userIds = getUserIdsByRole(roleId);
        for (String userId : userIds) {
            invalidateUserPermissionCache(userId);
        }
        log.info("已清除角色权限缓存 [roleId={}, users={}]", roleId, userIds.size());
    }
}

4. 修改 PermissionService

原代码

public void clearCache() {
    Try.run(() -> cacheManager.getCache(CACHE_PERMISSION).clear());
    Try.run(() -> cacheManager.getCache(CACHE_IS_ANONYMOUS_BY_RESOURCE_ID).clear());
    Try.run(() -> cacheManager.getCache(CACHE_ORG_ADMIN_HAS_PERMISSION).clear());
    Try.run(() -> cacheManager.getCache(CACHE_ORGANIZATION_PERMISSION_IDS).clear());
    userService.clearUserPermissionCache();  // 交叉依赖
}

新代码

@Autowired
private CacheInvalidationService cacheInvalidationService;

public void addPermission(PermissionCreateRequest request) {
    Permission permission = permissionMapper.toEntity(request);
    permissionRepository.save(permission);
    
    // 单一责任: 清除相关缓存
    cacheInvalidationService.invalidateAllPermissionCache();
}

public void updatePermission(String id, PermissionUpdateRequest request) {
    Permission permission = permissionRepository.findById(id).orElseThrow();
    permissionMapper.updateFromRequest(request, permission);
    permissionRepository.save(permission);
    
    // 清除缓存
    cacheInvalidationService.invalidateAllPermissionCache();
}

public void deletePermission(String id) {
    permissionRepository.deleteById(id);
    
    // 清除缓存
    cacheInvalidationService.invalidateAllPermissionCache();
}

5. 修改 RoleService (角色权限变更)

@Autowired
private CacheInvalidationService cacheInvalidationService;

public void addRolePermission(String roleId, String permissionId) {
    Role role = roleRepository.findById(roleId).orElseThrow();
    Permission permission = permissionRepository.findById(permissionId).orElseThrow();
    role.getPermissions().add(permission);
    roleRepository.save(role);
    
    // 清除该角色下所有用户的缓存
    cacheInvalidationService.invalidateRolePermissionCache(roleId);
}

public void removeRolePermission(String roleId, String permissionId) {
    Role role = roleRepository.findById(roleId).orElseThrow();
    role.getPermissions().removeIf(p -> p.getId().equals(permissionId));
    roleRepository.save(role);
    
    // 清除缓存
    cacheInvalidationService.invalidateRolePermissionCache(roleId);
}

6. Redis 缓存配置

添加到 application.yaml

spring:
  cache:
    type: redis
    redis:
      time-to-live: 600000  # 10分钟 TTL
      cache-null-values: false
      key-prefix: "coal:permission:"
      key-prefix-separator: ":"
  
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

Redisson 方案 (性能更优)

redisson:
  single-server-config:
    address: "redis://localhost:6379"
    password: ${REDIS_PASSWORD}
  
  caches:
    permission:user:
      ttl: 600000  # 10分钟
    permission:resources:
      ttl: 600000

7. 迁移计划

Phase 1: 验证 (1天)

// 1. 新建 PermissionCacheService (已提供)
// 2. 添加单元测试

@Test
public void testGetUserPermissions_SysAdmin() {
    List<PermissionDto> result = permissionCacheService.getUserPermissions("sys_admin_id");
    assertEquals(412, result.size());  // 验证性能测试的数据
}

@Test
public void testGetUserResources_OrgAdmin() {
    List<ResourceDto> result = permissionCacheService.getUserResources("org_admin_id");
    assertEquals(1232, result.size());
}

Phase 2: 迁移 (2天)

// 1. UserService 修改 (resources(), permissions() 方法)
// 2. PermissionService 修改 (使用 CacheInvalidationService)
// 3. RoleService 修改 (清除缓存)
// 4. 集成测试

Phase 3: 灰度发布 (3天)

Day 1: 测试环境验证
  - 功能测试 (权限正确性)
  - 性能测试 (缓存命中率 > 80%)
  - 压力测试 (100 并发用户)

Day 2: 灰度 10% (北京数据中心)
  - 监控指标: 缓存命中率, 响应时间, 错误率
  - 告警阈值: 命中率 < 70%, 响应时间 > 200ms

Day 3: 全量发布
  - 逐步扩大到 50% → 100%
  - 准备回滚方案

8. 性能监控指标

添加 Micrometer 指标

@Service
@Slf4j
public class PermissionCacheService {
    
    private final MeterRegistry meterRegistry;
    
    @Cacheable(...)
    public List<PermissionDto> getUserPermissions(String userId) {
        long startTime = System.currentTimeMillis();
        
        try {
            List<PermissionDto> result = ...

            // 记录执行时间
            meterRegistry.timer("permission.cache.query.duration")
                .record(System.currentTimeMillis() - startTime, 
                        TimeUnit.MILLISECONDS);
            
            // 记录结果大小
            meterRegistry.gauge("permission.cache.result.size", result.size());
            
            return result;
        } catch (Exception e) {
            meterRegistry.counter("permission.cache.error").increment();
            throw e;
        }
    }
}

Prometheus 查询

# 缓存命中率
rate(cache:user:hits[5m]) / (rate(cache:user:hits[5m]) + rate(cache:user:misses[5m]))

# 平均响应时间
histogram_quantile(0.95, permission_cache_query_duration)

# 错误率
rate(permission_cache_error[5m])

9. 检查清单

在部署前确认:

  • PermissionCacheService 实现完整
  • UserService 方法已修改 (resources, permissions)
  • CacheInvalidationService 已创建
  • PermissionService 已迁移
  • RoleService 已迁移
  • Redis 连接已验证
  • 单元测试全部通过 (100%)
  • 集成测试已验证
  • 性能基准已建立
  • 监控告警已配置
  • 回滚方案已准备
  • 文档已更新

10. 预期收益

指标 原始方案 优化方案 改进
代码行数 50+ 3 减少 94%
缓存层次 5+ 2 简化 60%
单个查询 50-100ms 70ms (无缓存) 持平
缓存命中 不可预测 < 1ms 200倍快
缓存失效操作 5个缓存 2个缓存 减少 60%
维护难度 高 (Stream 复杂) 低 (SQL 清晰) 显著降低

准备就绪后,请确认实施