“计算机科学中只有两个难题:缓存失效和命名事物。”
Phil Karlton
是否存在使缓存无效的通用解决方案或方法;要知道某个条目何时过时,所以您可以保证始终获得最新数据?
例如,考虑一个从文件中获取数据的函数getData()
。
它根据文件的最后修改时间对其进行缓存,每次调用时都会检查该文件
然后添加第二个函数transformData()
,它转换数据,并在下次调用函数时缓存其结果。它不知道该文件 - 如何添加依赖关系,如果文件被更改,此缓存将变为无效?
每次调用getData()
时都可以调用transformData()
,并将其与用于构建缓存的值进行比较,但最终可能会非常昂贵。
答案 0 :(得分:54)
你所说的是终身依赖链接,有一件事依赖于另一件可以在其控制范围之外修改的东西。
如果您有a
,b
到c
的幂等函数,如果a
和b
相同,那么c
是相同的,但检查b
的成本高于你:
b
b
你不能吃蛋糕而是吃它......
如果您可以在顶部基于a
对其他缓存进行分层,那么这会影响最初的问题而不是一点。如果你选择1,那么你拥有自己给予的任何自由,因此可以缓存更多,但必须记住考虑b
的缓存值的有效性。如果您选择2,则必须每次都检查b
,但如果a
签出,则可以退回b
的缓存。
如果您对高速缓存进行分层,则必须考虑是否由于组合行为而违反了系统的“规则”。
如果你知道a
b
总是有效,那么你就可以安排你的缓存(伪代码):
private map<b,map<a,c>> cache //
private func realFunction // (a,b) -> c
get(a, b)
{
c result;
map<a,c> endCache;
if (cache[b] expired or not present)
{
remove all b -> * entries in cache;
endCache = new map<a,c>();
add to cache b -> endCache;
}
else
{
endCache = cache[b];
}
if (endCache[a] not present) // important line
{
result = realFunction(a,b);
endCache[a] = result;
}
else
{
result = endCache[a];
}
return result;
}
显然,连续分层(比如x
)是微不足道的,只要在每个阶段,新添加的输入的有效性与a
的{{1}}:b
关系相匹配:x
和b
:x
。
然而,你很可能得到三个输入,其有效性完全独立(或者是循环的),因此不可能进行分层。这意味着标记为// important的行必须更改为
if(endCache [a] 过期或不存在)
答案 1 :(得分:14)
缓存失效的问题是,在我们不知道的情况下,东西会发生变化。因此,在某些情况下,如果有其他事情可以了解并可以通知我们,则可以采用解决方案。在给定的示例中,getData函数可以挂接到文件系统,该文件系统确实知道对文件的所有更改,而不管文件的进程是什么,并且该组件反过来可以通知转换数据的组件。
我认为没有任何一般的魔法修复可以解决问题。但在许多实际情况中,很可能有机会将基于“轮询”的方法转换为基于“中断”的方法,这可能会使问题消失。
答案 2 :(得分:3)
如果你每次进行转换时都要使用getData(),那么你已经消除了缓存的全部好处。
对于您的示例,似乎解决方案是在生成转换后的数据时,还要存储文件的文件名和最后修改时间(您已将数据结构存储在已返回的数据结构中)通过getData(),您只需将该记录复制到transformData()返回的数据结构中,然后再次调用transformData()时,检查文件的上次修改时间。
答案 3 :(得分:3)
恕我直言,功能反应式编程(FRP)在某种意义上是解决高速缓存失效的一般方法。
原因如下:FRP术语中的陈旧数据称为glitch。 FRP的目标之一是确保没有故障。
此'Essence of FRP' talk及此SO answer更详细地解释了FRP。
在talk中,Cell
表示缓存的对象/实体,如果刷新其中一个依赖项,则刷新Cell
。
FRP隐藏与依赖关系图关联的管道代码,并确保没有陈旧的Cell
。
我能想到的另一种方式(与FRP不同)是将计算值(类型为b
)包装到某种作者Monad Writer (Set (uuid)) b
中Set (uuid)
(Haskell表示法) )包含计算值b
所依赖的可变值的所有标识符。因此,uuid
是某种唯一标识符,用于标识计算出的b
所依赖的可变值/变量(例如数据库中的行)。
将这个想法与在这种编写器Monad上运行的组合器相结合,如果你只使用这些组合器来计算新的b
,那么这可能会导致某种通用缓存失效解决方案。这样的组合器(比如filter
的特殊版本)将Writer monads和(uuid, a)
- s作为输入,其中a
是可变数据/变量,由uuid
标识。
所以每次你改变&#34;原作&#34;数据(uuid, a)
的数据b
(比如计算b
的数据库中的规范化数据)依赖于b
类型的计算值,然后您可以使包含a
的缓存无效如果你改变计算出的b
值所依赖的任何值Set (uuid)
,因为基于Writer Monad中的uuid
,你可以判断这种情况何时发生。
因此,无论何时使用给定的b
变异,您都会将此变异广播到所有缓存中,并使值uuid
无效,这些值依赖于使用{{1}标识的可变值因为b
被包装的Writer monad可以判断b
是否依赖于所述uuid
。
当然,如果你阅读的次数比你写的要多得多,这只会得到回报。
第三种实用的方法是在数据库中使用物化视图,并将它们用作缓存。 AFAIK他们还旨在解决失效问题。这当然限制了将可变数据连接到派生数据的操作。
答案 4 :(得分:2)
我正在基于PostSharp和memoizing functions开展一种方法。我已经通过我的导师了,并且他同意这是一种以内容无关的方式实现缓存的好方法。
每个函数都可以使用指定其到期时间的属性进行标记。以这种方式标记的每个函数都被记忆,结果存储在缓存中,函数调用的哈希值和用作键的参数。我正在使用Velocity作为后端,后端处理缓存数据的分发。
答案 5 :(得分:1)
是否有通用的解决方案或方法来创建缓存,以了解条目何时过时,以确保始终获得新数据?
不,因为所有数据都不同。一些数据可能在一分钟后“陈旧”,一些在一小时后,有些可能会好几天或几个月。
关于您的具体示例,最简单的解决方案是为文件设置“缓存检查”功能,您可以从getData
和transformData
调用这些功能。
答案 6 :(得分:1)
没有一般解决方案,但是:
您的缓存可以充当代理(拉)。假设您的缓存知道最后一次原始更改的时间戳,当有人呼叫getData()
时,缓存会询问原点是否有最后一次更改的时间戳,如果相同,则返回缓存,否则它用源1更新其内容并返回其内容。 (变体是客户端直接发送请求的时间戳,如果时间戳不同,源只会返回内容。)
您仍然可以使用通知进程(推送),缓存观察源,如果源更改,它会向缓存发送通知,然后标记为&#34;脏&#34;。如果有人调用getData()
,缓存将首先更新到源,请删除&#34; dirty&#34;旗;然后返回其内容。
一般来说,选择取决于:
getData()
上的多次调用更喜欢推送,以避免源被getTimestamp函数淹没注意:由于使用时间戳是http代理正在工作的传统方式,另一种方法是共享存储内容的哈希值。我知道两个实体一起更新的唯一方法是我打电话给你(拉)或者你叫我......(推)这一切。
答案 7 :(得分:0)
缓存很难,因为你需要考虑: 1)缓存是多个节点,需要对它们达成共识 2)无效时间 3)多次获取/设置发生时的竞争条件
这是很好的阅读: https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
答案 8 :(得分:-2)
也许缓存无关的算法将是最通用的(或者至少,依赖于较少的硬件配置),因为它们将首先使用最快的缓存并从那里继续。这是麻省理工学院的一个讲座:Cache Oblivious Algorithms