双写一致性
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
注意这里是对数据库进行写操作而不是读操作,通常我们有两种方式完成这个写操作,分别是:先删除缓存再修改数据库 和 先修改数据库再删除缓存,但是这两种方式都存在问题(出现脏数据)。
先删除缓存再修改数据库:
先删除缓存再修改数据库的话,如果在 线程1 删除缓存后有 线程2 对数据进行了查询(此时数据库中的数据仍然是旧数据,且缓存中已经没有数据)并将查询的值写入缓存,那么缓存中存放的就是旧数据,此时 线程1 也完成了对数据库的更新,那么就会出现数据库是新数据而缓存是旧数据的情况。
先修改数据库再删除缓存:
我们这里讨论一种特殊情况,就是缓存中的数据过期了,此时 线程1 去查缓存查不到,就会去访问数据库(此时数据库是旧数据),突然 线程2 横插一脚,它来执行了一个更新数据库的操作,把数据库的数据更新为了新数据,并去删除缓存(虽然缓存中没有数据,不过不影响),线程2 执行完毕后 线程1 继续执行,把它访问数据库(旧数据)的数据更新到缓存中,这样就出现了缓存是旧数据而数据库是新数据的情况。
为了减少脏数据的出现,我们可以使用下面这种方法
延迟双删:
在删除缓存、修改数据库后再次进行一次缓存删除,注意要延时(因为数据库也可能有子节点,需要给时间给主从节点的数据同步),不过延时的时间并不能确定,所以依然有脏数据出现的可能,但是也已经极大的控制了这个可能性。
如果我们的系统有强一致性的要求,就需要使用分布式锁来实现了
分布式锁:
这种方法呢就保证了强一致性,但是性能较差,我们可以用读写锁(ReadWriteLock)优化一下
共享锁readLock,只限制写操作,加了这个锁后不影响别人的读操作(读操作时添加,读读不互斥,读写互斥)
独占锁writeLock,读写都影响(写操作时添加)
毕竟是加锁,其实这种方法的性能也是不高,不过保证了强一致性,在我们对强一致性要求不高(也许延迟一致的时候),可以使用下面两种异步通知的方法。
基于MQ的异步通知:
基于Canal的异步通知:
这种方法更好一点,canal会伪装成mysql的一个从节点,不会对代码进行侵入。
两种异步通知的方法当然都是存在短暂的延迟的,不能保证强一致性,都是最终数据都会变成一致的,我们可以按需选择自己的方法。