当系统引入缓存的时候,同时也就会引入数据库和缓存数据不一致的问题,因为两个数据,不管是先更新哪一个,这两个操作之间都会有一定的时间差,在高并发的情况下,就会有读取数据不一致,甚至会导致数据错误的问题。
一些缓存方案
一般来说处理缓存数据的方案有这样几种:
- 全量缓存,靠定时任务去同步缓存;
- 先更新缓存,再更新数据库;
- 先更新数据库,再更新缓存;
- 先删除缓存,再更新数据库;
- 先更新数据库,再删除缓存;
全量更新
这种方式很简单无脑,就是直接将数据库中的数据全都存放到缓存中,然后通过后台的定时任务定时去更新缓存数据,读数据的时候直接读缓存,写的时候就写数据库,简单高效,但是也存在着很多问题:
- 数据冗余,很多无效数据也都会再缓存中,占用内存空间,当数据量大的时候这部分开销就不容忽视了;
- 数据不一定,因为依靠的是定时任务定时去更新数据,也就是说在下次定时任务之前,读到的都是老数据;
- 如果更新数据较多,且定时任务执行比较频繁,可能出现上一次的任务还没执行完成,下一次的任务就开始了。
所以这种方式只适合数据体量比较小,且数据更新不太频繁的场景。
先更新缓存,再更新数据库
首先要明确这是两个操作,并且这两个操作不具备原子性,也就是说可能出现一个操作执行失败,另一个操作执行成功的情况。 如果先更新缓存执行成功了,但是更新数据库执行异常了,这样当缓存中的数据被淘汰清除之后,就会导致数据丢失了,显然这种问题是不能被接受的。 并且我们还要考虑并发问题,假设两个线程的执行顺序是这样的:
- 线程A执行了更新缓存操作;
- 线程B执行了更新缓存操作;
- 线程B执行了更新数据库操作;
- 线程A执行了更新数据库操作;
这样缓存和数据库中的数据就不一致的,我们在缓存中读到的永远都是旧数据(线程A更新的数据)。
先更新数据库,再更新缓存
这种跟先更新缓存区别不大,也会导致数据不一样,但是不会有数据丢失的情况,毕竟是先更新的数据库,失败了就是失败了,也会返回失败的结果。
先删除缓存,再更新数据库
因为少了一次更新操作,数据都是从数据库中读取的,所以少了两次更新操作带来的数据不一致的问题,但是它还是有并发问题,假设两个线程的执行顺序是:
- 线程A删除了缓存;
- 线程B读取缓存,发现缓存中没有数据,就从数据库中读取了;
- 线程B将读到的数据写入了缓存中;
- 线程A更新了数据库。
这样就导致了数据不一致,缓存中是旧数据,且一直都是旧数据,系统读到的也是旧数据直到缓存失效重新从数据库中读取数据。
先更新数据库,再删除缓存
这种方式其实也有并发问题,比如:
- 缓存中先是没有数据的,线程A从数据库中读取了数据;
- 线程B更新了数据库,并且删除了缓存数据;
- 线程A将原来的数据写到了缓存中。
但是这种情况产生的概率会小一些,要满足多种条件:
- 缓存原先是没有数据的或者缓存刚好失效;
- 刚好有读写并发;
- 线程A的两个操作时间间隔很长,而一般情况下,数据库的读操作要比写操作要快(涉及到加锁什么的),所以这个条件是不太容易满足的。
所以先更新数据库,再删除缓存是我们一般选择的方式,但是其实这种方式也是有问题:当删除缓存失败的时候,也会导致数据不一致的,缓存中的数据是旧数据,所以这里需要引入重试机制,比如可以向mq中发送一条删除缓存的消息,做一个异步重试删除缓存的操作,或者还有一个方案就是订阅数据库的binlog日志,应用程序专门订阅binlog日志,根据binlog日志的变化来维护缓存中的数据,阿里的canal中间件就提供了订阅binlog日志的功能。
缓存延迟双删策略
因为先更新数据库,再删除缓存也会有数据不一致的情况,所以有人就提出了延迟双删策略,就是你不是会把旧值写入缓存吗,那我就延迟一些,等下再来删除一次缓存,这样就能保证下次读取到的肯定是新值了,但是这个延迟时间不太好评估,需要根据实际项目去评判了。
|