保证缓存与数据库的数据一致性不是很容易

灵魂拷问
  • 保证缓存和数据库的一致性很简单吗?
  • 有哪些方式能保证缓存和数据库的一致性呢?
  • 若是发生了缓存和数据库数据不一致的状况怎么办呢?

在上篇文章咱们介绍了缓存的定义分类以及优缺点等,若是还没看的同窗能够移步这里node

据说你会缓存?mysql

当咱们的系统引入缓存组件以后,性能获得了大幅度提高,可是随之而来的是代码须要引入必定的复杂度,好比缓存的更新策略,写入策略,过时策略等,而其中最可能致使程序员加班的莫过于缓存和数据库的一致性问题了,既:缓存中的数据和数据库中的数据不一致。程序员

一致性问题

说到一致性问题,这算是分布式系统中不可避免的一个痛点,或者说分布式系统自然就自带了数据一致性问题,虽然能够利用不少分布式事务解决方案来作到一致性,可是实际的系统架构设计中,我仍是推崇避免分布式事务。缓存和数据库数据的一致性在产生原理上和分布式相似,其实能够把他们两个的关系看作是分布式系统中的两个操做节点。redis

凡是处于不一样物理位置的两个操做,若是操做的是相同数据,都会遇到一致性问题

产生数据一致性问题的根本缘由是对一个数据的多个操做过程,缓存和数据库数据的一致性也是这个原理,系统中最多见的操做流程是这样的:算法

  • 数据的请求首先查询缓存中是否存在该数据
  • 若是数据命中缓存(在缓存中存在)则直接返回数据,若是数据没有命中缓存(缓存中不存在),则去数据库中取数据
  • 从数据库中取回数据,而后把数据写入缓存

好图

从图中能够清楚的看到,对数据库的操做和对缓存的操做是两个不一样阶段的操做,在任何一个操做过程当中都会发生线程安全问题。好比说:sql

  • 当两个线程同时查询缓存的时候,可能会发生两个线程都没有命中缓存的问题
  • 若是两个线程都没有命中缓存就会发生同时查询数据库的问题
  • 接着就会发生两个线程同时回写缓存的问题

而这还不是最致命的,毕竟两个线程同时查询数据库,同时回写缓存数据在多数状况下缓存数据和数据库数据还能保持一致。最要命的是若是是两个线程都进行更新操做,最多见的更新过程是先更新数据库,而后更新缓存。下面就以最多见的用户积分场景为例,每一个用户都有本身的积分,假如发生如下过程:数据库

  • 线程A根据业务会把用户id为1的积分更新成100
  • 线程B根据业务会把用户id为1的积分更新成200
  • 在数据库层面,线程A和线程B确定不存在并发状况,由于数据库用锁来保证了ACID(假如是mysql等关系型数据库),不管数据库中最终的值是100仍是200,咱们都假设正确。
  • 假设线程B在A以后更新数据库,则数据库中的值为200
  • 线程A和线程B在回写缓存过程当中,极可能会发生线程A在线程B以后操做缓存的状况(由于网络调用存在不肯定性),这个时候缓存内的值会被更新成100,发生了缓存和数据库不一致的状况
经过以上案例可见,解决缓存和数据库数据不一致的根本解决方案是须要把两个操做合并成逻辑上能保证事务的一个操做

两个操做看作一个操做

分布式锁

在平时开发中,利用分布式锁可能算是比较常见的解决方案了。利用分布式锁把缓存操做和数据库操做封装为逻辑上的一个操做能够保证数据的一致性,具体流程为:设计模式

  • 每一个想要操做缓存和数据库的线程都必须先申请分布式锁
  • 若是成功得到锁,则进行数据库和缓存操做,操做完毕释放锁
  • 若是没有得到锁,根据不一样业务能够选择阻塞等待或者轮训,或者直接返回的策略

image

利用分布式锁是解决分布式事务的一种方案,可是在必定程度上会下降系统的性能,并且分布式锁的设计要考虑到down机和死锁的意外状况,而最多见的分布式锁就是利用redis,可是也会有很多坑,具体能够参考以前的文章缓存

redis作分布式锁可能不那么简单安全

删除缓存

相对于分布式锁的方案,而程序员实际中最喜欢使用的仍是删除缓存的方式,在一个可能会发生不一致的场景下,咱们会以数据库为主,在操做完数据库以后,不去更新缓存,而是删除缓存。这在必定意义上至关于只操做数据库,把须要维护的两个数据源变成了一个数据源。

image

这种方式要求必须先操做数据库,后操做缓存,否则的话发生不一致的概率会大不少。为何这么说呢?由于就算是先操做数据库也会有发生不一致的概率,可是毕竟在整个操做过程当中,删除缓存的操做只占整个流程时间的一小部分而已,并且咱们能够利用缓存的过时时间来保证数据的最终一致性,因此在一些能够容忍数据短暂不一致的场景下能够采用这种方案的。

删除缓存方案带来的另一个劣势是:若是一样的数据会被频繁更新,缓存会被频繁删除,当有读请求的时候又会被频繁的从数据库加载,因此这种方案适用于那种对缓存命中率不敏感的系统中。

单线程

发生缓存和数据库不一致的缘由在于多个线程的同时操做,若是相同的数据始终只会有一个线程去操做,不一致的状况就会避免了,好比nodejs,能够充分利用nodejs单线程的优点。提到单线程不能不提一下Actor模型,actor模型在对于一样的对象上能够看作是单线程模式,具体有兴趣的同窗能够查看以前的推文

分布式高并发下Actor模型如此优秀

单线程的模式基本上和分布式锁的方案相似,只不过单线程不须要锁就能够实现操做的顺序化,这也是单线程的优点所在。

其余方案

若是是以缓存为主呢?假如咱们的应用程序只和缓存组件通讯,至于持久化数据库由专门的程序负责,这样行不行呢?在理论上是能够的

image

不过这种方案须要考虑几个方面:

  • 数据从缓存持久化到数据采用什么样的解决方案,是同步进行仍是异步进行呢?
  • 在新数据请求的时候,若是缓存不存在,要采用什么样的方式来填充数据
  • 若是缓存模块挂掉了该怎么办?

以缓存为主的方案的优点是数据优先进入IO速度快的设备,对于那些请求量大,可是能够容忍必定数据丢失的应用很是合适,好比应用log数据的收集系统,这种系统其中一个最大的特色就是能够容忍必定数据的丢失,可是并发的请求数会很是大。因此咱们就能够利用缓存设备前置的方案来应对这种应用场景

更多精彩文章

image

相关文章
相关标签/搜索