缓存与数据库一致性问题深度剖析
既然说到了缓存,那就先聊一下什么样的数据适合用缓存,缓存的利与弊。
缓存适合量大但又不常变化的数据。对于那些经常变化的数据,其实并不适合缓存,一方面会增加系统的复杂性(缓存的更新,缓存脏数据),另一方面也给系统带来一定的不稳定性(缓存系统的维护)。
上缓存的优点:
- 能够缩短服务的响应时间,给用户带来更好的体验。
- 能够增大系统的吞吐量,依然能够提升用户体验。
- 减轻数据库的压力,防止高峰期数据库被压垮,导致整个线上服务垮掉。
上缓存的缺点:
- 缓存系统也要考虑分布式,比如redis的分布式缓存还会有很多坑,无疑增加了系统的复杂性。
- 在特殊场景下,如果对缓存的准确性有非常高的要求,就必须考虑缓存和数据库的一致性问题。
网上搜索很多资料,大部分观点认为,做缓存不应该是去更新缓存,而是应该删除缓存,然后由下个请求去缓存,发现不存在后再读取数据库,写入缓存。
下面分析为什么是删除缓存而不是更新缓存,
1:线程安全角度
试想,若同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就导致了脏数据,因此不考虑。
2:业务场景角度
如果业务场景是度很频繁的场景,但是读的操作不是那么的频繁,就会导致每次更新数据库都会更新缓存,而缓存并没有没读去,造成了性能的浪费。(虽然这种场景并不会使用推荐使用缓存,哈哈哈。)
那么不更新缓存,删除缓存就不会出现上面的问题了吗?并不是这样的,那么是该先操作缓存,还是该先操作数据库呢?
下面我们就分析一下两种场景,1:先删除缓存,在更新数据库。2:先更新数据库,在删除缓存。
1:先删除缓存,在更新数据库
试想一下,若同时有一个请求A进行更新操作,另一个请求B进行查询操作。则可能会出现:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
那么将会造成脏数据,如果没有设置缓存数据的过期时间,那么脏数据会一直在缓存中。
2:先更新数据库,在删除缓存
试想一下,若同时有两个请求,一个请求A做查询操作,一个请求B做更新操作,则可能会出现:
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
可见,此方式依旧不能保证数据库与缓存的一致性。但是发生这种情况的前提是操作(3)比操作(2)更快,但是一般情况下数据库的写操作都是比读的操作慢的,发生这种情况的概率很低。
那么就没有一种方案保持缓存与数据库的一致性了吗?有当然是有的,参阅网上的很多资料之后发现了延时双删策略,这种方案总共有三步,
1:先删除缓存
2:更新数据库
3:休眠一段时间(具体时间多长,是业务逻辑耗时而定),再次删除缓存(如果删除失败,重试再次删除)。
如果使用读写分离架构会不会出现数据库与缓存不一致呢?答案是可能的?让我们来看一下原因。
试想一下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
上述情形,就是数据不一致的原因。解决办法还是使用双删延时策略。只要把第二次的延时删除时间为在主从同步的延时时间基础上,加上一段时间就不会出现问题了。但是这样可能会影响我们系统的响应速度,我们加缓存的目的是为了减少系统的响应时间,现在加上睡眠时间不仅没有没有增加系统的响应速度,反而是我们的系统变得更加复杂了。
我们到底该何去何从,这个缓存还是必要的吗?网上有方案是讲第二次延时删除更改为异步删除,这样系统的响应速度就会大大提升,这样确实可以解决,但是我们系统的功能的开发复杂度将会大大提升。思虑再三,还是觉得这种方案可行是可行,但是开发成本确实有点大,而且系统的复杂度也骤然上升了一个层次。
无意间看到了一个大佬提出了一种方案,既不影响原有的而系统开发,也不会增加系统开发的复杂度。我个人觉得这是一种非常棒的方案。
这里附上大佬的链接:https://xie.infoq.cn/article/47241d099404a1565e168fad4
大佬的方案是:业务逻辑无需对缓存做任何操作,只需要做正常的数据库操作即可。然后我们启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作,这个方案不仅解决了数据库缓存不一致的问题,也解决了我们题外话中的问题,可谓一举两得。
这里附上大佬的流程图:
上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。至于oracle中,博主目前不知道有没有现成中间件可以使用。
总结:本文其实是对目前博主了解的数据库与缓存一致性方案做了一个总结,至于大家选择哪种方案,要根据系统的需求而定,避免对系统的架构过度设计,切不可认为最复杂的就是最好的,只有适合自己的才是最好的,最后,希望大家有所收获。
参考链接:
https://xie.infoq.cn/article/47241d099404a1565e168fad4
https://coolshell.cn/articles/17416.html