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

CodeTrailblazerX
• 阅读 1299

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

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

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

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

01 什么是缓存一致性

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

  • 强一致性,这个比较损耗性能,比较复杂,加入之后,可能会比没加缓存更慢。
  • 最终一致性,是允许缓存数据和数据库数据一段时间内不一致,但数据最终会保持一致。这个性能较高。

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

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

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

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

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

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

首先明确的是,读不会产生缓存一致性问题。是写操作,才会产生缓存一致性问题。
不懂缓存一致性,易把代码写成Bug

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

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

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

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

俩俩组合有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,这就更复杂了。

方案二:分布式读写锁

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

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

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

总结

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

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这