缓存失效算法
Cache invalidation algorithm
我正在考虑 在 Web 服务器中缓存动态内容。我的目标是通过返回缓存的 HTTP 响应而不打扰 DB(或 Hibernate)来桥接整个处理过程。这个问题不是关于在现有缓存解决方案之间进行选择;我目前担心的是失效。
我敢肯定,基于时间的失效根本没有意义:每当用户更改任何内容时,他们都希望立即看到效果,而不是几秒钟甚至几分钟。并且缓存几分之一秒是无用的,因为在这么短的时间内没有重复请求相同的数据(因为大部分数据都是用户特定的)。
对于每次数据更改,我都会收到一个事件,并可以使用它根据更改的数据使所有内容无效。由于请求同时发生,有两个与时间相关的问题:
- 失效可能来得太晚,过时的数据甚至可能会提供给更改它们的客户。
- 失效完成后,长 运行 请求可能会完成,其陈旧数据可能会被放入缓存。
这两个问题有点相反。我想,前者很容易通过使用每个客户端 ReadWriteLock
部分序列化来自同一客户端的请求来解决。所以还是算了。
后者更为严重,因为它基本上意味着失效失效并永远(或太长)提供陈旧数据。
我可以想象一个解决方案,比如在更改发生之前每个请求开始后重复失效,但这听起来相当复杂和耗时。我想知道是否有任何现有的缓存支持这一点,但我主要感兴趣的是一般情况下这是如何完成的。
澄清
问题是一个简单的竞争条件:
- 请求A执行查询并获取结果
- 请求 B 做了一些更改
- 发生B引起的失效
- 请求 A(无论出于何种原因被延迟)完成
- 请求 A 的 过时 响应被写入缓存
要解决竞争条件,请添加时间戳(或计数器)并在设置新的缓存条目时检查此时间戳。
这确保过时的响应不会被缓存。
这是一个伪代码:
//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
};
}
假设我们收到了 2 个对同一资源的请求 "foo":
- 请求 A(在 00:00:00.000 收到)执行查询并获取结果
- 请求 B(在 00:00:00.001 收到)做了一些更改
- B 导致的失效通过调用
setCache("foo", "00:00:00.001", null)
发生
- 请求 A 完成
- 请求 A 调用
setCache("foo", "00:00:00.000", ...)
将过时的响应写入缓存但失败,因为现有条目较新
这只是基本机制,还有改进的空间。
我认为您没有意识到(或不想明确指出)您在询问缓存同步策略之间的选择。有几种众所周知的策略:"cache aside"、"read through"、"write through" 和 "write behind"。例如阅读此处:A beginner’s guide to Cache synchronization strategies。它们提供各种级别的缓存一致性(您称之为无效)。
您的选择应取决于您的需要和要求。
到目前为止,您似乎选择了 "write behind" 策略(排队或延迟缓存失效)。但是从您的顾虑来看,这听起来像是您选择不正确,因为您担心缓存读取不一致。
因此,您应该考虑使用 "cache aside" 或 "read/write through" 策略,因为它们提供更好的缓存一致性。它们都是同一事物的不同风格 - 始终保持缓存一致。如果您不关心缓存一致性,那么好吧,继续 "write behind",但是这个问题变得无关紧要了。
在架构范围内,我永远不会通过引发事件来使缓存无效,因为看起来您已经将其作为业务逻辑的一部分,而它只是一个基础架构问题。作为 read/write 操作的一部分,而不是在其他地方,使缓存无效(或队列无效)。这让缓存成为基础设施的一个方面,而不是其他一切的一部分。
我正在考虑 在 Web 服务器中缓存动态内容。我的目标是通过返回缓存的 HTTP 响应而不打扰 DB(或 Hibernate)来桥接整个处理过程。这个问题不是关于在现有缓存解决方案之间进行选择;我目前担心的是失效。
我敢肯定,基于时间的失效根本没有意义:每当用户更改任何内容时,他们都希望立即看到效果,而不是几秒钟甚至几分钟。并且缓存几分之一秒是无用的,因为在这么短的时间内没有重复请求相同的数据(因为大部分数据都是用户特定的)。
对于每次数据更改,我都会收到一个事件,并可以使用它根据更改的数据使所有内容无效。由于请求同时发生,有两个与时间相关的问题:
- 失效可能来得太晚,过时的数据甚至可能会提供给更改它们的客户。
- 失效完成后,长 运行 请求可能会完成,其陈旧数据可能会被放入缓存。
这两个问题有点相反。我想,前者很容易通过使用每个客户端 ReadWriteLock
部分序列化来自同一客户端的请求来解决。所以还是算了。
后者更为严重,因为它基本上意味着失效失效并永远(或太长)提供陈旧数据。
我可以想象一个解决方案,比如在更改发生之前每个请求开始后重复失效,但这听起来相当复杂和耗时。我想知道是否有任何现有的缓存支持这一点,但我主要感兴趣的是一般情况下这是如何完成的。
澄清
问题是一个简单的竞争条件:
- 请求A执行查询并获取结果
- 请求 B 做了一些更改
- 发生B引起的失效
- 请求 A(无论出于何种原因被延迟)完成
- 请求 A 的 过时 响应被写入缓存
要解决竞争条件,请添加时间戳(或计数器)并在设置新的缓存条目时检查此时间戳。 这确保过时的响应不会被缓存。
这是一个伪代码:
//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
};
}
假设我们收到了 2 个对同一资源的请求 "foo":
- 请求 A(在 00:00:00.000 收到)执行查询并获取结果
- 请求 B(在 00:00:00.001 收到)做了一些更改
- B 导致的失效通过调用
setCache("foo", "00:00:00.001", null)
发生
- 请求 A 完成
- 请求 A 调用
setCache("foo", "00:00:00.000", ...)
将过时的响应写入缓存但失败,因为现有条目较新
这只是基本机制,还有改进的空间。
我认为您没有意识到(或不想明确指出)您在询问缓存同步策略之间的选择。有几种众所周知的策略:"cache aside"、"read through"、"write through" 和 "write behind"。例如阅读此处:A beginner’s guide to Cache synchronization strategies。它们提供各种级别的缓存一致性(您称之为无效)。
您的选择应取决于您的需要和要求。
到目前为止,您似乎选择了 "write behind" 策略(排队或延迟缓存失效)。但是从您的顾虑来看,这听起来像是您选择不正确,因为您担心缓存读取不一致。
因此,您应该考虑使用 "cache aside" 或 "read/write through" 策略,因为它们提供更好的缓存一致性。它们都是同一事物的不同风格 - 始终保持缓存一致。如果您不关心缓存一致性,那么好吧,继续 "write behind",但是这个问题变得无关紧要了。
在架构范围内,我永远不会通过引发事件来使缓存无效,因为看起来您已经将其作为业务逻辑的一部分,而它只是一个基础架构问题。作为 read/write 操作的一部分,而不是在其他地方,使缓存无效(或队列无效)。这让缓存成为基础设施的一个方面,而不是其他一切的一部分。