为什么要写这篇文章?首先,缓存以其高并发、高性能的特点,在项目中得到了广泛的应用。对于读取缓存,大家没有疑惑,都是按照下图的流程进行业务操作。但是在更新缓存方面,更新完数据库之后,是更新缓存还是删除缓存。或者先删除缓存,再更新数据库。其实大家之间也有很多争论。目前还没有综合性的博客分析这些解决方案。所以博主冒着被大家喷的风险写了这篇文章。文中先作说明。理论上,为缓存设置一个过期时间是保证最终一致性的一种解决方案。在这种方案下,我们可以为缓存中存储的数据设置一个过期时间,所有的写操作都以数据库为准,我们只对缓存操作尽力而为。也就是说,如果数据库写入成功,缓存更新失败,只要到了过期时间,后续的读请求自然会从数据库中读取新的值,然后回填缓存。因此,接下来讨论的思路不依赖于为缓存设置过期时间的方案。这里讨论三种更新策略:先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存没人问我为什么没有更新缓存这种策略首先,然后更新数据库。一般反对先更新数据库,再更新缓存。为什么?有两个原因。原因1(从线程安全的角度)如果同时有请求A和B进行更新操作,线程A更新数据库,线程B更新数据库,线程B更新缓存,线程A更新缓存,然后请求A更新缓存应该比请求B早更新缓存是对的,但是由于网络等原因,B更新缓存比A早,这样就造成了脏数据,是因此不予考虑。第二个原因(从业务场景来看)有以下两点:如果你有一个写数据库场景多,读数据场景少的业务需求,采用这种方案会导致缓存先阻塞甚至读取数据。频繁更新会浪费性能。如果你写入数据库的值不是直接写入缓存,而是经过一系列复杂的计算后才写入缓存。然后,每次写入数据库后,写入缓存的值都会重新计算,这无疑是一种性能浪费。显然,删除缓存更合适。接下来的讨论是争议最大的,先删除缓存,再更新数据库。或者先更新数据库,再删除缓存。先删除缓存,再更新数据库。此解决方案将导致不一致。同时有一个更新操作的请求A,还有一个查询操作的请求B。那么就会出现如下情况:请求A执行写操作,删除缓存,请求B查询发现缓存不存在,请求B查询数据库获取旧值,请求B写旧值放入缓存,请求A将新值写入数据库。上述情况会导致不一致的情况发生。而且,如果不采用缓存设置过期时间的策略,数据永远是脏数据。那么,如何解决呢?采用延迟双删策略的伪代码翻译成中文描述如下:先清除缓存再写入数据库(这两个步骤同前)sleep1秒,再次清除缓存.这样一来,缓存中1秒内产生的脏数据就可以被清除掉了。再删除。那么,这个1秒是怎么确定的,应该休眠多长时间呢?针对以上情况,请读者评估自己项目读取数据业务逻辑的耗时。那么,写数据的休眠时间可以根据读数据耗时的业务逻辑增加几百毫秒。这样做的目的是保证读请求结束,写请求可以删除读请求造成的缓存脏数据。如果使用mysql的读写分离架构呢?ok,这样的话,数据不一致的原因如下,还是有两个请求,一个请求A进行更新操作,一个请求B进行查询操作。请求A执行写操作,删除缓存请求A将数据写入数据库,请求B查询缓存发现缓存中没有值请求B从数据库中查询。此时主从同步还没有完成,所以查询的是旧值RequestB将旧值写入缓存数据库完成主从同步,从库更改为新值。以上情况是造成数据不一致的原因。还是用双删延迟策略。但是修改了sleep时间,主从同步的延迟时间增加了几百ms。采用这种同步淘汰策略,如果吞吐量降低了怎么办?好的,然后将第二个删除设置为异步。自己开一个线程,异步删除。这样写的请求就不需要sleep一段时间就可以返回了。这样做会增加吞吐量。第二次删除,删除失败怎么办?这是一个很好的问题,因为第二次删除失败,出现了下面的情况。还有两个请求,一个请求A执行更新操作,一个请求B执行查询操作。为了方便,假设是单数据库:请求A执行写操作,删除缓存,请求B查询发现缓存不存在,请求B查询数据库获取旧值请求B将旧值写入缓存请求A将新值写入数据库请求A尝试删除请求B写入缓存值,但失败了。ok,也就是说。如果第二次缓存删除失败,缓存和数据库不一致的问题又会出现。如何解决?具体解决办法,看博主对(3)更新策略的分析。先更新数据库,再删除缓存首先先说一下。老外提出了一个缓存更新例程,叫做《Cache-Aside pattern》。其中,指出失败:应用先从缓存中取数据,如果没有,再从数据库中取数据,成功后,放入缓存中。***:应用程序从缓存中取数据,取完后返回。update:先将数据存入数据库,成功后再使缓存失效。另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们也采用了先更新数据库再删除缓存的策略。这种情况不存在并发问题吗?不。假设会有两个请求,一个请求A进行查询操作,一个请求B进行更新操作,那么就会出现如下情况:缓存刚好失败请求A查询数据库,得到一个旧值请求B写入newvalueintothedatabaseRequestBdeletecacherequestA将找到的旧值写入缓存ok。如果出现上述情况,确实会出现脏数据。然而,这种情况发生的可能性有多大?上述情况的发生有一个先天条件,即步骤(3)中的写数据库操作比步骤(2)中的读数据库操作耗时少,这样就可以使步骤(4)先于步骤(5)。不过仔细想想,数据库的读操作比写操作快多了(不然为什么要做读写分离,读写分离的意思是因为读操作速度更快,占用资源更少),所以步骤(3)比步骤(2)花费的时间更少,这种情况很少见。假设,有人非要吵架,得了强迫症,非要解决怎么办?如何解决上面的并发问题呢?首先,为缓存设置一个有效时间是一个解决方案。其次,采用策略(2)中给出的异步延迟删除策略,保证在读请求完成后执行删除操作。不一致还有其他原因吗?是的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的问题。删除缓存失败怎么办?不会有矛盾吗?比如写数据的请求写入数据库,删除缓存失败,就会出现不一致的情况。这也是缓存更新策略(2)最后留下的问题。怎么解决?提供有保证的重试机制就足够了。这里有两组解决方案。方案一:下图所示流程更新数据库数据如下;缓存由于各种问题删除失败,将要删除的key发送到消息队列自己消费消息,得到要删除的key。继续重试删除操作,直到成功。但是,这种解决方案的缺点是对业务代码造成大量入侵。于是就有了第二个方案。方案二,启动一个订阅程序,订阅数据库的binlog,获取需要操作的数据。在应用程序中,启动另一个程序从订阅程序中获取信息,并删除缓存。方案二:流程如下图所示:更新数据库数据数据库将操作信息写入binlog日志,订阅程序提取需要的数据和key。再创建一个非业务代码获取信息并尝试删除缓存操作。如果删除失败,则将信??息发送到消息队列,从消息队列中检索数据并重试操作。备注:上面的binlog订阅程序在mysql中有一个现成的中间件canal,可以完成订阅binlog日志的功能。至于oracle,博主目前不知道有没有现成的中间件可以使用。另外,博主对重试机制使用了消息队列的方式。如果对一致性的要求不是很高,就在程序中另外开启一个线程,每隔一段时间重试即可。这些大家可以自由灵活的使用,只是提供一个思路。总结这篇文章其实是对网上现有的一致性方案的一个总结。关于先删除缓存再更新数据库的更新策略,也有维护一个内存队列的方案。博主看了看,觉得实现起来极其复杂,也没有必要,所以文章里没必要给出。***,希望大家有所收获。
