缓存和数据库一致性问题
目标
使用缓存的目标是:提升系统吞吐量(A Availability高可用),如果方案为了达到了强一致性(C Consistency一致性),但是强烈影响了吞吐量,那就相当于没有用缓存。(ps:数据存在redis和db已经满足分区容错性P Partition tolerance, CAP只能同时满足其中两者)。
几种方案
操作缓存、更新数据库
操作缓存
- 先操作缓存,再更新数据库
- 先更新数据库, 再操作缓存
操作数据库有两种方式:
- 更新缓存
- 删除缓存
什么是更新缓存: 数据不仅要写入数据库还要写入缓存。 核心关键词:简单。
什么是删除缓存: 数据只会写入数据库,删除缓存,写入缓存的操作等待数据查询的时候来做。核心关键词:复杂
如果更新数据库的值,不需要经过复杂计算,并且写少读多那么可以直接写入缓存,如果写入缓存的数据需要经过复杂计算,
那么如果每次写入数据库,都要更新缓存,在写多读少的场景下会增大缓存层的压力。
四种方案
先来看看更新缓存的两种方案。
1.先更新缓存,再更新数据库
现在有两个线程A,B:
A:开始更新缓存,更新完成
B:读数据,缓存中有数据,读到了数据
A:更新数据库的值,更新失败
这里B从缓存读到数据是无效的,所以这方案弃用,缓存层的数据应该依赖数据库。这种方案不考虑。
2.先更新数据库,再更新缓存
现在有两个线程A,B:
A:更新数据库
B:更新数据库
B:写入缓存
A:写入缓存
这里缓存最终的数据应该是B线程写入的值,但是这里由于A线程所在的实例由于网络抖动,导致A更新缓存晚于B线程,这就导致了
脏数据。上面也说了如果是读少写多的场景,会非常耗费性能。
所以更新缓存这种方案一般不予考虑。下面我们来看看删除缓存的两种方案。
3.先删除缓存,再更新数据库
现在有两个线程A,B:
- A:删除缓存,成功
- B:读缓存的数据
- A:更新数据库
- B:读缓存
这里可以看到,后续操作读到的缓存业务上是已经过期的。如果不设置key的过期策略,则数据永远都是脏数据。这里我们可以引入延时双删策略来保证4之后的步骤能
读到正确的值。
延时双删策略:第3步完成后,可以延时睡眠后直接删除key,之后再次读,可以从数据库拿到最新的值,也可以直接设置key的过期时间。
4.先更新数据库,再删除缓存
现在有两个线程A,B:
- A:更新数据库
- B:读缓存
- A:删除缓存
- B:读缓存,从数据库加载
这里可以看到只有第2步读到数据是脏数据,并且这个时间不会太长。所以这种方案来说缓存数据及时性比较高。
可能有同学有疑问,如果第3步失败了呢?这里可以做一种补偿方案,当删除缓存失败了,则发条消息或者维护一个内存队列(list),来做重试。
方案对比
缓存淘汰也不是万全之策,这种方案有一个缺点,就是如果在删除缓存成功的时候,如果有大量请求请求这个key,则会发生缓存击穿(不存在于缓存,存在于db),进而可能会导致缓存雪崩。
更新缓存可能会导致,脏数据的出现以及在读少写多的场景下降低性能。
怎么来选择方案呢?
问题本质是在分布式环境下(对db和redis的操作不是原子的)要求数据一致性。
- 强一致性
- 最终一致性
方案1,2,如果要达到最终一致性,key需要设置过期时间,就可以保证数据最终一致性。
方案3,4,本身就满足最终一致性。所以我们只讨论方案3,4。如果选择方案3,4呢?3,4的区别在于先删除缓存,还是先更数据库。
方案3: 先删除缓存,在更新数据库,如果更新数据库失败。再次读从数据库加载数据就好了。
方案4: 先更新数据库,在删除缓存,如果删除缓存失败了。缓存数据业务上已经过期,数据不一致。最终只能通过key的过期来保证数据一致性。
所以选择方案的原则是: 谁先执行对业务影响最小,就先执行谁。
上面说了方案3,4会导致缓存雪崩,那么如果在方案3,4和2中如何选择呢:热点数据采用更新缓存, 冷数据使用淘汰(删除)缓存
上面也只是说了如何保证最终一致性,那么如何保证强一致性呢?
在写db和缓存中加锁,不过这就会导致redis的性能依赖db,会导致DB+缓存这种方案性能直线下降,牺牲了高可用。
参考
https://www.zhihu.com/question/54105974
http://www.blogjava.net/hello-yun/archive/2012/04/27/376744.html
https://www.cnblogs.com/duanxz/p/3740595.html