高性能缓存
本文关键字:缓存 高性能 | 更新日期: 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
对象被许多线程使用。我不想使用锁,因为它会影响性能,在我的情况下,性能比数据一致性更重要。问题是什么最小的同步机制能满足我的需要?也许volatile
或get
和set
中的单个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,可以分为'具有读访问权限的任务'(并发)和'具有写访问权限的任务'(独占)。