如何在Caffeine LoadingCache中安全存储ByteBuf以避免竞态条件?
如何在Caffeine LoadingCache中安全存储ByteBuf以避免竞态条件?
这个问题我之前也踩过坑——Netty的ByteBuf靠引用计数管理生命周期,而Caffeine的异步驱逐机制刚好会在你拿到buf但还没来得及retain的间隙,把buf给释放了,直接导致手里的实例变成无效状态。
给你两个可行的解决方案,其中第一个更稳妥:
方案一:用Netty的retainIfAccessible()原子性保住有效引用
Netty专门提供了retainIfAccessible()方法,它会尝试增加引用计数,但只有当buf还处于可用状态(引用计数大于0)时才会成功。我们可以利用这个特性来规避竞态:
ByteBuf buffer; while (true) { buffer = cache.get("hello"); // 尝试原子性retain,成功说明buf还活着 if (buffer.retainIfAccessible()) { break; } // 失败的话,说明buf已经被释放了,赶紧把这个无效的缓存条目清掉,重新加载 cache.invalidate("hello"); } // 现在可以放心用buffer了 // 用完别忘了调用buffer.release()释放自己的引用
这个逻辑的关键是:哪怕缓存刚好在你get完buf之后就触发了驱逐,retainIfAccessible()也能检测到buf已经失效,这时候我们主动失效缓存,让下一次get重新生成一个新的ByteBuf,直到成功拿到可用的实例并完成retain。
方案二:调整缓存内ByteBuf的引用计数生命周期(有局限性)
你也可以让缓存本身持有ByteBuf的一个引用,确保缓存持有期间buf的引用计数至少为1,这样即使被驱逐,也不会直接把计数降到0:
- 加载时先给缓存留个引用:在
CacheLoader里创建完ByteBuf后,先调用一次retain(),让缓存持有这个buf的一个引用:
LoadingCache<String, ByteBuf> cache = Caffeine.newBuilder() .maximumSize(128) .evictionListener((key, buf, cause) -> buf.release()) // 缓存驱逐时释放自己持有的引用 .build(key -> { ByteBuf byteBuf = createByteBufFromPool(); // 从对象池创建buf byteBuf.retain(); // 缓存占一个引用位 return byteBuf; });
- 自己拿的时候再占一个引用:从缓存拿到buf后,立刻调用
retain():
ByteBuf buffer = cache.get("hello").retain(); // 使用buffer... buffer.release(); // 用完释放自己的引用
不过要注意,这个方案还是有小概率的竞态:如果get返回buf后,缓存立刻触发驱逐,listener的release()会把计数从1降到0,这时候你再调用retain()还是会失败。所以这个方案只能降低概率,不能完全避免问题,还是方案一更可靠。
备注:内容来源于stack exchange,提问作者James Goodall
相关产品推荐
相关产品推荐

