CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-225 · JHipster 项目开发

JHipster 开发 10:缓存——从 Hazelcast 到 Redis

缓存是 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,触发条件按出现频率排序:

  1. 多实例规模扩大:实例数与缓存量都涨,内嵌模式的堆压力与再平衡成本开始肉眼可见;
  2. 需要分布式锁:定时任务多实例只跑一份、防重复下单,Redis 的 SET NX/Redisson 是标准答案,Hazelcast 也有锁但生态薄;
  3. 业务缓存变重:热点数据、接口结果、验证码这类短命数据量大,Redis 的内存模型与过期策略(TTL/LRU)更顺手;
  4. 多应用共享:网关、多个服务要读同一份缓存(微服务形态下 Hazelcast embedded 基本出局,选 No + 外置 Redis);
  5. 队列/计数器等数据结构需求:限流计数、排行榜,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 按业务域命名productcustomer: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 当持久库把内存打爆;
  • 慢日志与大 keySLOWLOG GETredis-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 带抖动、缓存可一键清空。
  • 缓存一致性追求的是”可控的最终一致”,不是绝对一致。

系列导航

← Java 基础 010:去除多余的if else 目录 网络协议 010:工具:Wireshark介绍及抓包分析 →
← 返回文章列表