关于前端:从实战出发聊聊缓存数据库一致性

39次阅读

共计 2759 个字符,预计需要花费 7 分钟才能阅读完成。

在云服务中,缓存是极其重要的一点。所谓缓存,其实是一个高速数据存储层。当缓存存在后,日后再次申请该数据就会间接拜访缓存,晋升数据拜访的速度。然而缓存存储的数据通常是短暂性的,这就须要常常对缓存进行更新。而咱们操作缓存和数据库,分为读操作和写操作。

读操作的具体流程为,申请数据,如缓存中存在数据则间接读取并返回,如不存在则从数据库中读取,胜利之后将数据放到缓存中。

写操作则又分为以下 4 种:

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

一些一致性要求不高的数据,如点赞数等,能够先更新缓存,而后再定时同步到数据库。而在其它状况下,咱们通常会等数据库操作胜利,再操作缓存。

上面次要介绍更新数据库胜利后,更新缓存和删除缓存这两个操作的区别和改良计划。

先更新数据库,再删除缓存

先更新数据库,再删除缓存,这种模式也叫 cache aside,是目前比拟风行的解决缓存数据库一致性的办法。
它的长处是:

  • 呈现数据不统一的概率极低,实现简略
  • 因为不更新缓存,而是删除缓存,在并发写写状况下,不会呈现数据不统一的状况

呈现数据不统一的状况呈现在并发读写的场景下,详情可见下图:

这种状况产生的概率比拟低,必须要在某⼀工夫区间同时存在两个或多个写⼊和多个读取,所以大部分业务都容忍了这种小概率的不统一。

尽管产生的概率较低,但还是有一些计划能够让影响降到更低。

优化计划

第一种计划为:采纳较短的过期工夫来缩小影响。这种办法有两个毛病:

  • 删除后,读申请会 miss
  • 如果缓存不统一,不统一的工夫取决于过期工夫设置

第二种计划则是采纳提早双删的策略,比方:1 分钟当前删除缓存。这种做法也存在两个毛病:

  • 删除缓存之前的工夫里可能会有不统一
  • 删除后,读申请会 miss

第三种计划为双更新策略,思路与提早双删策略差不多。不同的点是,此计划不删除缓存而是更新缓存,所以读申请就不会产生 miss。然而另一个毛病还是存在。

先更新数据库,再更新缓存

相比先更新数据库再删除缓存的操作,先更新数据库再更新缓存的操作能够防止用户申请间接打到数据库,进而导致缓存穿透的问题。

此计划是更新缓存,咱们须要关注并发读写和并发写写两个场景下导致的数据不统一。

先来看看并发读写的状况,步骤如下图所示:

能够看到因为 4 和 5 操作步骤都设置了缓存,如果步骤 4 产生在步骤 5 之前,那么会呈现旧值笼罩新值的状况,也就是缓存不统一的状况。这种状况只须要批改一下步骤 5,便可解决。

优化计划

能够通过在第五步不要 set cache,改用 add cache,redis 中应用 setnx 命令来进行优化。批改后步骤示意图如下:

解决完了并发读写场景导致的数据不统一,再来看看并发写写状况导致的数据不统一问题。

呈现不统一的状况如下图所示,Thread A 比 Thread B 先更新完 DB,然而 Thread B 却先更新完缓存,这就导致缓存会被 Thread A 的旧值所笼罩。

这种状况也是有办法能够优化的,上面介绍两个支流办法:

  • 应用分布式锁
  • 应用版本号

应用分布式锁

要解决并发读写的问题,第一个思路就是毁灭并发写。而应用分布式锁,让写操作排队执行,实践上就能够解决并发写的问题,但当初并没有牢靠的分布式锁实现计划。

不论是基于 Zookeeper,etcd 还是 redis 实现分布式锁,为了避免程序挂掉而锁不能开释,咱们都会给锁设置租约 / 过期工夫,设想一种场景:如果过程卡顿几分钟(尽管概率较低),导致锁生效,而其它线程获取到锁,此时就又呈现了并发读写的场景了,还是有可能会造成数据不统一。

应用版本号

并发写导致的数据不统一,是因为低版本笼罩了高版本。那么咱们能够想方法不让这种状况产生,一种可行的计划是引入版本号,如果写入的数据低于现版本号,则放弃笼罩。

毛病:

  • 应用层保护版本的代价很大,大规模落地很难
  • 需批改数据模型,增加版本
  • 每次须要批改,让版本自增

不论是更新缓存还是删除缓存,优化当前都将呈现数据不统一的概率降到最低了。然而有没有一种方法既简略,又不会呈现数据不统一的场景呢。上面就介绍一下 Rockscache。

Rockscache

简介

Rockscache 也是一种放弃缓存一致性的办法,它采纳的缓存管理策略是:更新数据库后,将缓存标记为删除。次要通过以下两个办法来实现:

  • Fetch 函数实现了后面的查问缓存
  • TagAsDeleted 函数实现了标记删除的逻辑

在运行时只有读数据时调用 Fetch,并且确保更新数据库之后调用 TagAsDeleted,就可能确保缓存最终统一。这一策略有 4 个特点:

  • 不须要引入版本,简直能够实用于所有缓存场景
  • 架构上与 ” 更新 DB 后删除缓存”一样,无额外负担
  • 性能高:变动只是将原来的 GET/SET/DELETE,替换为 Lua 脚本
  • 强统一计划的性能也很高,与一般的防缓存击穿计划一样

在 Rockscache 策略中,缓存中的数据是蕴含几个字段的 hash:

  • value:数据自身
  • lockUtil:数据锁定到期工夫,当某个过程查问缓存无数据,那么先锁定缓存一小段时间,而后查问 DB,而后更新缓存
  • owner:数据锁定者 uuid

证实

因为 Rockscache 计划并不更新缓存,所以只有确保并发读写数据一致性即可。上面来看看 Rockscache 是怎么解决数据不统一的问题,先回顾一遍 cache aside 模式导致的数据不统一的起因。

联合 cache aside 模式呈现数据不统一的场景,来讲讲 Rockscache 是怎么解决的。

咱们要解决的外围问题是,避免旧值写入到缓存中。Rockscache 的解决方案是这样的:

  • 查问申请,如果缓存中读不到数据,还要做一个操作:锁定缓存,为 key 设置一个 uuid(代码示例:https://github.com/dtm-labs/r…)
  • 写申请在删除缓存的时候,须要把锁删了(代码示例:https://github.com/dtm-labs/r…)
  • 读申请在设置缓存的时候,通过 uuid 比对,发现上锁的不是本人,阐明有写申请把数据更新了,则放弃批改缓存(代码示例:https://github.com/dtm-labs/r…)

至此咱们曾经实现了 rockscache 策略下的缓存更新。不过和其余缓存更新策略一样,咱们都默认操作数据库胜利后,操作缓存必定胜利。然而这是不对的,在实际操作过程即使操作数据库胜利,也可能呈现缓存操作失败的状况,因而能够通过以下 3 种形式来保障缓存更新胜利:

  • 本地音讯表
  • 监听 binlog
  • dtm 的二阶段音讯

除了缓存更新,Rockscache 还有以下两种性能:

  • 避免缓存击穿
  • 避免避免穿透和缓存雪崩

这都是十分实用的性能,举荐大家理论应用操作试试看。

参考资料

  • https://dtm.pub/app/cache.html
  • https://smartkeyerror.oss-cn-…
正文完
 0