缓存是 JHipster 问答里容易被随手划过的选项,但它影响部署形态与一致性模型,值得单独一篇。JHipster 9 的生成器提供 Hazelcast / Redis / Memcached / No 几个选项(微服务场景还有专门的 No 缓存含义),这篇讲清楚每个选项的真实语义、我们什么时候从 Hazelcast 迁到 Redis,以及最容易翻车的缓存一致性问题。
两个不同层面的”缓存”
先拆概念,很多混乱来自把两件事混着说:
| 层面 | 缓存什么 | 典型实现 | 关心什么 |
|---|---|---|---|
| 基础设施层 | HTTP session(如登录态)、Hibernate 二级缓存(实体/查询) | Hazelcast(内嵌集群) | 部署多实例时状态共享 |
| 业务层 | 业务计算结果、热点数据、防重锁 | Spring Cache 抽象 + 任意后端 | 命中率、一致性、失效 |
JHipster 的 cacheProvider 选项主要决定基础设施层的默认装配(以及提供好的业务缓存后端),业务层缓存则统一走 Spring Cache 注解。选型时先想清楚你要的是哪一层。
JHipster 的缓存选项
| 选项 | 定位 | 适用 |
|---|---|---|
| Hazelcast | 内嵌分布式缓存,JHipster 默认推荐(单体) | 单体多实例部署,不想要外部缓存服务 |
| Redis | 外部缓存服务 | 多实例共享、分布式锁、业务缓存主力 |
| Memcached | 外部缓存服务 | 存量设施兼容场景,新项目少选 |
| No | 不装配缓存 provider | 无状态简单应用;微服务选项里常见(服务无状态化,缓存外置) |
Hazelcast:单体多实例的内置答案
单体选 Hazelcast 的场景是:应用要部署多个实例做负载均衡,但不想引入外部缓存服务。JHipster 生成的 config/cache/HazelcastConfiguration.java 会装配两件事:
flowchart LR
subgraph instances["应用实例 x N(内嵌 Hazelcast 成员)"]
A["实例 A"] ---|"TCP 自动发现<br/>集群同步"| B["实例 B"]
end
LB["负载均衡器"] --> A
LB --> B
subgraph hz["Hazelcast 分布式缓存(embedded)"]
S["HTTP Session 缓存<br/>Spring Session"]
L2["Hibernate L2 C<br/>实体/查询缓存"]
end
A & B --- hz
DB[("PostgreSQL")] -.-"缓存未命中"| hz
- HTTP session 缓存:配合 Spring Session,登录态(尤其 Session 认证时)存进 Hazelcast,任意实例宕机用户不掉线,负载均衡无需粘滞会话。JWT 认证下这层作用弱化(本就无状态),但 OAuth2 场景仍会用到。
- Hibernate 二级缓存:
hibernate.cache.use_second_level_cache打开,实体/集合/查询结果缓存跨实例共享,读密集的查询显著减压。注意 JDL 生成实体的缓存是默认开的,写频繁的实体记得显式关(@Cache注解与配置层面调整)。
内嵌模式的好处是零运维:Hazelcast 以成员库形式跑在应用 JVM 里,实例间 TCP 自动组网,没有独立缓存服务要管。代价是占用堆内存、节点数多时集群通信开销上升。我们的经验边界:3-5 个实例规模很舒服;节点更多或缓存数据量大(GB 级),就该迁 Redis。
何时引入 Redis
我们单体起家(Hazelcast),两年后迁到 Redis,触发条件按出现频率排序:
- 多实例规模扩大:实例数与缓存量都涨,内嵌模式的堆压力与再平衡成本开始肉眼可见;
- 需要分布式锁:定时任务多实例只跑一份、防重复下单,Redis 的
SET NX/Redisson 是标准答案,Hazelcast 也有锁但生态薄; - 业务缓存变重:热点数据、接口结果、验证码这类短命数据量大,Redis 的内存模型与过期策略(TTL/LRU)更顺手;
- 多应用共享:网关、多个服务要读同一份缓存(微服务形态下 Hazelcast embedded 基本出局,选 No + 外置 Redis);
- 队列/计数器等数据结构需求:限流计数、排行榜,Redis 原生结构直接支持。
迁移动作不伤筋动骨:生成层面把 cacheProvider 换成 redis(或加 Spring Data Redis 依赖),基础设施层 session/L2C 改指向 Redis,业务层因为走 Spring Cache 抽象(下一节)基本只动配置。
Spring Cache 抽象:@Cacheable / @CacheEvict
业务缓存 JHipster 生成代码里就有范例(比如 UserService 对用户查询的缓存),模式是标准 Spring Cache:
@Service
public class ProductService {
@Cacheable(cacheNames = "product", key = "#id")
public ProductDTO findOne(Long id) {
return productMapper.toDto(productRepository.findById(id)
.orElseThrow(EntityNotFoundException::new));
}
@CachePut(cacheNames = "product", key = "#result.id")
public ProductDTO update(ProductDTO dto) { … }
@CacheEvict(cacheNames = "product", key = "#id")
public void delete(Long id) { … }
}
注解只声明意图,后端(cacheManager)由配置决定——切 Redis 就是换 cacheManager 实现,注解一行不动。Redis 配置示例:
# application-prod.yml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: 6379
password: ${REDIS_PASSWORD:}
两个纪律:key 用 SpEL 显式声明(#id、#dto.customerId),别依赖默认的参数 hashCode 生成(调试地狱);cacheNames 按业务域命名(product、customer:detail),失效管理才有边界。TTL 不在注解里写(Spring Cache 的注解原生不带过期语义),在 cacheManager 的配置里按 cacheName 设默认过期:
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
var defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> perCache = new HashMap<>();
perCache.put("product", defaults.entryTtl(Duration.ofHours(2))); // 热点目录数据
perCache.put("captcha", defaults.entryTtl(Duration.ofMinutes(5))); // 短命数据
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(perCache)
.build();
}
}
Redis 侧给每个业务域不同 TTL,是命中率与新鲜度之间主要的调节旋钮。
缓存的观测与治理
缓存上线不是终点,没有观测的缓存等于盲飞:
- 命中率:Spring Cache 的统计经 Micrometer 暴露(
/management/metrics可查,接 Prometheus/Grafana 后看趋势更直观,第 20 篇),命中率长期偏低说明 key 设计或 TTL 有问题; - 内存与淘汰:Redis 侧盯
used_memory与淘汰策略(推荐allkeys-lru一类),避免拿 Redis 当持久库把内存打爆; - 慢日志与大 key:
SLOWLOG GET与redis-cli --bigkeys定期巡检,大 value 序列化开销会拖垮接口; - 演练:预发环境定期演练”Redis 整体不可用”,确认应用降级可跑(直查数据库)而不是雪崩。
缓存一致性与失效策略
缓存三大经典问题对应的我们实践:
1)先更新库还是先动缓存? 推荐 update DB → evict cache(cache aside,删除而非更新缓存)。删除是幂等的,下次 miss 会从库回填最新值。反过来”先删缓存再更新库”在并发下会回填旧值,是事故常客。极少数强一致场景接受短暂陈旧 + 短 TTL,别追求双写的分布式事务。
2)失效风暴与击穿。 热点 key 过期瞬间大量请求穿透到库。手段:TTL 加随机抖动错峰;关键热点用 @CachePut 维持常驻;高价值场景上互斥加载(进程内锁或 Redisson)。
3)空值与穿透。 查不存在的 id 反复打库——缓存空哨兵值(短 TTL 的 NULL 标记)或入口参数校验。雪崩则靠 TTL 抖动 + 缓存预热(发布后脚本灌一遍热点)。
最后一条通用原则:一切缓存都要能”一键全灭”。Redis 里按 cacheName 设计 key 前缀,管理端留 flush 手段,出数据异常先灭缓存止血——缓存的正确性兜底永远是数据库,而不是反过来。
小结
- 分两层看:Hazelcast 管基础设施(HTTP session + Hibernate L2C,单体多实例内置方案),业务缓存统一走 Spring Cache 注解。
- 3-5 实例规模 Hazelcast 舒适;实例/数据量上来了、要分布式锁与共享缓存,迁 Redis,注解层无痛。
- 业务缓存纪律:显式 key、按域命名 cacheNames、update DB 后 evict、TTL 带抖动、缓存可一键清空。
- 缓存一致性追求的是”可控的最终一致”,不是绝对一致。
系列导航
- 上一篇:安全体系(下)——授权
- 下一篇:REST API 设计与最佳实践(第 11 篇)
- 延伸阅读:微服务 vs 单体——怎么选 · 技术栈全景图