框架缓存使用指南(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等)请严格按照示例书写,切勿自行修改参数配置。
如在使用过程中遇到任何缓存相关问题,请及时联系技术支持团队。建议在生产环境使用前进行充分的压力测试和功能验证。
📌 特别注意:本文档中的固定写法(如keyGenerator、unless、allEntries等)请严格按照示例书写,切勿自行修改参数配置。
