从图中可以看到,Tomcat 上运行的应用,无论是新增(Insert 操作)、修改(Update 操作)、还是删除(Delete 操作)数据 X,都会直接在数据库中增改删。当然,如果应用执行的是修改或删除操作,还会删除缓存的数据 X。
如果是新增数据,数据会直接写到数据库中,不用对缓存做任何操作,此时,缓存中本身就没有新增据,而数据库中是最新值,此时,缓存和数据库的数据是一致的。
如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性。
先删除缓存值后更新数据库值
:数据库更新失败,导致请求再次访问缓存时,发现缓存缺失,在读数据库时,从数据库中读到的是旧值先更新数据库,后删除缓存值
:缓存删除失败,导致请求再次访问缓存时,发现缓存命中,并从缓存中读到旧值。重试机制
:借助消息队列,可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。缓存操作成功后,删除消息队列,防止重复消费导致一致性问题先删除缓存,再更新数据库
假设线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取。这会带来两个问题:
等到线程 B 从数据库读取完数据、更新了缓存后,线程 A 才开始更新数据库,此时,缓存中的数据是旧值,而数据库中的是最新值,两者就不一致了。
延迟双删
:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。
要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。所以,线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。
伪代码
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
先更新数据库值,再删除缓存值
如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。