Go中的线程安全(Goroutine安全)缓存

ANi*_*sus 3 caching interface thread-safety go lru

问题1

我正在为服务器构建/搜索RAM内存缓存层。它是一个简单的LRU缓存,需要处理并发请求(均获取一个Set)。

我发现https://github.com/pmylund/go-cache声称是线程安全的。

就获取存储的接口而言,这是正确的。但是,如果多个goroutines请求相同的数据,它们都将检索指向同一块内存的指针(存储在接口中)。如果有任何goroutine更改了数据,那么这将不再是非常安全的。

是否有任何可以解决此问题的缓存程序包?


问题1.1

如果对问题1的回答为,那么建议的解决方案是什么?
我看到两个选择:

备选方案1
解决方案:使用值将值存储在包装结构中,sync.Mutex以便每个goroutine在读取/写入数据之前都需要锁定数据。
type cacheElement struct { value interface{}, lock sync.Mutex }
缺点:缓存不知道对数据所做的更改,甚至可能已将其从缓存中删除。一个goroutine可能还会锁定其他goroutine。

备选方案2
解决方案:制作数据的副本(假设数据本身不包含指针)
缺点:每次执行缓存Get时的内存分配都会进行更多的垃圾回收。


对不起,这个问题很复杂。但是您不必全部回答。如果您对问题1有一个好的答案,那对我来说就足够了!

tux*_*21b 5

替代方法2对我来说听起来不错,但请注意,您不必复制每个方法的数据cache.Get()。只要可以将您的数据视为不可变的,就可以同时使用多个读取器来访问它们。

如果您要修改副本,则只需创建一个副本。这种习惯用法称为COW(写时复制),在并发软件设计中非常常见。它特别适合具有高读/写比的场景(就像高速缓存一样)。

因此,无论何时要修改缓存的条目,您基本上都必须:

  1. 创建旧缓存数据的副本(如果有)。
  2. 修改数据(在此步骤之后,应将数据视为不可变的,并且不再可以更改)
  3. 添加/替换缓存中的现有元素。您既可以使用您先前指出的go-cache库(基于锁),也可以编写自己的无锁库来简单地原子交换数据元素的指针。

此时,任何执行cache.Get操作的goroutine 都会获取新数据。但是,现有的goroutine可能仍在读取旧数据。因此,您的程序可能会同时在同一数据的许多不同版本上运行。但是请放心,所有goroutine一旦完成对旧数据的访问,GC就会自动收集它。