Skip to content

框架缓存使用指南(Redis替代EhCache)

一、为什么要废弃EhCache?

随着微服务架构和集群部署的普及,传统的本地缓存方案(如EhCache)已经无法很好地满足现代应用的需求:

  • 集群支持不足:EhCache在分布式环境下配置复杂,维护成本高
  • 性能瓶颈:对于大规模并发请求,本地缓存容易成为性能瓶颈
  • 扩展性差:难以适应动态扩缩容的微服务架构

因此,本框架从Spring Boot 3开始全面废弃EhCache,转而采用Redis作为统一的缓存解决方案。

二、Redis缓存的核心优势

  • 高并发支持:基于内存操作,读写速度极快
  • 分布式友好:天然支持集群部署,适合微服务架构
  • 丰富的数据结构:支持字符串、哈希、列表、集合等多种数据类型
  • 持久化机制:可配置RDB或AOF持久化策略,保障数据安全

三、使用缓存带来的核心价值

🚀 性能提升方面

  • 接口响应速度飞跃:缓存命中时接口响应时间从原来的几百毫秒甚至几秒降低到毫秒级,用户体验显著提升
  • QPS承载能力倍增:相同硬件资源配置下,系统并发处理能力可提升5-10倍
  • 页面加载速度优化:前端页面数据获取时间大幅缩短,页面渲染更加流畅

💰 成本节约方面

  • 数据库负载减轻:80%以上的读请求由缓存处理,显著降低数据库CPU和IO压力
  • 硬件投入减少:同等业务量下可减少30-50%的数据库服务器配置需求
  • 运维成本下降:数据库稳定性提升,故障率降低,维护工作量减少

🔧 系统稳定性方面

  • 抗压能力强:面对突发流量冲击时,缓存层可有效缓冲,避免数据库直接崩溃
  • 故障隔离效果:当数据库出现性能问题时,缓存可保证核心功能的基本可用性
  • 降级保护机制:数据库异常时可通过缓存提供降级服务,保障业务连续性

四、框架缓存默认配置

⚙️ 系统默认设置

  • 默认缓存时间:5分钟(300秒)
  • 缓存淘汰策略:LRU(最近最少使用)
  • 最大内存限制:根据Redis配置决定
  • 序列化方式:JDK原生序列化

注意: 如需自定义缓存时间或其他配置,请联系架构师进行全局配置调整。

五、Redis缓存注解使用说明

在本框架中,Redis缓存通过Spring的@Cacheable@CacheEvict注解实现,以下是详细说明:

1. @Cacheable 注解(缓存数据)

用于标记方法的结果需要被缓存。

核心参数:

  • value:指定缓存名称(必须参数)
  • keyGenerator:指定缓存Key的生成策略(必须使用框架封装的pageKeyGenerator
  • unless:指定缓存条件,当表达式结果为true时不缓存(必须使用T(com.kp.framework.utils.kptool.KPStringUtil).isEmpty(#result)

⚠️ 重要提醒: 以下两个参数为框架内部封装的固定写法,请严格按照示例书写,避免出现缓存问题:

java
keyGenerator = "pageKeyGenerator"
unless = "T(com.kp.framework.utils.kptool.KPStringUtil).isEmpty(#result)"

完整使用示例:

java
@Cacheable(value = "projectCache", keyGenerator = "pageKeyGenerator", unless = "T(com.kp.framework.utils.kptool.KPStringUtil).isEmpty(#result)")
public KPResult<ProjectPO> queryPageList(ProjectListParamPO projectListParamPO) {
    // 搜索条件
    LambdaQueryWrapper<ProjectPO> queryWrapper = Wrappers.lambdaQuery(ProjectPO.class)
            .like(KPStringUtil.isNotEmpty(projectListParamPO.getProjectName()), ProjectPO::getProjectName, projectListParamPO.getProjectName())
            .like(KPStringUtil.isNotEmpty(projectListParamPO.getProjectCode()), ProjectPO::getProjectCode, projectListParamPO.getProjectCode())
            .eq(KPStringUtil.isNotEmpty(projectListParamPO.getStatus()), ProjectPO::getStatus, projectListParamPO.getStatus())
            .eq(KPStringUtil.isNotEmpty(projectListParamPO.getManage()), ProjectPO::getManage, projectListParamPO.getManage())
            .like(KPStringUtil.isNotEmpty(projectListParamPO.getAppId()), ProjectPO::getAppId, projectListParamPO.getAppId());

    // 分页和排序
    PageHelper.startPage(projectListParamPO.getPageNum(), projectListParamPO.getPageSize(), projectListParamPO.getOrderBy(ProjectPO.class));
    return KPResult.list(this.baseMapper.selectList(queryWrapper));
}

2. @CacheEvict 注解(清除缓存)

用于清除缓存,通常在数据更新或删除后调用。

核心参数:

  • value:指定缓存名称(必须参数)
  • allEntries:是否清除该缓存区域下的所有条目(建议使用allEntries = true

⚠️ 重要提醒: 为了确保数据一致性,建议统一使用allEntries = true清除整个缓存区域:

java
allEntries = true

完整使用示例一 :单缓存清理

java
@CacheEvict(value = "projectCache", allEntries = true)
public void saveProject(ProjectEditParamPO projectEditParamPO) {
    ProjectPO projectPO = KPJsonUtil.toJavaObjectNotEmpty(projectEditParamPO, ProjectPO.class);

    List<ProjectPO> projectPOList = this.baseMapper.selectList(Wrappers.lambdaQuery(ProjectPO.class)
            .eq(ProjectPO::getProjectName, projectPO.getProjectName())
            .or()
            .eq(ProjectPO::getProjectCode, projectPO.getProjectCode()));

    if (KPStringUtil.isNotEmpty(projectPOList)) {
        projectPOList.forEach(projectPO1 -> {
            if (projectPO1.getProjectName().equals(projectPO.getProjectName()))
                throw new KPServiceException("项目名称已存在,请勿重复添加");
            if (projectPO1.getProjectCode().equals(projectPO.getProjectCode()))
                throw new KPServiceException("项目编号已存在,请勿重复添加");
        });
    }

    projectPO.setAppId(KPAuthorizationUtil.getAppId());
    projectPO.setAppSecret(KPAuthorizationUtil.getAppSecret(60));
    projectPO.setTokenFailure(KPAuthorizationUtil.TOKEN_FAILURE);
    projectPO.setTokenGainMaxNum(KPAuthorizationUtil.TOKEN_GAIN_MAX_NUM);
    projectPO.setVoucher(new BCryptPasswordEncoder().encode(projectPO.getAppSecret()));
    if (this.baseMapper.insert(projectPO) == 0)
        throw new KPServiceException(ReturnFinishedMessageConstant.ERROR);

    projectEditParamPO.setProjectId(projectPO.getProjectId());
    ProjectCache.clear();
}

完整使用示例二 :多缓存清理

java
@CacheEvict(value = {"userCache", "menuCache"}, allEntries = true)
public void doMenuInstall(RoleMenuInstallParamPO roleMenuInstallParamPO) {
    RolePO rolePO = roleMapper.selectById(roleMenuInstallParamPO.getRoleId());
    if (rolePO == null) throw new RuntimeException("角色不存在");

    List<RoleProjectRelevancePO> authRoleProjectRelevanceList = roleProjectRelevanceMapper.selectList(Wrappers.lambdaQuery(RoleProjectRelevancePO.class).eq(RoleProjectRelevancePO::getRoleId, roleMenuInstallParamPO.getRoleId()));
    if (KPStringUtil.isNotEmpty(authRoleProjectRelevanceList) && !authRoleProjectRelevanceList.stream().map(RoleProjectRelevancePO::getRoleId).toList().contains(roleMenuInstallParamPO.getRoleId())) {
        throw new RuntimeException("该角色没有分配该项目,请在角色里面设置所属项目");
    }

    //删除历史菜单
    List<String> armIds = this.baseMapper.selectList(new LambdaQueryWrapper<>(RoleMenuPO.class)
            .eq(RoleMenuPO::getRoleId, rolePO.getRoleId())
            .eq(RoleMenuPO::getProjectId, roleMenuInstallParamPO.getProjectId())
    ).stream().map(RoleMenuPO::getArmId).collect(Collectors.toList());
    if (KPStringUtil.isNotEmpty(armIds)) this.baseMapper.kpDeleteAllByIds(armIds);

    if (KPStringUtil.isEmpty(roleMenuInstallParamPO.getMenuIds())) return;

    List<RoleMenuPO> roleMenuPOList = new ArrayList<>();
    roleMenuInstallParamPO.getMenuIds().forEach(menuId -> {
        roleMenuPOList.add(new RoleMenuPO()
                .setRoleId(roleMenuInstallParamPO.getRoleId())
                .setProjectId(roleMenuInstallParamPO.getProjectId())
                .setMenuId(menuId));
    });

    if (this.baseMapper.kpInsertBatchSomeColumn(roleMenuPOList) == 0)
        throw new KPServiceException(ReturnFinishedMessageConstant.ERROR);
}

六、重要注意事项和风险提示

⚠️ 谨慎使用缓存

Redis缓存虽然强大,但如果使用不当可能导致严重问题:

  • 脏数据风险:未及时清理缓存导致旧数据仍然生效
  • 缓存穿透:查询不存在的数据导致频繁访问数据库

📋 缓存清理原则

所有设计数据变更必清理:数据新增、修改、删除操作后务必调用@CacheEvict清理相关缓存


💡 温馨提示:
如在使用过程中遇到任何缓存相关问题,请及时联系技术支持团队。建议在生产环境使用前进行充分的压力测试和功能验证。

📌 特别注意:本文档中的固定写法(如keyGenerator、unless、allEntries等)请严格按照示例书写,切勿自行修改参数配置。