缓存失效算法

4
我在考虑在Web服务器中缓存动态内容。我的目标是通过返回缓存的HTTP响应来桥接整个处理过程,而不必打扰数据库(或Hibernate)。这个问题不涉及选择现有缓存解决方案之间的选择;我目前关注的是失效。
我确定,基于时间的失效根本没有意义:每当用户更改任何内容时,他们都希望立即看到效果,而不是在几秒钟甚至几分钟后才看到。而且,在短时间内(因为大多数数据是特定于用户的),缓存一小部分秒钟是无用的,因为没有针对相同数据的重复请求。
对于每个数据更改,我都会收到一个事件,并可以使用它来使依赖于更改数据的所有内容失效。由于请求是同时发生的,所以存在两个与时间相关的问题:
- 使失效可能太晚了,可能会向已更改数据的客户端提供陈旧的数据。 - 在失效完成后,长时间运行的请求可能会完成,并且其陈旧的数据可能被放入缓存。
这两个问题在某种程度上是相反的。我猜,前者很容易通过针对相同客户端的ReadWriteLock部分序列化请求来解决。所以我们忘记它。
后者更为严重,因为它基本上意味着一个“失效的失效”,并永远(或太久)提供陈旧的数据。
我可以想像一种解决方案,就是在每个请求之后重复失效,以便于在更改发生之前启动。但这听起来相当复杂和耗时。我想知道是否有任何现有的缓存支持这个功能,但我主要感兴趣的是如何实现这个功能。
澄清:
问题是一个简单的竞争条件:
- 请求 A 执行查询并获取结果 - 请求 B 进行了一些更改 - 由于 B,使失效发生 - 延迟某些原因,请求 A 完成 - 请求 A 的过时的响应被写入缓存

缓存系统会与应用程序集成还是分开? - Rei
@Rei 它将被集成得尽可能紧密。 - maaartinus
我不知道一般情况下应该如何实现,但当我需要一个集成缓存系统时,我寻找现有的解决方案,只发现了基于时间的缓存。我决定自己编写,结果比我预期的要简单,并且肯定更有效,因为它没有基于时间的缓存已经解决的问题。那个“数据变更事件”是关键。 - Rei
@Rei 这就是我正在尝试的。我目前遇到的问题是数据更改事件和迟到的响应之间的竞态条件;我刚刚添加了一份澄清。 - maaartinus
2个回答

3
为了解决竞态条件,可以添加时间戳(或计数器),并在设置新的缓存条目时检查此时间戳。这可确保过时的响应不会被缓存。
以下是伪代码:
//set new cache entry if resourceId is not cached
//or if existing entry is stale
function setCache(resourceId, requestTimestamp, responseData) {
    if (cache[resourceId]) {
        if (cache[resourceId].timestamp > requestTimestamp) {
            //existing entry is newer
            return;
        } else
        if (cache[resourceId].timestamp = requestTimestamp) {
            //ensure invalidation
            responseData = null;
        }
    }

    cache[resourceId] = {
        timestamp: requestTimestamp,
        response: responseData
    };
}

假设我们有两个请求要访问同一个资源“foo”:
- 请求A(在00:00:00.000接收)执行查询并获取结果 - 请求B(在00:00:00.001接收)进行一些更改 - 通过调用setCache("foo", "00:00:00.001", null)来使B的无效化生效 - 请求A完成 - 请求A调用setCache("foo", "00:00:00.000", ...)将过时的响应写入缓存,但失败了,因为现有条目更新
这只是基本机制,还有改进的空间。

这听起来非常简单和傻瓜式,我肯定尝试过了...但我不记得是什么让我拒绝它的(一周可能对我的记忆力来说太长了)。现在,我相信它应该可以工作。 - maaartinus
1
这也发生在我身上,所以我不得不强迫自己注释我的代码。我并不总是成功,因为编写代码比编写注释要有趣得多。 :-) - Rei
我已经在此期间修复了它...使用起始日期进行作废并保留无效条目是必要的。那里有一个不同的错误,而我不知何故让我相信作废是罪魁祸首。+++现在,强制缓存保留无效条目直到所有重叠请求完成是个问题,但这是另一个问题。+++赏金肯定是你的,但我会再保留一段时间,也许有人会添加一些有用的想法。 - maaartinus
@maaartinus 当然可以。我很乐意得到新的想法并改进自己的代码。 - Rei

2
我认为你没有意识到(或者不想明确提出)你正在询问缓存同步策略之间的选择。有几种众所周知的策略:“旁路缓存”、“读取透过”、“写入透过”和“写入背后”。例如:在此处阅读:缓存同步策略的初学者指南。它们提供了各种级别的高速缓存一致性(您称之为失效)。 您的选择应该取决于您的需求和要求。 听起来你目前选择了“写入背后”策略(队列或推迟缓存失效)。但是从您的担忧中可以看出,您选择它是错误的,因为您担心高速缓存读取不一致。
因此,您应该考虑使用“旁路缓存”或“读/写透过”策略,因为这些策略提供更好的缓存一致性。它们都是同样东西的不同口味 - 始终保持高速缓存一致。如果您不关心高速缓存一致性,那么好吧,坚持“写入背后”,但这个问题就变得无关紧要了。
从整个架构上看,我永远不会选择引发事件来使高速缓存失效,因为这似乎已成为您业务逻辑的一部分,而它只是基础设施问题。作为读/写操作的一部分使高速缓存失效(或排队失效),而不是在其他地方使其失效。这样可以使高速缓存成为您基础设施的一个方面,而不是其他所有内容的一部分。

感谢提供文章链接,但很遗憾它并不适用于这里。该文章是关于数据缓存,其中发生数据重复(您在数据库中有数据,并且在缓存中有相同的数据)。问题是关于HTTP响应缓存,其中没有重复发生。目的是避免重新执行冗长的处理,如果HTTP响应无论如何都将是相同的。为此目的而设计的单独缓存系统称为Web加速器,但它旨在用于静态内容或具有可接受陈旧度的动态内容。在OP的情况下,任何程度的过时响应都是不可接受的。 - Rei
借用数据缓存术语,这里需要强一致性,因此可比较的策略绝不是“写后”。当然,它们实际上并不可比较,因为数据缓存和HTTP响应缓存是两个不同的东西。如果需要进一步解释,请告诉我。 - Rei
Rei是正确的,但我很感激你的回答,因为它让我重新评估了我的决定。这些事件是通过监听Hibernate提交而自动引发的,并且它们会立即被处理。那里有一些业务逻辑,但很简单:1.事件粗略化,因为当只有请求返回它们的集合时,没有必要单独监视每个单独的项目。2.有些东西不值得缓存,因此可以简单地忽略某些实体的更改。这两点可能是过早的优化。 - maaartinus
好的,我明白了,你们在谈论这个问题的并发性问题。你需要放弃这个问题,因为一致性会破坏可用性,具体来说,你要么必须锁定,要么必须重新执行延迟请求。如果你不想麻烦,就接受最终一致性(是的,这些术语来自分布式应用程序,但也适用于这里)。如果请求A延迟了,是的,它将有一个过时的数据,但你应该接受它。最终,当缓存失效时,请求A下一次将拥有正确的数据。SO对于Feed IMO也是如此。 - Tengiz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接