在ApiControllers服务层搭配Redis实现二级缓存存储匿名用户数据的最佳实践
二级缓存方案:内存缓存+Redis兜底(针对匿名用户短时效数据)
核心思路
针对匿名用户5分钟内的短时效数据(收藏、浏览记录),采用进程内内存缓存作为一级缓存、Redis作为二级缓存的架构:
- 内存缓存负责高频读写,设置5分钟过期,直接命中时性能拉满;
- Redis作为兜底,设置稍长过期时间(如10分钟),避免内存缓存失效后冷启动,同时保证服务重启或多实例部署时的数据一致性。
具体实现方案
1. 内存缓存选型
优先选Caffeine(性能优于Guava Cache,是当前Java生态最优的内存缓存库),配置核心参数:
expireAfterWrite=5分钟:写入后5分钟过期,匹配用户回访窗口;maximumSize:根据服务内存容量设置上限(如10000条),防止内存溢出;- 可选开启统计(
recordStats()),用于监控缓存命中率。
2. 统一二级缓存抽象层
封装内存缓存与Redis的操作逻辑,业务服务仅依赖抽象层,提升代码可读性与可维护性:
配置Caffeine缓存实例
@Configuration public class CaffeineCacheConfig { @Bean public Cache<String, Object> anonymousUserCache() { return Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(10000) .recordStats() // 可选:开启统计 .build(); } }
二级缓存服务实现
@Service public class TwoLevelCacheService { private final Cache<String, Object> anonymousUserCache; private final RedisService redisService; private final ThreadPoolTaskExecutor asyncRedisExecutor; // 构造注入依赖 public TwoLevelCacheService(Cache<String, Object> anonymousUserCache, RedisService redisService, ThreadPoolTaskExecutor asyncRedisExecutor) { this.anonymousUserCache = anonymousUserCache; this.redisService = redisService; this.asyncRedisExecutor = asyncRedisExecutor; } // 读操作:内存优先,Redis兜底并回写内存 @SuppressWarnings("unchecked") public <T> T get(String key, Class<T> clazz) { // 1. 查内存缓存 T value = (T) anonymousUserCache.getIfPresent(key); if (value != null) { return value; } // 2. 内存未命中,查Redis value = redisService.get(key, clazz); if (value != null) { // 3. 将Redis数据回写内存缓存 anonymousUserCache.put(key, value); } return value; } // 写操作:同步更新内存,异步写入Redis(避免阻塞业务线程) public void put(String key, Object value) { // 1. 同步更新内存缓存 anonymousUserCache.put(key, value); // 2. 异步写入Redis,设置10分钟过期 asyncRedisExecutor.execute(() -> redisService.set(key, value, 10, TimeUnit.MINUTES) ); } // 删除操作:同时失效内存与Redis数据 public void delete(String key) { anonymousUserCache.invalidate(key); asyncRedisExecutor.execute(() -> redisService.delete(key)); } }
异步线程池配置(可选,替代JDK原生线程池)
@Configuration public class AsyncConfig { @Bean("asyncRedisExecutor") public ThreadPoolTaskExecutor asyncRedisExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-redis-"); executor.initialize(); return executor; } }
3. 业务服务调用示例
以匿名用户收藏功能为例,统一使用缓存key前缀规范(anon:{匿名ID}:{数据类型}):
@Service public class AnonymousUserService { private final TwoLevelCacheService twoLevelCacheService; public AnonymousUserService(TwoLevelCacheService twoLevelCacheService) { this.twoLevelCacheService = twoLevelCacheService; } // 保存匿名用户商品收藏 public void addFavorite(String anonId, Long productId) { String cacheKey = String.format("anon:%s:favorites", anonId); // 从缓存获取现有收藏列表 Set<Long> favorites = twoLevelCacheService.get(cacheKey, Set.class); if (favorites == null) { favorites = new HashSet<>(); } favorites.add(productId); // 写入二级缓存 twoLevelCacheService.put(cacheKey, favorites); } // 获取匿名用户商品收藏列表 public Set<Long> getFavorites(String anonId) { String cacheKey = String.format("anon:%s:favorites", anonId); return twoLevelCacheService.get(cacheKey, Set.class); } }
可选简化方案:Spring Cache注解式实现
如果不想手动封装缓存逻辑,可以利用Spring Cache的扩展能力,同时配置Caffeine与Redis缓存管理器,通过@Cacheable、@CachePut注解实现二级缓存:
- 配置两个缓存管理器(Caffeine对应内存缓存,Redis对应远程缓存);
- 自定义缓存注解或使用
@Caching组合多个缓存操作,实现先读内存、再读Redis,写操作同步更新两者。
这种方式代码更简洁,但灵活性稍弱,适合业务逻辑简单的场景。
关键优化点
- Key命名规范:统一使用
anon:{匿名ID}:{数据类型}格式,避免缓存键冲突; - 序列化兼容性:Redis存储对象时,优先用JSON序列化(如Jackson),避免Java序列化的兼容性问题;
- 监控与调优:通过Caffeine的统计功能监控缓存命中率,调整
maximumSize与过期时间; - 多实例一致性:因为匿名用户数据一致性要求低,Redis稍长的过期时间足以覆盖多实例间的数据同步需求,无需额外的分布式缓存失效方案。
内容的提问来源于stack exchange,提问作者FoolCode
相关产品推荐
相关产品推荐

