共计 2368 个字符,预计需要花费 6 分钟才能阅读完成。
灵魂拷问
- 保障缓存和数据库的一致性很简略吗?
- 有哪些形式能保障缓存和数据库的一致性呢?
- 如果产生了缓存和数据库数据不统一的状况怎么办呢?
在上篇文章咱们介绍了缓存的定义分类以及优缺点等,如果还没看的同学能够移步这里
据说你会缓存?
当咱们的零碎引入缓存组件之后,性能失去了大幅度晋升,然而随之而来的是代码须要引入肯定的复杂度,比方缓存的更新策略,写入策略,过期策略等,而其中最可能导致程序员加班的莫过于缓存和数据库的一致性问题了,既:缓存中的数据和数据库中的数据不统一。
一致性问题
说到一致性问题,这算是分布式系统中不可避免的一个痛点,或者说分布式系统人造就自带了数据一致性问题,尽管能够利用很多分布式事务解决方案来做到一致性,然而理论的零碎架构设计中,我还是推崇防止分布式事务。缓存和数据库数据的一致性在产生原理上和分布式相似,其实能够把他们两个的关系看做是分布式系统中的两个操作节点。
但凡处于不同物理地位的两个操作,如果操作的是雷同数据,都会遇到一致性问题
产生数据一致性问题的根本原因是对一个数据的多个操作过程,缓存和数据库数据的一致性也是这个原理,零碎中最常见的操作流程是这样的:
- 数据的申请首先查问缓存中是否存在该数据
- 如果数据命中缓存(在缓存中存在)则间接返回数据,如果数据没有命中缓存(缓存中不存在),则去数据库中取数据
- 从数据库中取回数据,而后把数据写入缓存
从图中能够分明的看到,对数据库的操作和对缓存的操作是两个不同阶段的操作,在任何一个操作过程中都会产生线程平安问题。比如说:
- 当两个线程同时查问缓存的时候,可能会产生两个线程都没有命中缓存的问题
- 如果两个线程都没有命中缓存就会产生同时查询数据库的问题
- 接着就会产生两个线程同时回写缓存的问题
而这还不是最致命的,毕竟两个线程同时查询数据库,同时回写缓存数据在少数状况下缓存数据和数据库数据还能保持一致。最要命的是如果是两个线程都进行更新操作,最常见的更新过程是先更新数据库,而后更新缓存。上面就以最常见的用户积分场景为例,每个用户都有本人的积分,如果产生以下过程:
- 线程 A 依据业务会把用户 id 为 1 的积分更新成 100
- 线程 B 依据业务会把用户 id 为 1 的积分更新成 200
- 在数据库层面,线程 A 和线程 B 必定不存在并发状况,因为数据库用锁来保障了 ACID(如果是 mysql 等关系型数据库),无论数据库中最终的值是 100 还是 200,咱们都假如正确。
- 假如线程 B 在 A 之后更新数据库,则数据库中的值为 200
- 线程 A 和线程 B 在回写缓存过程中,很可能会产生线程 A 在线程 B 之后操作缓存的状况(因为网络调用存在不确定性),这个时候缓存内的值会被更新成 100,产生了缓存和数据库不统一的状况
通过以上案例可见,解决缓存和数据库数据不统一的基本解决方案是须要把两个操作合并成逻辑上能保障事务的一个操作
分布式锁
在平时开发中,利用分布式锁可能算是比拟常见的解决方案了。利用分布式锁把缓存操作和数据库操作封装为逻辑上的一个操作能够保证数据的一致性,具体流程为:
- 每个想要操作缓存和数据库的线程都必须先申请分布式锁
- 如果胜利取得锁,则进行数据库和缓存操作,操作结束开释锁
- 如果没有取得锁,依据不同业务能够抉择阻塞期待或者轮训,或者间接返回的策略
利用分布式锁是解决分布式事务的一种计划,然而在肯定水平上会升高零碎的性能,而且分布式锁的设计要思考到 down 机和死锁的意外状况,而最常见的分布式锁就是利用 redis,然而也会有不少坑,具体能够参考之前的文章
redis 做分布式锁可能不那么简略
删除缓存
绝对于分布式锁的计划,而程序员理论中最喜爱应用的还是删除缓存的形式,在一个可能会产生不统一的场景下, 咱们会以数据库为主 ,在操作完数据库之后,不去更新缓存,而是删除缓存。这在肯定意义上相当于只操作数据库,把须要保护的两个数据源变成了一个数据源。
这种形式要求必须先操作数据库 ,后操作缓存,不然的话产生不统一的几率会大很多。为什么这么说呢? 因为就算是先操作数据库也会有产生不统一的几率,然而毕竟在整个操作过程中,删除缓存的操作只占整个流程工夫的一小部分而已,而且咱们能够利用缓存的过期工夫来保证数据的最终一致性,所以在一些能够容忍数据短暂不统一的场景下能够采纳这种计划的。
删除缓存计划带来的另外一个劣势是:如果同样的数据会被频繁更新,缓存会被频繁删除,当有读申请的时候又会被频繁的从数据库加载,所以这种计划实用于那种对缓存命中率不敏感的零碎中。
单线程
产生缓存和数据库不统一的起因在于多个线程的同时操作,如果雷同的数据始终只会有一个线程去操作,不统一的状况就会防止了,比方 nodejs,能够充分利用 nodejs 单线程的劣势。提到单线程不能不提一下 Actor 模型,actor 模型在对于同样的对象上能够看做是单线程模式,具体有趣味的同学能够查看之前的推文
分布式高并发下 Actor 模型如此优良
单线程的模式基本上和分布式锁的计划相似,只不过单线程不须要锁就能够实现操作的程序化,这也是单线程的劣势所在。
其余计划
如果是以缓存为主呢?如果咱们的应用程序只和缓存组件通信,至于长久化数据库由专门的程序负责,这样行不行呢?在实践上是能够的
不过这种计划须要思考几个方面:
- 数据从缓存长久化到数据采纳什么样的解决方案,是同步进行还是异步进行呢?
- 在新数据申请的时候,如果缓存不存在,要采纳什么样的形式来填充数据
- 如果缓存模块挂掉了该怎么办?
以缓存为主的计划的劣势是数据优先进入 IO 速度快的设施,对于那些申请量大,然而能够容忍肯定数据失落的利用十分适合,比方利用 log 数据的收集零碎,这种零碎其中一个最大的特点就是能够容忍肯定数据的失落,然而并发的申请数会十分大。所以咱们就能够利用缓存设施前置的计划来应答这种利用场景
更多精彩文章
- 分布式大并发系列
- 架构设计系列
- 趣学算法和数据结构系列
- 设计模式系列