【一道面试题】 讲一下缓存更新策略(如何维护Redis与数据库之间的一致性)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

讲一下缓存更新策略/如何维护Redis与数据库之间的一致性

这个问题的话很容易作为一系列缓存问题的起点注意如果面试官问的是如何维护Redis和数据库的一致性的话以下的第一点就不用回答了

第一点:从Redis角度来看缓存更新包括以下三种:

  • 内存淘汰:这种方式由redis自动进行当redis内存达到设定的max-memery时会自动触发淘汰机制淘汰掉一些不重要的数据(可以自己设置策略方式)这种方法的优点是维护成本低因为完全由redis自动进行我们无需编写任何代码进行维护缺点是很难保证缓存与数据库之间的数据的一致性我们只能被动等待缓存被淘汰至于缓存什么时候被淘汰哪些缓存被淘汰都不在我们控制的范围内甚至当内存一直很充足时缓存会一直得不到更新数据甚至连最终一致性都做不到
  • 超时剔除:当我们给redis设置了过期时间ttl之后redis会将超时的数据进行删除方便腾出内存空间来继续使用缓存。这种方式的优点在于可以保证缓存删除的时间和范围是在我们可以控制的范围内的缺点是在redis未过期的时间内还是无法保证数据的一致性。
  • 主动更新:用户主动修改或删除缓存中的数据。这种方式能保证数据库和缓存之间数据的一致性但是用户需要手动编写代码去完成加大了维护成本

针对上述三种方案个人建议是对于很少发生变动的数据我们可以使用内存淘汰的策略对于经常变动的数据我们可以使用主动更新的策略最后无论是哪种数据我们最好都为其设计一个超时时间作为兜底策略

第二点:从应用层面来看缓存更新包括以下三种

  • Cache Aside旁路缓存

    旁路缓存又称为双写方案是开发中用的最多的一种这种方式流程如下:

    • 应用在查询数据的时候先从缓存中读取数据如果缓存中没有则再从数据库中读取数据得到数据库的数据之后将这个数据放到缓存中。
    • 如果应用要更新某个数据先去更新数据库中的数据更新完成之后再去删除缓存中的策略

    这种方案适合读多写多的场景写多的话就不适合了因为会频繁删除缓存缓存的命中率比较低

    旁路缓存中存两个问题需要注意面试官不问就不说:

    • 为什么是删缓存而不是更新缓存?

      • 第一点更新缓存很容易在并发时产生脏数据

        • 如果先更新数据库再更新缓存那么假设两个线程同时更新同一个数据线程A先完成了第一步把数据库中的值更新成a然后卡住了然后线程B一口气把第一步第二步都完成了把数据库中的值更新成了b缓存中的值也更新成了b那么再轮到线程a执行第二步时就会把缓存中的值更新为a那么就寄了数据库中的值是b你缓存中的值却是a。

          还有一个问题通常情况下我们都会通过事务来保证redis和数据库操作的原子性但是数据库操作毕竟才是主操作所以如果更新缓存的操作出现了问题我们肯定是不会对数据库进行回滚的这种情况下我们一般会对更新缓存的操作进行重试但是如果重试时间过长缓存迟迟得不到更新也会造成数据不一致的问题4

          那么可能有人就要问了你删缓存时就不会出现问题吗?是的也会出现问题但是删除操作比更新操作出现问题的概率明显低很多你愿意选择一个出现问题概率更高的方案还是出现问题概率更低的方案呢?

        • 如果更新缓存再更新数据库也一样存在问题假设第二步更新数据库失败了要求回滚缓存的更新这时该怎么办呢?redis不支持事务的回滚我们可以采用手工回滚的方式先保存原有数据然后再将缓存更新回原来的数据但是这种方案的话存在一些缺陷假设一个场景:

          • 假设现在缓存中的值是z有两个线程同时更新数据库
          • 线程A将缓存中的值更新成a且保存了原来的值z然后更新数据库
          • 线程B将缓存中的值更新成b且保存了原来的值b然后更新数据库
          • 线程A更新数据库时失败了它必须回滚那现在缓存中的值更新成什么呢理论上应该更新成b因为现在数据库中的值是b但是线程A并没有保存b这个值它只保存了z这样就出现了问题

          如果在线程A更新缓存与数据库的整个过程中先把缓存及数据库都锁上确保别的线程不能更新是否可行?当然是可行的。但是其他线程能不能读取?假设线程A更新数据库失败回滚缓存时线程C也加入进来它需要先读取缓存中的值但是这时数据库操作是失败的你缓存中的数据就是脏数据线程C能否去读呢?

      • 第二点更新缓存的代价有时候是很高的如果你每次修改的时候都将对应的缓存修改一份那么你必须思考一个问题这些缓存真的会被马上用到吗?如果你更新了十次缓存但是期间却没有一个人来访问那么你前九次的缓存更新有意义吗?之所以选择删除缓存而不是更新缓存其实就是一个延迟加载的思想你用的时候我再给你

    • 为什么是先更新数据库再删缓存?不能先删缓存再更新数据库吗?

      首先无论是哪种方案都是存在问题的就是比较哪种方案问题更不容易出现而已

      • 第一种方案:先删缓存再更新数据库需要注意的是你更新数据库是一个比较耗时的动作假如线程A删了缓存然后去改数据库希望把数据库中的b值改成a还没改完但是此时线程B来读数据了没读到然后去读数据库由于此时线程A还没改完所以数据库中的值还是b线程B拿到了数据之后就准备去写到缓存中了假设到这个时候你线程A才更新完数据库那这时候数据库中的值是a缓存中的值是b数据库就和缓存不一致了。

        而且这种情况在高并发场景下出现概率还是蛮高的我线程B只要读缓存和读数据库的操作比你线程A更新数据库的操作快就可以了而读操作本来就是比较快的操作

      • 第二种方案:先更新数据库再删除缓存假设此时数据库中有一个a值但是没有没加载到缓存中可能是第一次访问、缓存被删除或者缓存失效了线程A来读没读到然后去读数据库拿到了a值正准备把a写入缓存中时线程B来了它把数据库中的值改成了b然后删了一下缓存当然此时删了等于没删等线程B搞完后线程A才终于把缓存写完那就出现问题了数据库中的值是b你缓存中的值是a这怎么行?但是这种情况是比较极端的首先你缓存要失效然后你线程B的更新数据库动作和删除缓存动作要比线程A写缓存的动作快除非线程A是真的因为某种原因阻塞了不然这种情况几乎可以不考虑。

        还有一个问题就是说如果有用户在更新数据库之后删除缓存之前访问数据的话也会读到旧数据这种情况相对来说比上面那种情况更容易出现一些。

      • 还存在一种延时双删的策略概括来说就是先删缓存再更新数据库隔一小会之后再删缓存这相当于是对第一种方案做了一个优化之所以要隔一小会是在等待线程A完成数据库操作。个人认为延时双删更侧重于出现数据不一致时如何解决这种策略能在第二次删缓存后基本确保数据是一致的当然肯定还会有一些特别极端场景下才会出现的问题但是在第二次删缓存之前出现数据不一致的概率等同于第一种方案

      无论是哪种方案都或多或少的存在一些问题个人还是认为第二种方案比较好一点双删其实没有必要两次删缓存效率上来说肯定不如一次删缓存的使用第二种方案时如果但是出现比较极端的场景还可以为数据加上一个超时时间来兜底

  • Read/Write Through读穿 / 写穿

    这种策略的原则是应用程序只和缓存交互不再和数据库交互由缓存和数据库交互相当于更新数据库的操作由缓存自己代理了也就是说在应用程序看来缓存和数据库是一体的。

    这种方案的问题在于我们常见的分布式组件无论是Memcached还是Redis都不提供写入数据库和自动加载数据库中的数据的功能如果要使用这种方案我们需要自己来实现例如将缓存和数据库整合为一个服务由该服务去维护数据库与缓存的一致性调用者只需要调用该服务即可但是这样的话开发成本明显就变高了但是对于编写业务接口的开发者来说确实是变简单了总之根据业务来做取舍即可

  • Write Back写回

    这种策略是指在更新数据的时候只更新缓存同时将缓存数据设置为脏数据然后立马返回并不会更新数据库。对于数据库的更新会通过批量异步更新的方式进行。

    这种方案其实是算机体系结构中的设计比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back写回策略。但是这种策略也很难应用到我们常用的数据库和缓存的场景中因为 Redis 并没有异步更新数据库的功能。

    Write Back 策略特别适合写多的场景因为发生写操作的时候只需要更新缓存就立马返回了但是这种方案也带来了两个问题:

    • 数据是异步更新到数据库的因此是最终一致而不是强一致
    • 数据会有丢失的风险如果Redis宕机了那么未更新到数据库的数据就会丢失
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: redis数据库