13 | 缓存的使用姿势(一):如何选择缓存的读写策略?

这次我们先讲讲缓存的读写策略。你可能觉得缓存的读写很简单,只需要有限读缓存,缓存不命中就从数据库查询,查询到了就回种缓存。实际上针对不同的业务场景,缓存的读写策略也是不同的。

我们以标准的“缓存+数据库”的场景为例,剖析经典的缓存读写策略以及它们适用的场景。这样一来,就可以在日常的工作中根据不同的场景选择不同的读写策略。

Cache Aside(旁路缓存)策略

我们来考虑一种最简单的业务场景,比如说在你的电商系统中有一个用户表,表中只有ID和年龄两个字段,缓存中我们以ID为key存储用户的年龄信息。那么当我们要把ID为1的用户的年龄从19变为20改如何做?

你可能会产生这样的思路:先更新数据中ID为1的记录,再更新缓存中Key为1的数据

这个思路会造成缓存和数据库中的数据不一致。请看下图 :

image.png

为什么会产生这个问题呢?因为变更数据库和变更缓存是两个独立的操作,而我们并没有对操作做任何的并发控制。那么当两个线程并发更新它们的时候,就会因为写入顺序的不同造成数据不一致。

另外,直接更新缓存还会存在另一个问题就是丢失更新。以我们的电商系统为例,假如电商系统的账户中有三个字段:ID、户名和金额,这个时候缓存中存储的就不只是金额信息,而是完整的账户信息了。当更新缓存中账户的金额时,你需要从缓存中查询完整的账户数据,把金额变更后再写入到缓存中。

这个过程也会有并发问题,比如说原有金额时20,A请求从缓存督导数据,并且把金额+1变更为21,在未写入请求之前又有请求B也读到缓存数据后把金额+1,也变更为21,两个请求同时把金额写回缓存,这时缓存里面的金额是21,但是语气金额数+2,这是个比较大的问题。

要如何解决这个问题呢?其实我们可以在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据后,再从数据库读取数据,更新到缓存中。

image.png

这个策略就是我们使用缓存最常见的策略,Cache Aside策略(也叫旁路缓存策略),这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略,其中读策略的步骤是:

  • 从缓存中读取数据;
  • 如果缓存命中,则直接返回数据;
  • 如果缓存不命中,则从数据库中查询数据;
  • 查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略的步骤是:

  • 更新数据库中的记录;
  • 删除缓存记录。

在写策略中,能否先删除缓存,后更新数据呢?答案是不行的,因为这样也有可能会出现缓存数据不一致的问题,我们以用户表的场景为例解释一下。

image.png

那么像Cache Aside策略这样先更新数据库,后删除缓存就没问题了么?其实在理论上还是有缺陷的。

image.png

不过这种问题出现的概率并不高,因为缓存的写入通常远远快于数据库的写入,所以在实际中很难出现请求B已经更新了数据库并且清空了缓存,请求A才更新完缓存的情况。而一旦请求A早于B清空缓存之前更新了缓存,那么接下来的请求就会因为缓存为空,而从数据库中重新加载数据,所以不会出现这不一致的情况。

****Cache Aside策略是我们日常开发中经常使用的缓存策略略,不过我们在使用时也要学会依情况而变。**比如说当新注册一个用户,按照这个更新策略,你要写数据库,然后清理缓存(当然缓存中没有数据给你清理)。可当我注册用户后立即读取用户信息,并且数据库主从分离时,会出现因为主从延迟所以读不到用户信息的情况。

而解决这个问题的方法是在插入新数据到DB之后写入缓存,这样后续的请求就会从缓存中读到数据了。并且因为是新注册的用户,所以不会出现并发更新用户信息的情况。

Cache Aside存在的最大的问题是当写入比较频繁时,缓存你中的数据会被频繁的清理,这样会对缓存的命中率有一些影响。如果你的业务对缓存命中率有严格要求,那么可以考虑两种解决方案:
1.在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
2.另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快的过期,对业务的影响也是可以承受的。

当然,除了这个策略,在计算机领域还有其他几种经典的缓存策略,它们也有各自适用的使用场景。

Read/Write Through(读穿/写穿)策略

这个策略的核心原则是用户只与缓存打交道,由缓存和DB通信,写入或者读取数据。就比如你不能越级汇报。

Write Through的策略是这样的:先查询要写入的数据在缓存中是否已经存在,如果存在,则更新缓存中的数据,并且由缓存组件同步更新到DB中。如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。

一般来说,我们可以选择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到DB中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到DB中。
在Write Through策略中,我们一般选择“不按写分配”方式,原因是无论采用哪种“Write Miss”方式,我们都需要同步将数据更新到数据库中,而“不按写分配”方式相比“按写分配”还减少了一次缓存的写入,能够提升写入的性能。

Read Though策略就简单一些,它的步骤是这样的:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
下面是Read Though/ Write Though策略的示意图:


image.png

Read Though/ Write Though策略的特点是由缓存节点而非用户来和数据库打交道,在我们开发过程中相比Cache Aside策略要少见一些,原因是我们经常使用分布式缓存组件,无论是Memcached还是redis都不提供写入DB,或者自动加载DB中数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略,比如在上一节中提到的本地缓存Guava Cached中的Loading就有Read Though策略的影子。

我们看到Write Though策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相对于写缓存,同步写数据库的延迟就要高很多了name我们可否异步地更新数据库?

Write Back(写回)策略

这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块只有被再次使用时才会将其中的数据写入到后端存储中。
需要注意的是,在“write Miss”的情况下,我们采用的是“按写分配”的方式,也就是在写入后端存储的同时要写入缓存,这样我们在之后的写请求中都只需要更新缓存即可,而无需更新后端存储了。

image.png

如果使用Write Back策略的话,读的策略也有一些变化了。我们在读取缓存时如果发现缓存命中则直接返回缓存数据。如果不命中则寻找一个可用的缓存块儿,如果这个缓存块是“脏”的,就把缓存块儿中之前的数据写入到后端存储中,并且从后端存储加载数据到缓存块,如果不是脏的,则由缓存组件将后端存储中的数据加载到缓存中,最后我们将缓存设置为不是脏的,返回数据就好了。

image.png

其实这种策略不能被应用到我们常用的DB和缓存场景中,它是计算机体系结构中的设计,比如我们在向磁盘中写数据时采用的就是这种策略。
无论是操作系统层面的Page Cache,还是日志的异步刷盘,亦或是消息队列中消息的异步写入磁盘,大多采用了这种策略。因为这个策略在性能上的优势毋庸置疑,它避免了直接写磁盘造成层的随机写问题,毕竟写内存和写磁盘的随机I/O的延迟相差了几个数量级。

但因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏块数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为Page Cache还没有来得及刷盘造成的。

当然你依然可以在一些场景下使用这个策略,在使用时,建议是:在向低速设备写入数据的时候,可以再内存里先暂存一段时间的数据,甚至做一些统计汇总,然后定时的刷新到低速设备上。比如说你在统计接口响应时间的时候,需要将每次请求的响应时间打印到日志中,然后监控系统手机日之后再做统计。但是如果每次请求都打印日志无疑会增加磁盘的I/O,那么不如把一段时间的响应时间暂存起来,经过简单的统计平均耗时,每个耗时区间的请求数量等,然后定时的批量打印到日志中。

内容总结

1.Cache Aside是我们在使用分布式缓存时最常用的策略,可以在实际工作中直接拿来使用。
2.Read/Write Through和Write Back策略需要缓存组件的支持,所以比较适合在实现本地缓存组件的时候使用;
3.Write Back策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。

而且,你还需要了解,我们今天提到的策略都是标准的使用姿势,在实际开发过程中需要结合实际的业务特点灵活使用甚至加以改造。这些业务特点包括但不仅限于:整体的数量级情况,访问的读写比例的情况,对于数据的不一致时间的容忍度,对于缓存命中率的要求等等。理论结合实践,具体肩况具体分析,你才能得到更好的解决方案

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 一、引言 本文谈及的是后台业务服务缓存问题,在构建和优化业务服务时,第一想到的应该是优化数据库,比如数据库模型设计...
    东_山_郎阅读 1,138评论 0 8
  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,835评论 0 11
  • PS:转载自《架构师之路》,觉得受益匪浅,故收录之 缓存误用 缓存,是互联网分层架构中,非常重要的一个部分,通常用...
    Huang远阅读 11,177评论 4 27
  • 缓存误用 缓存,是互联网分层架构中,非常重要的一个部分,通常用它来降低数据库压力,提升系统整体性能,缩短访问时间。...
    叫我峰兄阅读 1,208评论 0 0
  • 今天是加入运营学院的第五天,走完一天日程,对几个点可算是有收获,不算辜负今日时光。 1.认定目标。 今晚听了娜儿大...
    牛魔王爱写作阅读 206评论 0 1