缓存数据更新与饱和替换的设计模式

1- 缓存与数据库

应用程序通常使用缓存来提高系统系能,特别是对于只读事务来说。当数据发生变化时,这些应用程序会直接更新数据库。问题在于随着负载的增加,响应时间将会增加,响应时间将随着更新的增长而延长。关系型数据库其实并不擅长大量处理少量记录的并发事务(多而分散),处理批量事务才是数据库的强项(少而集中)

1.1- 缓存更新的必要性

由于硬件内存空间的限制,以及满足JVM的空余内存空间的限制,内存中缓存的数据的上限是一定的,所以需要不停的释放旧数据,才能持续不断的加载数据到内存

1.2- 缓存的实现方式

JVM 堆内存的实现 基于SoftReference或者WeakReference

使用堆内存实现的方式,其缓存大小受制于JVM堆空间的大小

内存服务器如Redis

不受JVM堆内存大小的限制,但是受制于操作系统总的内存大小的限制。

2- 缓存更新的设计模式

当数据时效性要求很高时,需要保证缓存中的数据与缓存中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象。这就比较依赖缓存的过期和更新策略。一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存。

2.1- 引子:在更新缓存时,到底是,先删除缓存,然后再更新数据库,还是先更新数据库,然后再删除缓存?
  1. 先删后更:考虑并发的情况(只读缓存):两个并发请求,一个是要更新操作,另一个是查询操作,更新操作会致使当前缓存失效,删除缓存后;这时查询操作没有命中缓存,就会将数据库中的数据读出来放到缓存中,然后更新操作更新了数据库。于是此时缓存中数据并不是更新操作的新值,而是原来的数据库中的值。所以说这种更新策略是错误的
  2. 先更后删:如果更新数据库的时候,正好有读请求到达,此时读到的数据将是脏数据,但是当更新完数据库,会删除旧的缓存,等下次读请求到达时,没有命中缓存,会从数据库重新load到内存中,保证只出现因此脏数据,之后都是正确的数据

以下将介绍四种缓存更新的四种设计模式,遵循最佳实践少走弯路

Cache Aside Pattern(常用)
  • 失效:程序先从缓存中读数据,没有命中缓存,则程序从数据库中load到内存中。
  • 命中:程序先从缓存中读数据,正好命中缓存,则程序将缓存中的数组直接返回。
  • 更新:程序写操作先把数据存储到数据库中,成功后,程序再让缓存失效。(可能会出现几次脏读)
Read/Write Through Pattern(读写穿透模式)

Cache Aside模式中,同是要维护两个数据存储,一个是缓存,一个是数据库。所以比较啰嗦。而 Read/Write Through模式是将把数据库的操作由缓存自己代理了,所以就将缓存和数据库的操作合并在一次,对于数据访问来说面对的是单一存储,所以对于存储来说要同时维护自己的缓存。

Read Through

读操作中更新缓存:用缓存服务自己加载,对程序调用来说是透明的(不许要像Cache Aside模式程序还要自己加载数据到缓存),当读操作没有命中缓存时将触发Read Through

Write Through

写操作中更新缓存:当数据进行更新时,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己更新数据库(这是同步操作)

Write Behind Caching Pattern

在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库,优点是将缓存与数据库的同步异步化,带来飞快的数据I/O操作,而且可以将数据库的读写合并,所以对性能的会有较大的显著(特别是大数据量,数据库读写成为性能瓶颈你能过得时候),当缓存需要失效的时候,才会真正的进行持久化。

可能会出现数据不一致的情况:当系统掉电,缓存中数据还没有来得及写到数据库,则会造成一定的数据丢失,数据不是强一致的。但是性能和强一致性往往需要权衡。

注意
  1. 上面的缓存更新的设计模式还没有考虑缓存和数据库的整体事务的问题,比如更新了缓存,但是数据库更新失败了怎么办?或者数据库更新失败,但是缓存load失败了怎么办?解决方法是就是使用“两阶段提交协议”——prepare,commit/rollback。当然这样保证数据强一致性将导致性能的下降,实际设计时还需要权衡一下。
  2. 从上面的缓存更新的策略,也能看出来当只是查询数据时,缓存能抵挡绝大多数的DB查询,但是如果都是插入或者修改的操作业务(如秒杀等),每一次都将命中DB,这时候为了平滑瞬间并发DB操作,应该采用异步+消息队列的形式,或者使用Write Behind Caching Pattern。不管使用哪种方式,必须保证高可用,实时的将内存操作的数据刷盘。

3- 缓存饱和替换策略

探讨的是缓存中的数据达到缓存设定的内存使用上限,新插入的数据时,替选择什么样的原来的旧数据来替换的问题。

  1. Least Frequently Used(LFU)

每个缓存对象计算他们被使用的频率,把最不常用的缓存对象替换掉。

  1. Least Recently User(LRU):

把最近最少使用的缓存对象给替换,实现上会把最新被访问的缓存对象,放到缓存队列的顶部,可基于array 或者是LinkedHashMap来实现。

  1. Least Recently Used 2(LRU2):

也叫最近最少使用 twice。把被两次访问过的对象放入缓存队列中,当缓存池满了之后,会把有两次最少使用的缓存对象替换掉。因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。在大容量的缓存池中,就会有较高性能开销问题。另外,还需要跟踪那些不在缓存的对象,因为他们还没有被第二次读取。效果比LRU好,但是性能开销也大。

  1. Two Queues(2Q):

把被访问的数据放到 LRU 的缓存中,如果这个对象再一次被访问,我就把他转移到第二个、更大的 LRU 缓存。转移缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。当缓存的访问负载是固定的时候,把 LRU 换成 LRU2,就比增加缓存的容量更好。这种机制使得我比 LRU2 更好,改善了LRU2的性能。

  1. Adaptive Replacement Cache(ARC):

介于 LRU 和 LFU 之间,为了提高效果。由2个 LRU 组成: L1,包含的条目是最近只被使用过一次的; L2,包含的是最近被使用过两次的条目。因此, L1 放的是新的对象,而 L2 放的是常用的对象。被认为是性能最好的缓存算法之一,能够自调,并且是低负载的、很快,适用性也强。

  1. Most Recently Used(MRU):

和 LRU 相对应会移除最近最多被使用的对象。作用:接下来访问的随机性,并且在缓存系统中找出最少最近使用的对象是一项时间复杂度非常高的运算,这就是MRU存在的原因。

  1. First in First out(FIFO):

先进先出,是一个低负载的算法,并且对缓存对象的管理要求不高。通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被替换,然后把新的缓存对象加进去。很快,但是并不适用。

  1. Second Chance:

通过 FIFO 修改而来的,改善了 FIFO 的成本。在替换缓存,移除队首元素式,会检查即将要被移除的对象有没有之前被使用过的标志(1一个 bit 表示),没有被使用过,就把他移除;否则,会把这个标志位清除,然后把这个缓存对象当做新增缓存对象加入队列。这就像一个环队列。当再一次在队头碰到这个对象时,由于他已经没有这个标志位了,所以立刻就把他踢开了,在速度上比 FIFO 快。

  1. CLock

持有一个装有缓存对象的环形列表,头指针指向列表中最老的缓存对象。当缓存 miss 发生并且没有新的缓存空间时,会问问指针指向的缓存对象的标志位去决定应该怎么做。如果标志是0,我会直接用新的缓存对象替代这个缓存对象;如果标志位是1,会把头指针递增,然后重复这个过程,直到新的缓存对象能够被放入,比 second chance 更快。

  1. Simple time-based:

通过绝对的时间周期去失效那些缓存对象。对于新增的对象,会保存特定的时间。很快,但是并不适用。

  1. Extended time-based expiration:

通过相对时间去失效缓存对象的;对于新增的缓存对象,会保存特定的时间,比如是每5分钟,每天的12点。

  1. Sliding time-based expiration:

与前面不同的是,被管理的缓存对象的生命起点是在这个缓存的最后被访问时间算起的,很快,但是也不太适用。

其他需要注意的:
  1. 成本:如果缓存对象有不同的成本,应该把那些难以获得的对象保存下来。

  2. 容量:如果缓存对象有不同的大小,应该把那些大的缓存对象清除,这样就可以让更多的小缓存对象进来了。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,046评论 25 707
  • 需要原文的可以留下邮箱我给你发,这里的文章少了很多图,懒得网上粘啦 1数据库基础 1.1数据库定义 1)数据库(D...
    极简纯粹_阅读 7,380评论 0 46
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 作者:清风悲秋云冷漠的笼罩在山谷夜也不会再离去了此时已经入冬一切又趋于平静 没有谁会留在这山谷里农夫压弯腰挑去稻子...
    清风悲秋阅读 330评论 0 0
  • 生活中总是不缺这样的人:他们觉得生活哪哪都不如意,却又不愿意做出改变。他们负能量爆棚,整天把愤懑的枪口对准别人,觉...
    不等于阅读 484评论 4 2