如上图所示,假设A、B两个线程,A先更新数据库后 B再更新数据库,然后分别进行更新缓存,但是B先更新缓存成功,A后更新缓存成功,这样就导致数据库是最新的数据但是缓存中是旧的脏数据。而如果失效缓存数据的话,可以保证下一次读请求回源到数据库将最新的数据载入到缓存中,避免脏数据的问题。因此,针对数据更新缓存采用失效的方式进行处理,也可以参考这篇文章《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》。
❷ 双写不同数据源容易造成数据不一致:同时写数据库以及缓存数据,任何一个更新失败都会造成数据不一致,由于“物理失败”造成的数据不一致在下一个章节进行阐述。另外事务都成功,无论是先更新缓存还是再更新数据库,还是先更新数据库再更新缓存,这两种情况在并发的情况下也很容易出现双写不成功,操作时序如下图,这种方式不推荐。
❸ 先更新缓存再更新数据库
❹ 先更新数据库再更新缓存
❺ 违背数据懒加载,避免不必要的计算消耗:如果有些缓存值是需要经过复杂的计算才能得出,所以如果每次更新数据的时候都更新缓存,但是后续在一段时间内并没有读取该缓存数据,这样就白白浪费了大量的计算性能,完全可以后续由读请求的时候,再去计算即可,这样更符合数据懒加载,降低计算开销。
2)可能存在的更新时序?
在确定数据更新后缓存会失效来进行处理的话,针对数据库以及缓存更新时序就存在如下这几种:
❶ 先失效缓存再更新数据库
❷ 假设在并发的情况下,按照这种更新时序会存在什么问题? 如时序图所示,线程A先失效缓存数据的时候,B线程读请求发现缓存数据为空的话,就会从数据库中读取旧值放入到缓存中,这样就导致后续的读请求读到的都是缓存中的脏数据。针对这样的情况可以采用延时双删的策略来有效避免,伪代码 如下:
cache.delKey(key); db.update(data); Thread.sleep(xxx); cache.delKey(key);
主要是在写请求更新完数据库后进行休眠一段时间,然后删除可能由读请求带来的脏数据存入到缓存。另外,数据库如果采用的是主从分离的架构的话,读出来的数据也有可能是主从未同步完成造成的脏数据。这种通过延时双删的方式需要线程休眠,因此很显然会降低系统吞吐量,并不是一种优雅的解决方式,也可以采用异步删除的方式。当然可以设置过期时间,到期后缓存失效载入最新的数据,需要系统能够容忍一段时间的数据不一致。
❸ 先更新数据库再失效缓存 :这是推荐的更新数据时采用的方式,实际上这也是可能存在数据不一致的情况,时序图如下: