高性能缓存

本文关键字:缓存 高性能 | 更新日期: 2023-09-27 17:50:45

下面的代码应该缓存最后一次读取。LastValueCache是一个可以被许多线程访问的缓存(这就是我使用共享内存的原因)。对我来说,有竞争条件是可以的,但我希望其他线程能够看到变化的LastValueCache

class Repository
{
    public Item LastValueCache
    {
        get
        {
            Thread.MemoryBarrier();
            SomeType result = field;
            Thread.MemoryBarrier();
            return result;
        }
        set
        {
            Thread.MemoryBarrier();
            field = value;
            Thread.MemoryBarrier();
        }
    }
    public void Save(Item item)
    {
        SaveToDatabase(item);
        Item cached = LastValueCache;
        if (cached == null || item.Stamp > cached.Stamp)
        {
            LastValueCache = item;
        }
    }
    public void Remove(Timestamp stamp)
    {
        RemoveFromDatabase(item);
        Item cached = LastValueCache;
        if (cached != null && cached.Stamp == item.Stamp)
        {
            LastValueCache = null;
        }
    }
    public Item Get(Timestamp stamp)
    {
        Item cached = LastValueCache;
        if (cached != null && cached.Stamp == stamp)
        {
            return cached;
        }
        return GetFromDatabase(stamp);
    }
}

Repository对象被许多线程使用。我不想使用锁,因为它会影响性能,在我的情况下,性能比数据一致性更重要。问题是什么最小的同步机制能满足我的需要?也许volatilegetset中的单个MemoryBarrier就足够了?

高性能缓存

如果这是愚蠢的,你不需要投票给我。
告诉我,我会删掉的。
但我并不遵循这种逻辑。

public void Save(Item item)
{
    SaveToDatabase(item);
    Item cached = LastValueCache;
    if (cached == null || item.Stamp > cached.Stamp)
    {
        LastValueCache = item;
    }
}

你担心内存毫秒数,但是你在更新缓存之前正在等待写数据库。
基于公共项目Get戳记是一个键。

让我们假设一个数据库写是20毫秒
一个db读取是10ms
Cache get和Cache set各为2ms

public void Save(Item Item)
SaveToDatabase(项);20 ms
项目缓存= LastValueCache;2
女士If (cached == null || item。Stamp> cached.Stamp) 1 ms
LastValueCache = item;2ms

在LastValueCache =项目之前的23毫秒内;任何对public Item Get(Timestamp stamp)的调用都将访问数据库而不是缓存。

在LastValueCache =项目之前的23毫秒内;对公共项目LastValueCache的任何调用Get将会得到一个陈旧了23ms的值。声明的目标是让其他线程看到LastValueCache——但是它们看到的是一个过时的LastValueCache。

和Remove一样。
您将对数据库进行几次本可以避免的访问。

你想达到什么目标?
你做过侧写吗?

我打赌瓶颈是对数据库的调用。
数据库调用比锁和MemoryBarrier之间的差长1000倍。

public void Save(Item item)
{   
   // add logic that the prior asynchonous call to SaveToDatabase is complete
   // if not wait for it to complete 
   // LastValueCache will possible be replaced so you need last item in the database 
   // the time for a lock is not really a factor as it will be faster than the prior update 
   Item cached = LastValueCache;
   if (cached == null || item.Stamp > cached.Stamp)
   {
       LastValueCache = item;
   }
   // make the next a task or background so it does not block    
   SaveToDatabase(item);
}

甚至可以改变逻辑,只等待先前的调用如果你设置LastValueCache = item;
但是你需要以某种方式限制数据库

下一步是缓存最后一个X,并在Item Get(Timestamp stamp)
中使用它。数据库调用是你需要优化的
同样,您需要配置

之后,逻辑将变得更复杂,但将数据库调用提供给BlockingCollection。需要确保最后一个X缓存大于BlockingCollections的大小。如果不阻塞,等待BC清除。您还需要对插入和删除操作使用相同的BC,以便按顺序处理它们。可以聪明一点,不插入有删除操作的记录。不要一次只插入或删除一条记录。

我实现了一个为并发工作负载设计的线程安全伪LRU: ConcurrentLru。性能非常接近ConcurrentDictionary,比MemoryCache快10倍,命中率比传统的LRU好。下面的github链接提供了完整的分析。

用法如下:

int capacity = 666;
var lru = new ConcurrentLru<int, SomeItem>(capacity);
var value = lru.GetOrAdd(1, (k) => new SomeItem(k));

GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

volatile对于您的情况应该是足够的,并且性能最好。在某些情况下,仅volatile不足以确保读取得到最新的值,但在这种情况下,我认为这不是一个重要因素。有关详细信息,请参阅Joe Albahari撰写的这篇很棒的文章中的"volatile关键字"部分。

ReaderWriterLockSlim类的另一种用途可能是为像您这样的读写器场景启用线程同步。

    ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
    get
    {
        _rw.EnterReadLock();
        SomeType result = field;
        _rw.ExitReadLock();
        return result;
    }
    set
    {
        _rw.EnterWriteLock();
        field = value;
        _rw.ExitWriteLock();
    }

这是读写同步的有效实现。Eric Lippert的这篇博客可能会让你感兴趣。

另一个有趣的选项可能是ConcurrentExclusiveSchedulerPair类,它为任务创建两个TaskScheduler,可以分为'具有读访问权限的任务'(并发)和'具有写访问权限的任务'(独占)。