不懂缓存一致性,易把代码写成Bug

哈喽哈喽大家猴,我是把代码写成bug的大头菜。公众号:大头菜技术(bigheadit)。原创不易,但欢迎转载。

本文主要分享一下关于缓存一致性问题和其解决方案。下面是本文的主要目录,大家可以挑着看。

目录
  1. 什么是缓存一致性
  2. 为什么要保证缓存一致性
  3. 如何保证缓存一致性
  4. 如何做到强一致性
  5. 总结

01 什么是缓存一致性

就是缓存和数据库的数据不一致导致的问题,缓存一致性分为强一致性和最终一致性。

  • 强一致性,这个比较损耗性能,比较复杂,加入之后,可能会比没加缓存更慢。

  • 最终一致性,是允许缓存数据和数据库数据一段时间内不一致,但数据最终会保持一致。这个性能较高。

02 为什么要保证缓存一致性

因为业务中存在一些写操作导致的,是要先写缓存,还是先写数据库。二者顺序的不同会导致不同的问题。

单纯的读操作,是不会导致缓存一致性问题的,因为读是幂等的哈。读无数次都是不会变的,因此就不存在读操作引起缓存一致性问题。

因此导致缓存不一致的就是写操作了。写操作是导致缓存不一致的原因。但这不是要保证缓存一致性的理由,
归根结底都是业务需要,如果业务需求允许缓存和数据库的不一致,那就不需要保证缓存一致性了。

03 如何保证缓存一致性(解决方案)

相信很多人都知道经典方案:cache aside pattern。

首先明确的是,读不会产生缓存一致性问题。是写操作,才会产生缓存一致性问题。

第一点,失效:请求过来时,先访问缓存,缓存不存在,再去访问数据库,更新缓存。

第二点,读:请求过来时,缓存中有数据,直接返回数据。

第三点,写:先更新数据库,后删除缓存。

关键在第三点,前提,数据库肯定是更新的。剩下的问题就是:是要更新缓存?还是要删除缓存?是先对数据库操作?还是先对缓存操作?

俩俩组合有4种可能性:

  • 先更新缓存,后更新数据库

  • 先更新数据库,后更新缓存

  • 先删除缓存,后更新数据库

  • 先更新数据库,后删除缓存

1.先更新缓存,后更新数据库

首先我们要明白,更新数据库或者更新缓存,都面临着更新失败的风险。但在互联网高并发的环境中和根据墨菲定律,这个事是一定会发生的。

  1. 先更新缓存,成功了

  2. 后更新数据库,失败了,当然你会说重试,好,那我就重试N次,但如果数据库彻底挂了,恢复不了了,重试也没用

导致问题:数据丢失,数据库里面的数据还是老数据

2.先更新数据库,后更新缓存

假设有两个请求,A请求是更新,B请求是更新,A先B后,但二者间隔很短

  1. 线程A更新了数据库

  2. 线程B更新了数据库

  3. 线程B更新了缓存

  4. 线程A更新了缓存

导致问题:缓存中是旧数据,数据库中是新数据,这就不一致了。还有就是更新后的缓存,真的会被再读取吗?如果缓存数据不再被读取,那就白白操作了一次缓存更新操作。并且还占用内存空间。

根据这个例子,可以看出,更新缓存是不可取的,那就直接删除缓存吧。接着看

3.先删除缓存,后更新数据库

假设有两个请求,A请求是更新,B请求是读,可能出现的问题

  1. 线程A删除缓存

  2. 线程B查询不到缓存,直接去数据库查旧值

  3. 线程A将新值写入数据库

  4. 线程B更新缓存

导致问题:缓存中的是旧值,数据库中的是新值,二者不一致。进一步,如果数据库存在读写分离,那么缓存和数据库数据不一致的情况进一步加剧。

  1. 线程A删除缓存

  2. 线程A将新值写入主数据库,但未同步数据到从数据库

  3. 线程B查询不到缓存,直接去从数据库查,查到旧值

  4. 线程B更新缓存

  5. 新数据同步到从数据库

导致问题:缓存是旧值,数据库是新值,二者数据不一致

4.先更新数据库,后删除缓存

假设有两个请求,A请求是读,B请求是更新,可能会出现的问题

  1. 缓存刚好失效

  2. 线程A查数据库,得到旧值

  3. 线程B更新数据库

  4. 线程B删除缓存

  5. 线程A更新缓存

导致问题:缓存是旧值,数据库是新值,二者不一致。但这种情况的可能性相对来说比较小,因为需要缓存刚好失效,并且此时有一个线程去读,且刚好又有一个写的线程。而且写的线程理论上是比读的线程慢的,因为写的线程,需要加锁。而查询不用加锁,不包括复杂的查询。

在数据库读写分离的情况下,这种情况会更加明显:

  1. 线程B更新主库

  2. 线程B删除缓存

  3. 线程A查询缓存,没有命中,查询从库得到旧值

  4. 数据同步到从库

  5. 线程A更新缓存

导致问题:缓存数据和数据库数据不一致

如果考虑更新数据库或者更新缓存失败的话,那么更新数据库失败的话,其实数据库和缓存都是旧数据,因此不存在数据不一致的情况。

如果更新缓存失败,那么有过期时间来保证最终一致性。如果非要较真的话,可以加入重试机制。

重试机制可以用线程池,也可以用MQ。MQ更加可靠。可以直接订阅MySQL的binlog,来触发缓存的删除。当然,其实MQ也会挂。但是MQ和缓存都一起挂的几率,应该很小吧。

综上所述四种情况

虽然每种方案都有各自的问题,但出现几率较小的是先更新数据库,后删除缓存方案。为什么先更新数据库?因为数据库的持久化能力比缓存好。上述四种情况,还可能出现缓存并发,缓存穿透,缓存雪崩的问题。这些问题,这里就不讨论了。感兴趣的话,自己去看我的相关文章。

04 如何做到强一致性

方案一:分布式事务

可以用分布式事务,分布式事务,具体的实现有2PC、3PC、消息队列等。如果要采用这个方案,架构设计中要引入很多容错、回退、兜底的措施。业务代码就增加复杂性了。还有人说用分布式一致性算法paxos和raft,这就更复杂了。

方案二:分布式读写锁

首先,我们回到先更新数据库,后删除缓存 ,要明白什么时候会出现脏数据?

出现脏数据:更新数据库后,删除缓存之前。这时候二者数据是不一致的。

如果实现更新数据库时,所有读请求都被阻塞。这就解决了数据不一致的问题,这其实是串行化思路。但后果,当然是性能下滑。

总结

其实选择数据的强一致性和数据的最终一致性。得看具体需求,我好像说了一句废话。但是放弃强一致性,意味着我们系统的性能得到一定程度的提升。相反,如果我们追求强一致性,那就会巨复杂,而且可能得不偿失,可能性能比不加缓存时还低。缓存这东西,想要用得好,就需要好好琢磨,比如过期时间的设置,持久化,故障恢复,空间和时间的平衡,一致性的选择,这些都要好好斟酌,没有最好的方案,只有合适的方案。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务