【JetCache】从配置到注解:构建高效缓存的实践指南
1. JetCache快速入门:为什么选择它?
第一次接触JetCache是在一个电商项目性能优化中。当时我们的商品详情页接口QPS突破5000,数据库已经扛不住了。尝试了几种缓存方案后,最终被JetCache的多级缓存和注解驱动的特性吸引。它完美解决了我们既要降低数据库压力,又要保证数据一致性的痛点。
JetCache是阿里开源的一个缓存框架,核心优势在于:
- 二合一缓存:本地缓存(Caffeine/LinkedHashMap)+远程缓存(Redis/Tair)可组合使用
- 注解即缓存:像@Cached这样的注解直接标注在方法上就能自动缓存
- 灵活过期策略:支持固定时间、访问后失效等多种过期方式
- 防穿透保护:当缓存失效时,自动避免大量请求直接打到数据库
举个例子,我们商品服务的代码从这样:
public Product getProduct(long id) { // 直接查数据库 return productMapper.selectById(id); }变成了这样:
@Cached(name="product:", key="#id", expire = 60, cacheType = CacheType.BOTH) public Product getProduct(long id) { // 只有缓存未命中时才执行 return productMapper.selectById(id); }2. 从零配置JetCache环境
2.1 SpringBoot项目配置实战
新建一个SpringBoot项目时,在application.yml中添加以下配置(以Redis+Caffeine组合为例):
jetcache: statIntervalMinutes: 15 # 缓存统计间隔 areaInCacheName: false # 新项目建议false local: default: type: caffeine limit: 1000 # 本地缓存最大元素数 keyConvertor: fastjson expireAfterWriteInMillis: 600000 # 10分钟过期 remote: default: type: redis keyConvertor: fastjson valueEncoder: kryo # 比java序列化更高效 valueDecoder: kryo poolConfig: minIdle: 5 maxTotal: 100 host: 127.0.0.1 port: 6379几个容易踩坑的配置项:
- areaInCacheName:老项目需要保持true兼容历史数据,新项目建议false
- valueEncoder:推荐kryo,序列化体积比java小30%以上
- limit:本地缓存大小要根据业务数据量调整,过小会导致频繁淘汰
2.2 非Spring环境配置
如果是普通Java项目,可以通过代码初始化:
GlobalCacheConfig config = new GlobalCacheConfig(); config.setLocalCacheBuilders( Maps.newHashMap("default", new CaffeineCacheBuilder("default") .limit(1000) .expireAfterWrite(10, TimeUnit.MINUTES)) ); config.setRemoteCacheBuilders( Maps.newHashMap("default", new RedisCacheBuilder("default") .keyConvertor(FastjsonKeyConvertor.INSTANCE) .valueEncoder(KryoValueEncoder.INSTANCE) .valueDecoder(KryoValueDecoder.INSTANCE) .jedisPool(new JedisPool("127.0.0.1", 6379))) ); JetCache.init(config);3. 核心注解深度解析
3.1 @Cached的十八般武艺
这个注解是使用频率最高的,先看一个综合案例:
@Cached( name = "user:", key = "#userId", area = "default", expire = 30, timeUnit = TimeUnit.MINUTES, cacheType = CacheType.BOTH, localLimit = 500, localExpire = 5, serialPolicy = SerialPolicy.KRYO, keyConvertor = KeyConvertor.FASTJSON, condition = "#userId > 1000", postCondition = "#result != null" ) public User getUserById(long userId) { return userMapper.selectById(userId); }关键属性解析:
- name+key:组合成最终缓存键,如user:1001
- cacheType:BOTH表示两级缓存,查询顺序:本地→远程→DB
- localExpire:本地缓存5分钟过期,而远程缓存30分钟(适合热点数据)
- condition:只缓存userId>1000的查询结果
3.2 缓存更新与失效的黄金组合
保持缓存一致性的关键三剑客:
// 查询带缓存 @Cached(name="order:", key="#orderId") public Order getOrder(long orderId) { /*...*/ } // 更新时同步缓存 @CacheUpdate(name="order:", key="#order.orderId", value="#order") public void updateOrder(Order order) { /*...*/ } // 删除时清理缓存 @CacheInvalidate(name="order:", key="#orderId") public void deleteOrder(long orderId) { /*...*/ }特别要注意的是:
- 这三个注解的name和area必须完全一致
- 更新操作可能失败,建议配合重试机制
- 批量删除可以使用@CacheInvalidate的multiKeys属性
3.3 高级特性实战
自动刷新缓存(适合低频变动的配置数据):
@Cached @CacheRefresh(refresh = 60, stopRefreshAfterLastAccess = 120) public List<Config> getSystemConfigs() { return configMapper.selectAll(); }防雪崩保护:
@Cached @CachePenetrationProtect public Product getProduct(long id) { // 当缓存失效时,只有一个请求能进入此方法 return productMapper.selectById(id); }4. 性能优化实战技巧
4.1 多级缓存调优策略
我们通过压测发现,合理配置多级缓存可使吞吐量提升8倍:
| 场景 | QPS | 平均响应时间 |
|---|---|---|
| 无缓存 | 1,200 | 85ms |
| 仅Redis | 8,000 | 12ms |
| Redis+Caffeine | 10,000 | 8ms |
| 本地缓存预热 | 15,000 | 5ms |
优化建议:
- 热点数据:使用CacheType.BOTH,并设置较短的localExpire
- 大对象缓存:只放Redis,避免本地内存爆掉
- 频繁更新数据:建议只用远程缓存
4.2 序列化选型对比
我们测试了不同序列化方案在User对象上的表现:
| 方案 | 序列化大小 | 耗时(ms) |
|---|---|---|
| Java原生 | 1,024 bytes | 45 |
| Kryo | 512 bytes | 12 |
| Fastjson | 768 bytes | 22 |
结论:Kryo在性能和空间上都是最佳选择,但要注意:
- 需要注册所有要序列化的类
- 字段增减会导致反序列化失败
4.3 监控与问题排查
启用统计配置后:
jetcache: statIntervalMinutes: 5 # 每5分钟输出统计日志典型日志分析:
[PRODUCT] hitCount=2345, missCount=123, loadSuccessCount=120 [ORDER] hitCount=5678, missCount=45, loadSuccessCount=45如果发现某个缓存的missCount异常高,可能是:
- 过期时间设置过短
- 缓存键设计不合理导致无法命中
- 需要增加本地缓存层
5. 真实业务场景解决方案
5.1 电商商品详情优化
我们的最终方案:
@Cached( name = "product:v3:", key = "#productId + '_' + #showType", cacheType = CacheType.BOTH, localLimit = 2000, localExpire = 1, expire = 30, timeUnit = TimeUnit.MINUTES ) @CachePenetrationProtect public ProductDetail getDetail(long productId, int showType) { // 复杂组装逻辑 }关键设计:
- 将showType加入缓存键,区分不同展示模板
- 本地缓存1分钟,解决瞬时热点问题
- 分布式锁保护防止缓存击穿
5.2 秒杀库存缓存方案
秒杀场景的特殊处理:
@CacheUpdate( name = "seckill:stock:", key = "#itemId", value = "#result", condition = "#result >= 0" ) public int deductStock(long itemId, int count) { // 原子性扣减库存 return seckillMapper.updateStock(itemId, count); }配合Redis的DECR命令实现:
- 提前预热库存到Redis
- 先用Redis做预扣减
- 异步持久化到数据库
5.3 分布式会话管理
用户会话存储方案:
@Cached( name = "session:", key = "#token", expire = 1440, timeUnit = TimeUnit.MINUTES, serialPolicy = SerialPolicy.KRYO ) public SessionInfo getSession(String token) { return sessionService.loadSession(token); }这种设计:
- 避免重复查库
- 天然支持分布式
- 自动过期清理
6. 避坑指南与最佳实践
6.1 缓存键设计的艺术
我们踩过的坑案例:
// 错误示范:没有区分业务场景 @Cached(name="query", key="#param") public List<User> queryUsers(Map param) { /*...*/ } // 正确做法:明确业务语义 @Cached(name="user:query:byStatus", key="#status") public List<User> queryByStatus(int status) { /*...*/ }缓存键设计原则:
- 包含业务语义(如"order:paid:")
- 避免使用复杂对象作为key
- 不同查询条件要区分key
6.2 缓存一致性解决方案
对于财务类强一致性要求的场景:
- 使用@CacheUpdate更新缓存
- 配合@Transactional确保数据库和缓存同时更新
- 增加重试机制应对网络抖动
@Transactional public void updateAccount(Account account) { accountMapper.update(account); cacheManager.updateCache(buildCacheKey(account.getId()), account); }6.3 内存控制策略
本地缓存容易导致OOM,建议:
- 根据数据大小设置合理的localLimit
- 大对象不要放本地缓存
- 使用WeakReference模式(Caffeine支持)
@Cached( cacheType = CacheType.BOTH, localLimit = 100, // 严格控制数量 localExpire = 10 // 短期持有 ) public BigDataReport getReport(long id) { /*...*/ }7. 扩展与集成方案
7.1 与SpringCache的对比迁移
JetCache比SpringCache强大之处:
- 支持TTL过期控制
- 提供多级缓存支持
- 更丰富的注解功能
迁移示例:
// SpringCache版本 @Cacheable(value = "users", key = "#id") public User getUser(long id) { /*...*/ } // JetCache改进版 @Cached(name = "user:", key = "#id", expire = 30) public User getUser(long id) { /*...*/ }7.2 自定义缓存扩展
实现自定义缓存的关键步骤:
- 继承AbstractEmbeddedCache
- 注册自定义CacheBuilder
- 配置中使用type指定
public class CustomCache extends AbstractEmbeddedCache { // 实现必要方法 } // 注册Builder public class CustomCacheBuilder extends EmbeddedCacheBuilder { @Override public CustomCache build() { return new CustomCache(); } }7.3 监控系统集成
通过JMX暴露指标:
@Bean public MBeanExporter jetcacheMBeanExporter() { MBeanExporter exporter = new MBeanExporter(); exporter.setBeans(Collections.singletonMap( "com.alicp.jetcache:type=CacheStatistics", new CacheStatistics() )); return exporter; }Prometheus监控配置:
management: metrics: export: prometheus: enabled: true cache: jetcache: enabled: true8. 性能压测数据参考
我们使用JMeter对商品查询接口进行测试:
场景1:纯数据库查询
- 线程数:100
- RPS:1,200
- 平均响应时间:85ms
- 错误率:0%
场景2:JetCache两级缓存
- 线程数:100
- RPS:15,000
- 平均响应时间:8ms
- 错误率:0%
关键发现:
- 99线从200ms降到15ms
- 数据库负载下降90%
- 本地缓存命中率达85%
压测建议:
- 先小规模测试找到最优配置
- 关注localLimit对GC的影响
- 监控网络带宽使用情况
9. 复杂场景解决方案
9.1 分页查询缓存
特殊处理方案:
@Cached( name = "user:page:", key = "#pageNo + '_' + #pageSize", expire = 10 ) public PageInfo<User> queryByPage(int pageNo, int pageSize) { return userMapper.selectPage(pageNo, pageSize); } // 数据变更时清除所有分页缓存 @CacheInvalidate(name = "user:page:", multiKeys = true) public void addUser(User user) { userMapper.insert(user); }9.2 批量查询优化
使用CacheLoader模式:
@Cached( name = "user:", key = "#userId", cacheLoader = "userBatchLoader" ) public User getUser(long userId) { // 单查走默认逻辑 } // 批量加载器 public Map<Long, User> userBatchLoader(Set<Long> userIds) { return userMapper.selectBatchIds(userIds) .stream() .collect(Collectors.toMap(User::getId, u -> u)); }9.3 热点数据发现
结合监控数据自动识别:
- 分析缓存命中率
- 对高频访问数据自动提升本地缓存级别
- 动态调整过期时间
@Scheduled(fixedRate = 60000) public void adjustHotData() { cacheStats.getHotKeys().forEach(key -> { cacheManager.upgradeToLocalCache(key); }); }10. 未来架构演进
虽然JetCache已经很强大了,但在我们的使用过程中还发现可以进一步优化:
- 自动分级存储:根据访问频率自动移动数据(内存→SSD→HDD)
- 智能预加载:基于历史访问模式预测性加载数据
- 跨机房同步:解决多地域部署时的缓存一致性问题
目前我们正在尝试的混合架构:
- 一级缓存:Caffeine(本地内存)
- 二级缓存:JetCache+Redis(分布式)
- 三级缓存:自研磁盘缓存(大容量存储)
这种架构在保证性能的同时,将缓存成本降低了60%。特别是在处理海量商品数据时,通过智能淘汰算法,热点数据的命中率始终保持在95%以上。
