在高并发的系统中,使用缓存来提升查询性能是十分必要的。在关系型数据库中(如MySQL)对于高并发处理能力并不是很强,而缓存由于在内存中处理,并不需要磁盘IO,所以非常适合高并发处理。
缓存虽然能显著提升查询效率,但是更新缓存和更新数据库不可能在一个事务中进行,所以就很难保证缓存的一致性。下面我们看看缓存更新都有哪些策略,以及对应的优缺点。
定时刷新
使用定时刷新策略,写入数据库和写入缓存是独立进行的,写入数据库后,需要单独使用定时任务去刷新缓存。这种方式会导致在较长的一段时间内,缓存与数据库数据不一致,而且不管数据是否有更新,都会定时去刷新缓存,效率低下。此方案适用于系统配置模型、或者归档的数据报表等数据不经常改变,或者数据改变对业务影响不是很大的场景。
先更新数据库,在更新缓存
这是最直观想到的,也是最简单的一种刷新缓存策略。
使用这种双写的方案,只要在数据库更新成功,立即更新缓存,但是在并发场景下,容易造成数据不一致。例如线程A和B同时来更新数据库,但更新数据库和更新缓存不能保证原子性,可能出现如下情况:
这中方案存下一下弊端:
1、数据库和缓存中的数据不一致,从而缓存数据成了脏数据。
2、对于写多读少的操作,由于频繁的刷新缓存,而缓存的数据根本没有被读取过,增加写负担和服务资源浪费。
先删缓存再更新数据库
由于双写存在的问题,那我们考虑先删除缓存,再更新数据库,并且不主动更新缓存,等到再次查询的时候写入缓存。这样在更新数据库前,由于缓存中的数据被删除,这是请求查询缓存中不存在,再去查询数据库,然后将数据放到缓存中,缓存就不会频繁的被刷新了。
这种方案是否完美了呢?同样有两个请求,请求A去写数据,请求B去读数据,就会存一下一下问题:
这种情况下,脏数据又进入缓存了,如果没有设置过期时间,那么在下一次写入数据之前,脏数据会一直存在。针对这种脏数据的出现,我们决定在写入数据后,增加一点延迟,再删除一次数据,于是就有了下面的延迟双删策略。
延迟双删
延迟双删策略,能够很好的解决之前我们应对并发引起的数据不一致的情况。那是不是延迟双删就没有问题了呢?
我们再来看一个场景,针对做了读写分离场景,使用双删会出现什么问题呢?
糟糕,由于主从同步有延迟,又导致数据不一致了。再从性能角度查看,由于二次删除需要延迟,只能做成异步。那异步线程执行删除就会出现新的问题,如果异步线程删除失败了,那么旧数据就不会被删除,数据又不一致了。在此基础上,考虑使用可靠消息队列保证删除缓存业务百分百执行。
队列删除缓存
我们把数据删除更新到数据库中,把删除缓存的消息加入到队列中,如果消息投递失败,就再次加入到队列执行,直到成功为止。
这样,我们就能够有效保证数据库和缓存数据不一致了,不管是读写分离还是其他情况,只要消息队列能够保证安全,那么缓存就一定会被刷新。(在发生删除消息前,Master需要等Slave节点发生ACK,否则在消息消费了,单主从并未完成同步,还是去从库读取旧值,可以参考下MySQL半同步复制)
根据这个方案,还可以进一步优化。因为我们是基于业务代码进行缓存刷新的,导致业务代码和刷新缓存耦合度很高。那有没有办法能将刷新缓存抽取出来,不基于业务代码执行呢?
binlog订阅删除缓存
我们都知道,MySQL主从数据同步,或者canal进行数据同步都是基于binlog日志文件进行同步的。
使用binlog订阅,我们就完美的将业务代码和缓存刷新独立开了。代码量小了,也方便维护了,程序员不需要关系是否需要刷新缓存。
当然,实战中,我们还有很多不同的业务场景,可能需要的数据不一致同步方案也不同,你在工作中都是要到哪些缓存同步方案呢?