找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

241

积分

1

好友

23

主题
发表于 4 天前 | 查看: 16| 回复: 0

在技术面试中,一旦提到 Caffeine 本地缓存,面试官往往会展开深入的技术探讨。今天通过一个亿级用户平台的真实案例,分析什么时候该用 Caffeine,什么时候应该避免使用。

面试场景还原

在最近一次技术面试中,当提到项目中使用了 Caffeine 本地缓存时,面试官立即提出了一系列问题:

面试官:"为什么不用 Redis?" 候选人:"因为..."

面试官:"数据变更如何处理?" 候选人:"这个..."

面试官:"服务重启后缓存丢失怎么办?数据库压力不会激增吗?" 候选人:"呃..."

面试官:"大量数据存储在内存中,不会导致频繁 Full GC 吗?" 候选人:"......"

image

其实面试官的质疑很有道理。在 90% 的业务场景中,直接使用 Redis 就能满足需求,它简单可靠且不易出错。但 Caffeine 作为本地缓存解决方案,确实有其特定的适用场景。如果能清晰阐述选择它的理由和逻辑,反而会成为技术面试的加分项。

image

真实案例:POI 服务的性能瓶颈

服务背景介绍

某大型外卖平台(用户量达亿级)的核心 POI 服务(Point of Interest)负责管理商家信息,包括肯德基、星巴克、海底捞等商家的详细信息。

这个服务的重要性不言而喻:QPS 峰值超过 10000,数十个上游系统依赖它(外卖、到店、酒旅、搜索、推荐等),一旦服务不可用,整个平台将陷入瘫痪。

image

POI 服务存储的商家信息包含商家名称、联系电话、地址、经纬度坐标、营业时间、整体介绍等 30 多个字段。总数据量为 400 万条商家记录,每条记录平均 1.3KB,总数据量约 6GB。这些数据原本全部存储在 3 主 3 从的 Redis Cluster 中。

image

核心问题:Redis 网络带宽瓶颈

上游系统调用 POI 服务时通常采用批量查询方式(每次请求查询几十到上百个商家),平均每次请求返回 1MB 数据。在 QPS 峰值 10000 的情况下,所需网络带宽为 10000 × 1MB = 10GB/s。

而 Redis Cluster(3 主 3 从)的网络容量如何?万兆网卡理论速度为 1.28GB/s,只有 3 个主节点接收请求,总容量为 3 × 1.28 = 3.84GB/s。

结论:需求 10GB/s,实际容量只有 3.84GB/s,网络带宽成为瓶颈!

image

面对这个问题,我们考虑了两种解决方案。

方案一:Redis 扩容 从 3 主 3 从扩展到 12 主 12 从,网络容量提升到 15.36GB/s。这个方案简单直接,数据一致性有保障,但服务器成本增加 4 倍(从 6 台增加到 24 台)。

方案二:引入 Caffeine 本地缓存 利用现有的 12 台应用服务器,将热点数据缓存在应用服务器内存中。这个方案成本不变,响应速度更快,Redis 压力显著降低,但需要解决数据一致性和内存占用问题。

我们最终选择了方案二,关键原因在于我们发现了一个重要规律。

关键洞察:1% 的热点数据承载 90% 流量

通过分析一个月的访问日志,我们发现了一个显著规律:每个城市中 1% 的热门商家,承接了 90% 的访问流量

以北京为例,虽然有 10 万家商家,但用户经常访问的只有约 1000 家(如肯德基、麦当劳、星巴克、海底捞等)。这是典型的二八定律,甚至可以说是一九定律。

image

这个发现意味着我们只需要缓存这 1% 的热门商家数据即可:

项目 全量数据 热点数据(1%)
数据量 400 万条 4 万条
内存占用 6GB 60MB
缓存命中率 100% 90%+

用 60MB 内存换取 90% 的请求不经过网络,这个 trade-off 非常划算!

应用服务器配置为 4C8G,60MB 内存占用完全在可接受范围内。而且 Caffeine 使用 W-TinyLFU 算法,能够自动淘汰冷数据、保留热数据,无需手动维护。

image

三大技术挑战与解决方案

挑战一:数据一致性保障

商家信息会发生变更(如修改营业时间、更新联系电话)。如果使用 Redis,只需在一个地方更新即可。但使用 Caffeine 后,数据分散在 12 台服务器上,如何保证数据同步?

传统做法是使用广播机制,在数据变更时通知所有服务器更新缓存,但这需要引入消息队列或发布订阅系统,增加了系统复杂度。

我们的解决方案:设置合理的过期时间

商家信息对实时性要求不高,营业时间变更延迟几秒生效是可以接受的。因此我们设置了 8-12 秒的随机过期时间

Random random = new Random();
int expireSeconds = random.nextInt(5) + 8; // 8-12秒随机
Cache<String, Object> cache = Caffeine.newBuilder()
    .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
    .maximumSize(50_000)
    .recordStats() // 开启统计
    .build();

为什么使用随机过期时间?如果所有数据都在同一时刻过期,会导致大量请求同时打到 Redis,引发缓存雪崩。随机 8-12 秒的过期时间让数据分散过期,流量更加平滑。

image

挑战二:服务重启缓存预热

应用服务器重启后,Caffeine 缓存会丢失。如果预热 6GB 全量数据,需要数分钟时间。但我们只需要预热 60MB 的热点数据,3-5 秒即可完成!

@Component
public class CacheWarmUp implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private PoiMapper poiMapper;
    @Autowired
    private Cache<String, Poi> cache;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("开始预热缓存...");
        long start = System.currentTimeMillis();

        // 只加载热门商家(按访问量排序,取前4万条)
        List<Poi> hotPois = poiMapper.selectHotPoi(40000);
        for (Poi poi : hotPois) {
            cache.put(poi.getId(), poi);
        }

        long cost = System.currentTimeMillis() - start;
        log.info("预热完成,加载 {} 条数据,耗时 {} ms", hotPois.size(), cost);
    }
}

实际运行中,预热 4 万条数据仅需 3 秒左右,对服务启动时间影响很小。

挑战三:GC 性能影响

这是面试官最关心的问题。60MB 数据存放在堆内存中,是否会导致频繁 Full GC?

答案是:不会,因为数据量较小。

我们进行了压力测试,在 4C8G 的服务器上,60MB 的 Caffeine 缓存对 GC 的影响微乎其微。Young GC 频率没有明显变化,Full GC 几乎不会发生。

image

但如果需要缓存几个 GB 的数据,就需要谨慎考虑了。这时候可以使用堆外内存方案:

// 使用堆外内存(需要序列化)
Cache<String, byte[]> cache = Caffeine.newBuilder()
    .maximumWeight(500 * 1024 * 1024) // 500MB
    .weigher((key, value) -> value.length)
    .build();

// 存储时序列化
cache.put(poiId, serialize(poi));

// 读取时反序列化
byte[] bytes = cache.getIfPresent(poiId);
Poi poi = deserialize(bytes);

但这会增加 CPU 开销(序列化/反序列化),需要根据具体场景权衡。对于我们的场景,60MB 数据直接存放在堆内存中已经足够。

完整代码实现

1. Caffeine 配置类

基于 Spring 框架的 Caffeine 配置

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String, Poi> poiCache() {
        Random random = new Random();
        int expireSeconds = random.nextInt(5) + 8; // 8-12秒随机

        return Caffeine.newBuilder()
                // 最大缓存 5 万条
                .maximumSize(50_000)

                // 写入后 8-12 秒过期
                .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)

                // 开启统计
                .recordStats()

                // 移除监听器(可选)
                .removalListener((key, value, cause) -> {
                    log.debug("缓存移除:key={}, cause={}", key, cause);
                })

                .build();
    }
}

2. 多级缓存查询实现

@Service
public class PoiService {
    @Autowired
    private Cache<String, Poi> cache;
    @Autowired
    private RedisTemplate<String, Poi> redisTemplate;
    @Autowired
    private PoiMapper poiMapper;

    public Poi getPoi(String poiId) {
        // 第一层:Caffeine 本地缓存
        Poi poi = cache.getIfPresent(poiId);
        if (poi != null) {
            log.debug("命中 Caffeine 缓存:{}", poiId);
            return poi;
        }

        // 第二层:Redis 缓存
        String redisKey = "poi:" + poiId;
        poi = redisTemplate.opsForValue().get(redisKey);
        if (poi != null) {
            log.debug("命中 Redis 缓存:{}", poiId);
            // 回写到 Caffeine
            cache.put(poiId, poi);
            return poi;
        }

        // 第三层:数据库
        poi = poiMapper.selectById(poiId);
        if (poi != null) {
            log.debug("查询数据库:{}", poiId);
            // 回写到 Redis 和 Caffeine
            redisTemplate.opsForValue().set(redisKey, poi, 1, TimeUnit.HOURS);
            cache.put(poiId, poi);
        }

        return poi;
    }

    // 批量查询(上游系统常用)
    public List<Poi> batchGetPoi(List<String> poiIds) {
        List<Poi> result = new ArrayList<>();
        List<String> missIds = new ArrayList<>();

        // 先从 Caffeine 批量获取
        for (String poiId : poiIds) {
            Poi poi = cache.getIfPresent(poiId);
            if (poi != null) {
                result.add(poi);
            } else {
                missIds.add(poiId);
            }
        }

        if (missIds.isEmpty()) {
            return result;
        }

        // 未命中的从 Redis 批量获取
        List<Poi> redisPois = redisTemplate.opsForValue().multiGet(
            missIds.stream().map(id -> "poi:" + id).collect(Collectors.toList())
        );

        // 回写到 Caffeine
        for (Poi poi : redisPois) {
            if (poi != null) {
                result.add(poi);
                cache.put(poi.getId(), poi);
            }
        }

        return result;
    }
}

3. 缓存预热机制

@Component
public class CacheWarmUp implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private PoiMapper poiMapper;
    @Autowired
    private Cache<String, Poi> cache;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("开始预热缓存...");
        long start = System.currentTimeMillis();

        // 只加载热门商家
        List<Poi> hotPois = poiMapper.selectHotPoi(40000);
        for (Poi poi : hotPois) {
            cache.put(poi.getId(), poi);
        }

        long cost = System.currentTimeMillis() - start;
        log.info("预热完成,加载 {} 条数据,耗时 {} ms", hotPois.size(), cost);
    }
}

4. 缓存监控统计

@Component
public class CacheMonitor {
    @Autowired
    private Cache<String, Poi> cache;

    @Scheduled(fixedRate = 60000) // 每分钟统计一次
    public void monitorCache() {
        CacheStats stats = cache.stats();

        log.info("Caffeine 缓存统计:" +
                "命中率={}, " +
                "命中次数={}, " +
                "未命中次数={}, " +
                "加载成功次数={}, " +
                "加载失败次数={}, " +
                "驱逐次数={}",
                String.format("%.2f%%", stats.hitRate() * 100),
                stats.hitCount(),
                stats.missCount(),
                stats.loadSuccessCount(),
                stats.loadFailureCount(),
                stats.evictionCount()
        );

        // 命中率过低告警
        if (stats.hitRate() < 0.8) {
            log.warn("⚠️ Caffeine 缓存命中率过低:{}", stats.hitRate());
        }
    }
}

实施效果对比

上线后的性能改善非常显著:

指标 优化前(纯Redis) 优化后(Caffeine+Redis) 提升
平均响应时间 5ms 1ms ↓ 80%
P99 响应时间 15ms 3ms ↓ 80%
Redis QPS 10000 1000 ↓ 90%
Redis 网络流量 10 GB/s 1 GB/s ↓ 90%
Caffeine 命中率 - 92% -
服务器成本 需扩容到24台 保持12台 节省50%

image

更重要的是,Redis 的压力降低了 90%,为后续业务增长预留了充足的容量空间。

Caffeine 适用场景分析

通过这个案例,我们总结了 Caffeine 的适用场景,必须同时满足三个条件

image

条件一:热点数据占比小

业务数据必须符合二八定律,少量热点数据承载大部分访问流量。如果数据访问分布均匀,每条数据都可能被访问,那么 Caffeine 就不适合。

判断方法:分析访问日志,查看 Top 1% 的数据占了多少流量。如果超过 80%,就符合条件。

内存占用限制:热点数据缓存后,内存占用不应超过 500MB,否则会影响 GC 性能。

条件二:高 QPS 且单次返回数据量大

Caffeine 的优势在于减少网络开销,因此只有在网络成为瓶颈时才有意义。

QPS 要求:至少 10000+,否则 Redis 完全能够满足需求。

数据量要求:单次请求返回数据 > 100KB,否则网络开销可以忽略不计。

计算公式:网络流量 = QPS × 单次数据量

如果计算出的网络流量超过了 Redis 集群的容量,就应该考虑使用 Caffeine。

条件三:对实时性要求不高

Caffeine 是本地缓存,数据更新后需要等待过期才能生效,因此只适合对实时性要求不高的场景。

可接受的延迟:秒级或分钟级

不适合的场景:强一致性要求(如金融交易、库存扣减、订单状态)

典型场景对比

场景 是否适合 Caffeine 原因
商品详情页 ✅ 非常适合 热点商品少,QPS高,延迟可接受
商家信息(POI) ✅ 非常适合 本文案例
配置信息 ✅ 非常适合 变更少,数据量小
字典数据 ✅ 非常适合 几乎不变,数据量小
用户信息 ⚠️ 看情况 需要考虑一致性
订单信息 ❌ 不适合 强一致性要求
库存信息 ❌ 不适合 实时性要求高
支付信息 ❌ 不适合 强一致性要求

注意事项与最佳实践

image

1. 完善的监控体系

Caffeine 的效果需要通过监控数据来验证。必须监控的关键指标:

@Component
public class CacheMonitor {
    @Autowired
    private Cache<String, Poi> cache;

    @Scheduled(fixedRate = 60000)
    public void monitorCache() {
        CacheStats stats = cache.stats();

        // 命中率
        double hitRate = stats.hitRate();
        log.info("Caffeine 命中率:{}", String.format("%.2f%%", hitRate * 100));

        // 命中率过低告警
        if (hitRate < 0.8) {
            log.warn("⚠️ Caffeine 缓存命中率过低:{}", hitRate);
            // 发送告警通知
        }

        // 驱逐次数
        long evictionCount = stats.evictionCount();
        if (evictionCount > 1000) {
            log.warn("⚠️ Caffeine 驱逐次数过多:{}", evictionCount);
        }
    }
}

2. 降级方案准备

如果 Caffeine 出现异常(如命中率骤降、内存占用过高),需要能够快速切换回 Redis:

@Service
public class PoiService {
    @Value("${cache.caffeine.enabled:true}")
    private boolean caffeineEnabled;

    public Poi getPoi(String poiId) {
        try {
            // 如果 Caffeine 开关关闭,直接查 Redis
            if (!caffeineEnabled) {
                return getFromRedis(poiId);
            }

            // 先查 Caffeine
            Poi poi = cache.getIfPresent(poiId);
            if (poi != null) {
                return poi;
            }

            // 再查 Redis
            poi = getFromRedis(poiId);
            if (poi != null) {
                cache.put(poiId, poi);
            }
            return poi;

        } catch (Exception e) {
            log.error("Caffeine 查询失败,降级到 Redis", e);
            // 降级:直接查 Redis
            return getFromRedis(poiId);
        }
    }

    private Poi getFromRedis(String poiId) {
        String redisKey = "poi:" + poiId;
        Poi poi = redisTemplate.opsForValue().get(redisKey);
        if (poi == null) {
            poi = poiMapper.selectById(poiId);
            if (poi != null) {
                redisTemplate.opsForValue().set(redisKey, poi, 1, TimeUnit.HOURS);
            }
        }
        return poi;
    }
}

配置文件中的开关设置:

cache:
  caffeine:
    enabled: true  # 可以通过配置中心动态调整

3. 防止缓存穿透

对于不存在的数据查询,缓存空对象避免每次请求都访问数据库:

private static final Poi NULL_POI = new Poi(); // 空对象标记

public Poi getPoi(String poiId) {
    // 从 Caffeine 获取
    Poi poi = cache.getIfPresent(poiId);
    if (poi == NULL_POI) {
        return null; // 之前查过,数据不存在
    }
    if (poi != null) {
        return poi;
    }

    // 从 Redis 获取
    poi = getFromRedis(poiId);
    if (poi == null) {
        // 数据不存在,缓存空对象
        cache.put(poiId, NULL_POI);
        return null;
    }
    cache.put(poiId, poi);
    return poi;
}

4. 防止缓存击穿

热点数据过期时,可能瞬间有大量请求访问 Redis。Caffeine 提供 refreshAfterWrite 机制,可以异步刷新缓存:

LoadingCache<String, Poi> cache = Caffeine.newBuilder()
    .maximumSize(50_000)
    .expireAfterWrite(10, TimeUnit.SECONDS)
    // 8秒后异步刷新,不会阻塞请求
    .refreshAfterWrite(8, TimeUnit.SECONDS)
    .build(key -> {
        // 从 Redis 加载数据
        return getFromRedis(key);
    });

这样在数据即将过期时,Caffeine 会异步加载新数据,避免缓存击穿。

5. 合理设置过期时间

过期时间设置需要平衡:

  • 过短(< 5秒): 缓存命中率低,频繁加载数据,失去缓存意义
  • 过长(> 30秒): 数据更新延迟太大,影响用户体验

推荐:8-12 秒随机过期时间,既保证命中率,又控制延迟。

Random random = new Random();
int expireSeconds = random.nextInt(5) + 8; // 8-12秒
Cache<String, Poi> cache = Caffeine.newBuilder()
    .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
    .build();

6. 内存占用监控

虽然只缓存 60MB 数据,但仍需定期检查内存使用情况:

@Scheduled(fixedRate = 300000) // 每5分钟检查一次
public void checkMemory() {
    Runtime runtime = Runtime.getRuntime();
    long totalMemory = runtime.totalMemory();
    long freeMemory = runtime.freeMemory();
    long usedMemory = totalMemory - freeMemory;
    double usedPercent = (double) usedMemory / totalMemory * 100;

    log.info("内存使用情况:总内存={}MB, 已用={}MB, 使用率={}%",
            totalMemory / 1024 / 1024,
            usedMemory / 1024 / 1024,
            String.format("%.2f", usedPercent));

    // 内存使用率超过 80% 告警
    if (usedPercent > 80) {
        log.warn("⚠️ 内存使用率过高:{}%", String.format("%.2f", usedPercent));
    }
}

7. 灰度发布策略

引入 Caffeine 是重要架构变更,建议采用灰度发布:

第一步:在 1-2 台服务器上开启 Caffeine,观察效果 第二步:效果良好后,逐步扩展到 50% 的服务器 第三步:最终全量上线

配置中心控制:

cache:
  caffeine:
    enabled: true
    gray-ratio: 0.1  # 10% 的流量使用 Caffeine
@Service
public class PoiService {
    @Value("${cache.caffeine.gray-ratio:0}")
    private double grayRatio;

    public Poi getPoi(String poiId) {
        // 根据灰度比例决定是否使用 Caffeine
        boolean useCaffeine = Math.random() < grayRatio;

        if (useCaffeine) {
            return getFromCaffeine(poiId);
        } else {
            return getFromRedis(poiId);
        }
    }
}

Caffeine 与 Redis 的选型对比

很多人会问:既然 Caffeine 性能这么好,为什么不完全替代 Redis?答案是:Redis 和 Caffeine 解决的是不同维度的问题。

Redis 的核心价值

Redis 是分布式缓存,所有服务器共享同一份数据,数据更新立即生效,无需考虑同步问题。它可以存储几十 GB 甚至上百 GB 数据,不受应用服务器内存限制。同时支持复杂数据结构(List、Set、Hash、ZSet)、分布式锁、发布订阅等功能,运维体系成熟,具备完善的监控和高可用方案。

简而言之:Redis 稳定、可靠、功能全面,90% 的场景使用它就足够了。

Caffeine 的核心价值

Caffeine 是本地缓存,基于内存访问,响应时间达到纳秒级,没有网络开销和序列化成本。它能让 90% 的请求不经过网络,显著降低 Redis 压力,而且不需要额外服务器,充分利用现有应用服务器内存。

简而言之:Caffeine 速度快、成本低,但只适合特定场景。

技术选型决策逻辑

image

判断标准:

  • 数据量 > 1GB?选择 Redis
  • QPS < 10000?选择 Redis
  • 数据访问分布均匀(无热点)?选择 Redis
  • 要求强一致性?选择 Redis
  • 以上条件都不满足?考虑 Caffeine + Redis 组合

最佳实践:两级缓存架构

在实际项目中,最优方案是 Caffeine + Redis 两级缓存

第一层 Caffeine:缓存热点数据,承载 90% 流量,响应时间 < 1ms 第二层 Redis:缓存全量数据,承载 10% 流量,响应时间 < 5ms 第三层 MySQL:持久化存储,仅在缓存未命中时查询,响应时间 < 50ms

image

这种架构既保证了性能,又确保了数据一致性,同时还控制了成本。记住:Caffeine 不是要替代 Redis,而是要与 Redis 配合使用。

面试回答技巧

回到文章开头的面试场景,现在你知道该如何专业地回答这些问题了吗?

面试官:"为什么不用 Redis?"

欠佳回答:"因为 Caffeine 更快。"

专业回答: "我们的业务场景具有以下特点:POI 服务 QPS 峰值超过 10000,单次请求返回 1MB 数据,需要 10GB/s 的网络带宽,但 Redis 集群只有 3.84GB/s 的容量,网络带宽成为瓶颈。

如果选择 Redis 扩容方案,需要从 6 台服务器扩展到 24 台,成本增加 4 倍。

我们通过分析访问日志发现,1% 的热门商家承载了 90% 的访问流量,符合二八定律。因此引入 Caffeine 缓存这 1% 的热点数据,仅需 60MB 内存就能让 90% 的请求不经过网络,Redis 压力降低 90%。

这样既解决了性能瓶颈问题,又避免了成本的大幅增加。"

面试官:"数据变更如何处理?"

欠佳回答:"使用消息队列通知所有服务器更新缓存。"

专业回答: "商家信息对实时性要求相对宽松,营业时间变更延迟几秒生效是可以接受的。因此我们设置了 8-12 秒的随机过期时间,让缓存自然过期。

使用随机过期时间的原因是:如果所有数据在同一时刻过期,会导致大量请求同时访问 Redis,引发缓存雪崩。8-12 秒的随机过期让数据分散过期,流量更加平滑。

如果业务对实时性要求极高,我们会考虑使用 Redis 的发布订阅机制或消息队列进行数据同步,但这会增加系统复杂度。"

面试官:"服务重启怎么办?"

欠佳回答:"重启后缓存丢失,只能慢慢预热。"

专业回答: "我们只缓存 60MB 的热点数据,服务启动时会自动执行预热流程,从数据库加载访问量最高的 4 万条商家信息,整个过程只需 3-5 秒。

如果缓存全量数据,确实需要较长的预热时间。但由于我们只缓存热点数据,预热速度很快,对服务启动时间影响很小。"

面试官:"会不会导致频繁 Full GC?"

欠佳回答:"应该不会吧..."

专业回答: "我们进行了充分的压力测试,60MB 的 Caffeine 缓存对 GC 的影响可以忽略不计。应用服务器配置为 4C8G,60MB 只占堆内存的不到 1%,Young GC 频率无明显变化,Full GC 几乎不会发生。

如果需要缓存几个 GB 的数据,确实需要考虑 GC 问题。这时候可以使用堆外内存方案,但会增加序列化/反序列化的 CPU 开销。

对于我们的具体场景,60MB 数据直接存放在堆内存中是完全可行的。而且 Caffeine 使用 W-TinyLFU 算法,能够自动淘汰冷数据,避免缓存无限增长。"

核心总结

关键观点

1. 90% 的场景 Redis 已足够 不要为了技术炫技而引入 Caffeine。Redis 简单、可靠、运维成熟,在大多数场景下都是最优选择。

2. Caffeine 不是万能解决方案 只有在 Redis 网络带宽成为瓶颈,且数据访问符合二八定律,且对实时性要求不高的特定场景下,才考虑使用 Caffeine。

3. 三个必要条件必须同时满足

  • 热点数据占比小(< 5%)
  • QPS 高且单次数据量大(网络成为瓶颈)
  • 对实时性要求不高(秒级延迟可接受)

4. 完善的监控和降级机制 引入 Caffeine 后,必须严格监控命中率、内存占用、GC 情况。一旦出现问题,要能够快速切换回 Redis。

典型场景快速参考

场景 推荐方案 原因
电商商品详情 Caffeine + Redis 热点商品少,QPS高,延迟可接受 ✅
外卖商家信息 Caffeine + Redis 本文案例,完美匹配 ✅
系统配置信息 Caffeine 几乎不变,数据量小 ✅
字典数据 Caffeine 变更少,数据量小 ✅
用户基本信息 Redis 数据量大,访问均匀 ⚠️
用户会话信息 Redis 需要分布式共享 ⚠️
订单信息 Redis 强一致性要求 ❌
库存信息 Redis 实时性要求高 ❌
支付信息 Redis 强一致性要求 ❌
实时排行榜 Redis 需要 ZSet 数据结构 ❌

常见问题解答

Q1:Caffeine 是否会导致数据不一致?

A: 会存在数据不一致,但在可控范围内。

Caffeine 作为本地缓存,数据更新后需要等待过期才能生效,最大延迟为 12 秒。如果业务对实时性要求很高(如订单状态、库存信息),就不适合使用 Caffeine。

但对于商家信息、商品详情等对实时性要求不高的场景,12 秒的延迟是可以接受的。

Q2:多台服务器的 Caffeine 缓存如何同步?

A: 不需要主动同步,依靠过期机制即可。

如果要求同步,需要引入消息队列或发布订阅系统,这会显著增加系统复杂度。而且即使进行同步,也无法保证强一致性(存在网络延迟、消息丢失等风险)。

因此最简单的方案是:不进行同步,让缓存自然过期。反正最大延迟只有 12 秒,用户通常感知不到。

Q3:Caffeine 缓存的数据是否会丢失?

A: 会丢失,但这不影响系统功能。

Caffeine 是基于内存的缓存,服务重启后数据会丢失。但我们有 Redis 和数据库作为后备存储,缓存丢失只会影响性能,不会影响系统功能。

而且我们设计了预热机制,服务启动后 3-5 秒即可恢复正常性能。

Q4:Caffeine 适合缓存大对象吗?

A: 不适合。

如果单个对象超过 1MB(如图片、视频等),不建议使用 Caffeine 缓存,原因包括:

  • 内存占用过大
  • 序列化/反序列化开销大
  • 容易引发 Full GC

大对象建议直接存储在 Redis 或对象存储(OSS)中。

Q5:Caffeine 和 Redis 哪个性能更好?

A: Caffeine 性能更好,但不能替代 Redis。

Caffeine 基于内存访问,响应时间达到纳秒级,比 Redis 快约 1000 倍。但 Caffeine 是本地缓存,无法跨服务器共享,也不能保证数据一致性。

因此两者不是竞争关系,而是互补关系。最佳实践是 Caffeine + Redis 的两级缓存架构。

Q6:Caffeine 是否会导致内存溢出?

A: 不会,因为有完善的淘汰机制。

Caffeine 配置了 maximumSize 参数,超过设定大小时会自动淘汰访问频率较低的数据。而且使用 W-TinyLFU 算法,会优先淘汰访问频率低的数据。

只要合理设置 maximumSize 参数,就不会出现内存溢出的问题。

Q7:Caffeine 适合缓存用户信息吗?

A: 需要根据具体情况判断。

如果用户信息变更频率较低(如用户昵称、头像等),可以考虑使用 Caffeine。

如果用户信息变更频率较高(如用户余额、积分等),不建议使用 Caffeine,因为数据一致性问题会比较严重。

Q8:Caffeine 和 Spring Cache 有什么区别?

A: Spring Cache 是抽象层,Caffeine 是具体实现。

Spring Cache 提供了 @Cacheable@CacheEvict 等注解,简化了缓存的使用。底层可以使用 Caffeine、Ehcache、Redis 等多种实现。

如果项目使用 Spring 框架,建议采用 Spring Cache + Caffeine 的组合,代码更加简洁。

技术选型要基于实际业务场景,而不是为了技术炫技。

除非业务场景确实需要,否则不要盲目将 Redis 和 Caffeine 混合使用。

因为这会引入新的数据一致性问题,可能得不偿失。

记住:简单的方案往往是最优的方案。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-1 14:51 , Processed in 0.060969 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表