Dubbo 的缓存机制了解吗?

#中间件#面试八股

Dubbo 的缓存机制了解吗?

结论先行:Dubbo 的结果缓存(Result Cache)在消费端用 lru、threadlocal、jcache 三种实现缓存 RPC 返回结果,用 cache 参数开启并按方法或参数做 key。它只适合“读多写少、能容忍短暂不一致”的查询接口,写接口与强一致读绝对不能用。

一、三种缓存实现

实现存储位置生命周期命中范围适用场景
lru消费端 JVM 内 LinkedHashMap进程级,默认 1000 条同进程所有线程共享热点字典、配置类查询
threadlocal消费端线程本地线程级仅当前线程单线程内重复查询同一 key
jcacheJSR-107 实现(如 Ehcache)由实现决定,可持久化可跨进程共享需要过期策略与磁盘缓存

二、key 是怎么算的

缓存 key = 方法名 + 参数值序列化结果
  getById(1L)      → getById_1
  listByCity("上海") → listByCity_上海

因此参数必须能稳定序列化,Map、Set 这类无序结构的 key 可能不稳定,导致缓存不命中甚至膨胀。

三、开启方式

@DubboReference(cache = "lru")           // 使用默认容量 1000
private DictService dictService;
// lru 的容量通过 cache.size 参数指定,默认 1000
@DubboReference(cache = "lru", parameters = {"cache.size", "500"})
private DictService dictService;

@DubboReference(cache = "threadlocal")
private ConfigService configService;

方法级更细的控制:

dubbo:
  reference:
    com.demo.DictService:
      cache: lru
      parameters:
        cache.size: 1000
      methods:
        getById:
          cache: lru
          parameters:
            cache.size: 200
        listAll:
          cache: "false"        # 全量列表不走缓存

四、必须知道的三个风险

风险说明缓解
数据不一致提供端数据已更新,消费端仍返回旧值只缓存低频变更数据,或缩短进程重启周期
内存膨胀lru 容量按条数算,大对象会吃掉堆限制容量,避免缓存列表类接口
缓存穿透空结果也被缓存,数据新增后仍返回空对空值设短 TTL,或干脆不缓存空结果

注意:Dubbo 的结果缓存没有 TTL(lru 与 threadlocal 都是),条目只能靠容量淘汰或进程重启失效,这一点常被误解。

五、和本地缓存框架的关系

如果需要 TTL、刷新、统计,通常不用 Dubbo 自带缓存,而是在业务侧引入 Caffeine:

private final Cache<Long, Dict> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .build();

public Dict get(Long id) {
    return cache.get(id, dictService::getById);
}

常见追问

  • 追问:缓存放在消费端还是提供端?Dubbo 的 cache 是消费端缓存,提供端另有更高层的缓存方案(如 Redis)。消费端缓存的好处是省一次网络,坏处是多实例间不一致。
  • 追问:集群环境下会不会不一致?会,每个消费端各自缓存,更新后要等各自容量淘汰,所以只适合几乎不变的数据。
  • 追问:能用在写接口上吗?不能,写操作被缓存后直接不发出请求,属于严重 bug。

小结

  • lru、threadlocal、jcache 三种实现,按 key 与方法缓存返回值。
  • 无 TTL,只有容量淘汰,这是它最容易被误用的地方。
  • 只用于读多写少、可容忍不一致的查询。
  • 需要过期与统计时,用 Caffeine 等专业本地缓存替代。
笔记加载中…