如何保证缓存和数据库的一致性?原来没办法保证啊。。。
常见的缓存更新策略
1. Cache aside 旁路缓存。
比如我们平时使用的Redis就属于旁路缓存。 读请求先从缓存中读,读不到再去数据库中读,然后写入缓存。
写请求:
根本原因在于更新缓存和数据库的操作不是原子性的,中间会有其他请求来插一脚,容易出现并发问题。
如果换个思路,先更新数据库再更新缓存。
数据库与缓存不一致的原因也是一样的,根本原因在于不是原子性操作。换句话说只要能保证这俩操作的原子性,就能够保证强一致,那么很容易想到分布式锁,拿到锁的线程才能够去更新数据库,可是真的能解决问题吗??
虽然分布式锁可以解决缓存数据库不一致的问题,可是又出现了新问题:无法并发写,存在性能问题,而且分布式锁自身也存在很多问题。
所以比较好的思路是:查的时候写缓存,更新数据库的时候直接删除缓存。
那如果先删除缓存再更新数据库呢?
还是会出现读写不一致的问题。
既然这种方法不行,那么我们如果先删除缓存,等一会再删除缓存呢?
延迟双删:
具体操作就是更新数据时,先更新缓存在更新数据库,线程等待一会再删除缓存。留给其他线程先读数据库然后再写入缓存的时间。
那么问题来了,要等待多久?另外一个线程才能够完成这个读数据库写缓存的这个操作呢?要考虑网路情况、服务器负载、数据库负载等。
还是换个思路吧:先更新数据库,再删除缓存。
仍然会出现缓存数据库不一致的情况(笔者写到这里已经有点崩溃了。。。)。
但是注意,这种情况发生的概率非常小,必须是这个key刚好过期然后又刚好要更新数据库,第二个条件是请求2刚更新完数据库并删除缓存,这个请求1才写入缓存。也就是说写缓存的速度要慢于更新数据库+删除缓存的速度,这个概率是非常低的。所以先更新数据库再删除缓存这个操作在绝大多数情况下都是可行的。但是如果更新数据库后删除缓存失败,还是会出现不一致的情况。
解决方法:删除操作放入MQ,异步删除缓存,失败就重试。还可以监听数据库的binlog,更新数据库成功就会产生一条binlog,监听binlog然后删除缓存,删除失败就重试,这也能解决问题,优点是完全不侵入业务代码。
总结:先更新数据库,然后通过MQ或者监听binlog的方式去异步的删除缓存,删除失败了就重试,这种方案比较好。 但是在实际生产环境中,如果我们使用的是分布式数据库,主库只写,从库只读,主从延迟,还是会出现不一致的的情况,即使这种概率非常低,如果非要解决这个问题,我们可以在更新主库的一段时间内,强制的让读请求去读主库,但是会增大主库压力,得不偿失。。。
所以,想要保证数据库和缓存一致性,几乎是不可能的,只能尽可能的去保证。最好就是给缓存加上过期时间,不至于脏数据一直存在。
特点: 业务代码既要操作缓存也要操作数据库,以数据库的数据为主,缓存只是暂时存储数据而已。
2. Read through/Write through(读穿写穿)
设计思想:不去直接操作数据库,只操作缓存,让缓存自身操作数据库。读穿指的是每次读都要读缓存,如果有数据就返回,如果没有数据,缓存自身去数据库中读,然后写入缓存。写穿指的是每次写数据的时候就写缓存,缓存自身去更新数据库。 优点:只需要操作缓存,读写操作由缓存中间件去完成。
3. Write写回(异步写缓存)
设计思想:与读穿、写穿思想差不多,只操作缓存,然后让缓存自身去操作数据库。不一样的点在于写回是批量、异步的去写数据库。写操作只更新缓存,让后台线程去异步、批量的把缓存中的更新写入数据库。非常适合写多读少的场景。 缺点:异步写数据库,如果发生宕机会有数据丢失的风险。
无论是读穿写穿、写回,目前做业务的缓存比如Redis、memcache或者一些大厂自研的缓存中间件,都没有提供缓存自身和数据库交互的这样一种功能,但是这种思想大量应用到了操作系统、中间件的底层设计。比如MySQL的buffer pool。MySQL的插入数据操作都是先写到buffer pool里面然后在某个时刻异步的刷入到磁盘中,MySQL通过redolog避免数据的丢失。又比如操作系统的page cache,本身也是缓存,写数据先写到page cache,然后由操作系统在某个时间异步的把page cache数据刷入到磁盘中。很多业务系统也有异步刷盘的思想,比如现在的很多文档编辑器或者画图软件,只要不点保存,它就会在某个时间自动的帮你保存到磁盘,或者自动的帮你保存到云端,这也是异步写回的思想。
