缓存失效 - 是否有通用解决方案?

时间:2009-07-27 14:45:27

标签: algorithm caching generalization cache-invalidation

  

“计算机科学中只有两个难题:缓存失效和命名事物。”

Phil Karlton

是否存在使缓存无效的通用解决方案或方法;要知道某个条目何时过时,所以您可以保证始终获得最新数据?

例如,考虑一个从文件中获取数据的函数getData()。 它根据文件的最后修改时间对其进行缓存,每次调用时都会检查该文件 然后添加第二个函数transformData(),它转换数据,并在下次调用函数时缓存其结果。它不知道该文件 - 如何添加依赖关系,如果文件被更改,此缓存将变为无效?

每次调用getData()时都可以调用transformData(),并将其与用于构建缓存的值进行比较,但最终可能会非常昂贵。

9 个答案:

答案 0 :(得分:54)

你所说的是终身依赖链接,有一件事依赖于另一件可以在其控制范围之外修改的东西。

如果您有abc的幂等函数,如果ab相同,那么c是相同的,但检查b的成本高于你:

  1. 接受您有时使用过时信息进行操作,并且不要经常检查b
  2. 尽可能快地检查b
  3. 你不能吃蛋糕而是吃它......

    如果您可以在顶部基于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关系相匹配:xbx

    然而,你很可能得到三个输入,其有效性完全独立(或者是循环的),因此不可能进行分层。这意味着标记为// 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)) bSet (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)

我正在基于PostSharpmemoizing functions开展一种方法。我已经通过我的导师了,并且他同意这是一种以内容无关的方式实现缓存的好方法。

每个函数都可以使用指定其到期时间的属性进行标记。以这种方式标记的每个函数都被记忆,结果存储在缓存中,函数调用的哈希值和用作键的参数。我正在使用Velocity作为后端,后端处理缓存数据的分发。

答案 5 :(得分:1)

  

是否有通用的解决方案或方法来创建缓存,以了解条目何时过时,以确保始终获得新数据?

不,因为所有数据都不同。一些数据可能在一分钟后“陈旧”,一些在一小时后,有些可能会好几天或几个月。

关于您的具体示例,最简单的解决方案是为文件设置“缓存检查”功能,您可以从getDatatransformData调用这些功能。

答案 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