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

问题描述

| 请考虑以下两种用伪代码编写的方法,它们分别提取并更新复杂的数据结构:
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)
}
在上述实现中,并发存在问题,我们可能最终会在缓存中获取过时的数据:考虑分别运行consider1ѭ和
updateData()
的两个并行执行。如果第一个执行正好在另一个执行对
storeDataStructureInDatabase()
clearCache()
调用之间从缓存中获取数据,那么我们将获得该数据的过时版本。您如何解决这个并发问题? 我考虑了以下解决方案,其中在提交数据之前使缓存无效:
storeDataStructureInDatabase(id,data) {
   executesql(\"UPDATE table1 SET...\")
   executesql(\"UPDATE table2 SET...\")
   executesql(\"UPDATE table3 SET...\")
   clearCache(id)
   executesql(\"COMMIT\")
}
但是再说一遍:如果一个执行在另一个执行对
clearCache()
COMMIT
调用之间读取了缓存,则过时的数据将被提取到缓存中。问题没有解决。     

解决方法

用缓存的方式思考,您不能阻止检索过时的数据。 例如,当某人开始发送HTTP请求(如果您的应用程序是Web应用程序),该请求稍后将使缓存无效时,我们是否应该在POST请求启动时认为缓存无效?该请求何时由您的服务器处理?当您启动控制器代码时?好吧实际上,仅当数据库事务结束时,缓存才无效。即使在事务的COMMIT阶段仅在事务开始时也没有结束。并且任何处理先前数据的工作过程都很少有机会知道数据已更改,在Web应用程序中,在浏览器中显示过时数据的html页面如何处理,您是否要刷新这些页面? 但是,让我们认为您的并行过程不仅适用于Web,而且适用于真正的并发关键并行作业。 一个问题是您的缓存不是由数据库服务器处理的,因此不在事务COMMIT / ROLLBACK中。您无法决定先清除高速缓存,但如果回滚,则必须重建高速缓存。因此,您只能在提交事务后清除并重建缓存。 如果您的获取介于数据库提交和缓存清除指令之间,则可能导致获取过时的缓存版本。因此: 拥有过时的缓存版本真的重要吗?假设您的并行处理仅花费了几毫秒的时间,您就可以检索到这个新版本(因此它是旧版本)并使用它大约40毫秒,然后就此生成最终报告而没有注意到工作结束前15毫秒已清除缓存。如果您的流程响应不能包含任何过时的数据,那么您必须在输出数据之前检查数据的有效性(因此,您应重新检查工作流程中使用的所有数据到最后仍然有效)。 因此,如果您不想重新检查数据的有效性,这意味着您的进程在启动时应该已经放置了一些锁(信号量?),而仅在工作结束时才释放该锁,则您正在对您的工作进行序列化。数据库可以通过在事务的伪序列化级别上进行处理来加快序列化,并在发生任何更改使伪序列化变得很麻烦时中断事务。但是在这里,您不仅要使用数据库,还应该自己进行序列化。 进程序列化的速度很慢,但是您可以尝试与数据库做相同的事情,即并行运行作业,并在数据更改后使正在运行的任何作业无效(因此,要有一些可以检测到您的缓存的文件并清除并重新运行所有现有的并行作业,表示您掌握了所有并行作业的内容) 或简单地接受您可以拥有少量过去无效且过时的数据。如果我们谈论Web应用程序,那么您的响应基于TCP / IP到达客户端浏览器的时间可能已经无效。 您可能会接受使用过时的缓存数据。唯一真正重要的一点是,如果您不能为真正重要的事情信任缓存数据,则不应为此使用缓存。例如,如果您正在处理会计数据。获得并行任务序列化的唯一方法是: 在写过程中:所有重要的读操作(将获得一些写操作)和事务中具有高隔离级别(第4级)和所有必要的行锁的所有写操作。仅使用数据库很难做到这一点,如果添加用于读取操作的外部缓存,这是完全不可能的。 并行读取过程:如果读取的数据不会用于写入操作,请执行所需的操作(从外部缓存读取)。如果稍后将其中一个读取数据用于写入操作,则必须在写入事务中检查此数据的有效性(因此在写入过程中)。为什么不在数据上添加时间戳水印,这样当它返回进行写操作时,您将能够知道它是否仍然有效。