Dubbo 的缓存机制了解吗?
结论先行:Dubbo 的结果缓存(Result Cache)在消费端用
lru、threadlocal、jcache三种实现缓存 RPC 返回结果,用cache参数开启并按方法或参数做 key。它只适合“读多写少、能容忍短暂不一致”的查询接口,写接口与强一致读绝对不能用。
一、三种缓存实现
| 实现 | 存储位置 | 生命周期 | 命中范围 | 适用场景 |
|---|---|---|---|---|
lru | 消费端 JVM 内 LinkedHashMap | 进程级,默认 1000 条 | 同进程所有线程共享 | 热点字典、配置类查询 |
threadlocal | 消费端线程本地 | 线程级 | 仅当前线程 | 单线程内重复查询同一 key |
jcache | JSR-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 等专业本地缓存替代。