如何在并发关键上下文中使用内存缓存

时间:2011-06-15 11:21:22

标签: performance concurrency memcached

考虑以下两种用伪代码编写的方法,它们分别获取复杂的数据结构并对其进行更新:

getData(id) {
   if(isInCache(id)) return getFromCache(id)         // already in cache?
   data = fetchComplexDataStructureFromDatabase(id)  // time consuming!
   setCache(id, data)                                // update cache
   return data
}

updateData(id, data) {
   storeDataStructureInDatabase(id, data)
   clearCache(id)
}

在上面的实现中,并发存在问题,我们可能最终得到缓存中过时的数据:考虑分别运行getData()updateData()的两个并行执行。如果第一次执行从另一个执行调用storeDataStructureInDatabase()clearCache()之间准确地从缓存中获取数据,那么我们将获得过时的数据版本。你会如何解决这个并发问题?

我考虑了以下解决方案,其中缓存在提交数据之前无效:

storeDataStructureInDatabase(id, data) {
   executeSql("UPDATE table1 SET...")
   executeSql("UPDATE table2 SET...")
   executeSql("UPDATE table3 SET...")
   clearCache(id)
   executeSql("COMMIT")
}

但话又说回来:如果一个执行在另一个执行调用clearCache()COMMIT之间读取缓存,那么过时的数据将被提取到缓存中。问题没有解决。

1 个答案:

答案 0 :(得分:2)

在缓存思维方式中,您无法阻止检索过时的数据。

例如,当有人开始发送HTTP请求(如果您的应用程序是Web应用程序),稍后将缓存无效时,我们是否应该在POST请求启动时认为缓存无效?当您的服务器处理请求时?当你启动控制器代码?那么没有。实际上只有在数据库事务结束时缓存才有效。甚至在事务开始时,仅在结束时,在事务的COMMIT阶段。使用以前数据的任何工作流程很少有机会意识到数据已更改,在Web应用程序中,显示浏览器中显示过时数据的html页面,您是否要刷新这些页面?

但是,让我们认为您的并行流程不仅适用于Web,而且适用于真正的并发关键并行作业。

一个问题是您的缓存不是由数据库服务器处理的,因此它不在事务COMMIT / ROLLBACK中。您无法首先清除缓存,但如果您回滚则重建缓存。因此,您只能在提交事务后清除并重建缓存

如果你的get介于数据库提交和缓存清除指令之间,那么就有可能获得一个过时的缓存版本。所以:

  • 你有一个过时的缓存版本真的很重要吗?假设你的并行进程在你检索这个新版本之前只有几毫秒就做了一些事情(所以它是旧的版本)并且可以使用它大约40ms,然后构建最终报告而不注意缓存已经刷新了15ms之前工作结束了。如果您的流程响应不能包含任何过时数据,那么您必须在输出之前检查数据有效性(因此您应该重新检查工作流程中使用的所有数据在结束时是否仍然有效)。
  • 因此,如果您不想重新检查数据有效性,这意味着您的流程应该在启动时设置一些锁定(信号量?),并且只应在工作结束时释放锁定, 序列化您的工作。数据库可以通过处理事务的伪序列化级别来加速序列化,并且如果任何更改导致此伪序列化为非法,则会中断事务。但是在这里,您不仅要使用数据库,还应该自己进行序列化。
  • 进程序列化很慢,但您可能会尝试执行与数据库相同的操作,即并行运行作业并使数据更改时运行的任何作业无效(因此,有一些内容可以检测到缓存清除并终止并重新运行所有现有数据并行工作,暗示你掌握了所有并行工作的成果)
  • 只是接受您可以使用过去无效的小数据。如果我们谈论Web应用程序时您的响应在TCP / IP上走向客户端浏览器的时间可能已经无效。

您可能会接受使用过时的缓存数据。唯一非常重要的一点是如果您不能信任您的缓存数据以获得真正关键的东西,那么您就不应该使用缓存。例如,如果您正在操作会计数据。获得并行任务序列化的唯一方法是:

    写作过程中的
  • :所有重要读物(将获得一些写入的读物)和所有写东西交易中具有高隔离级别(级别4)以及所有必需的行锁。仅使用数据库很难做到这一点,如果为读取操作添加外部缓存,则完全不可能。
  • 并行读取过程:如果读取数据不用于写入操作,则执行您想要的操作(从外部缓存读取)。如果稍后将其中一个读取数据用于写入操作,则必须在写入事务中检查此数据有效性(因此在写入过程中)。 为什么不在数据上添加时间戳水印,这样当它返回进行写入操作时,您将能够知道它是否仍然有效。