关于分布式“缓存”的思考

一 缓存的划分

从由谁来维护缓存的角度去划分Cache-Aside模式和Cache-Proxy模式。

1 .调用方自己维护缓存(Cache-Aside模式)

调用方的伪代码:

value = get_from_cache(key);
if (value == null) {
    value = get_value_from_database(key);
    set_cache(key, value)
}

业务调用方自己感知缓存的状态,如果命中则从缓存中取,不命中则从DB拉取数据,并更新缓存。

1)In-Memory(本地私有缓存):

本地缓存的模式下,get_from_cache是本地函数调用(无网络交互),尝试从本地cache中获取缓存数据;如果命中则返回数据,不命中则从DB拉取数据后,更新本地的缓存。

2)Shared-Cache(远端共享存储):

远端缓存的模式下,get_from_cache是RPC调用(需网络交互),尝试从memcached、redis等缓存中间件中拉取缓存数据;如果命中则返回数据,不命中则从DB拉区数据之后,更新远端的缓存。

In-Memory VS Shared-Cache:

1)In-Memory模式,效率更高,少网络交互,但是容易出现数据不一致。如果A和B本地都缓存了相同的数据,那么A更新之后,如果通知B更新,就变成了一个比较复杂的问题。几个参考方案可。

● A和B按照号段划分数据,各自缓存不同的数据,处理不同的请求。这样可以保证自己的缓存数据不会与别人的冲突,避免了复杂的一致性问题;

● 有些情况(例如单号段的请求量非常大)A和B必须缓存相同的数据,可以考虑引入一些简单的一致性策略。例如参考微信账号体系的缓存方式,引入远端版本号服务器解决问题等(参考文献2《微信账号体系的缓存解决方案》)。

2)Shared-Cache模式,需要网络交互,但是数据一致性相对容易实现(当然仔细考虑下,如果要完美实现也不是很容易的)。相对而言,访问cache的效率低一些;网络流量较大;需要考虑网络超时等异常和操作相同节点的并发情况。

2.委托缓存代理侧维护(Cache-Proxy模式)

调用方的伪代码简化为:

value = get_from_cache_proxy(key);

Cache-Proxy(也称之为Cache-Service),带来的好处如下:

● 对调用方隔离了DB操作。在调用方看来,访问Cache-Proxy就实现了访问DB的能力,不关心更新缓存的细节; ● 调用方可以简化调用协议。通常更新缓存和请求DB的协议不同,而这个协议转换的工作,完全交给了Cache-Proxy去实现; ● 各种缓存调整对调用方透明(隔离)。选择不同的淘汰策略算法、调整缓存大小、命中率等等。

从读取和更新缓存的方式去划分:

1)Read-Through:如果读不命中缓存,那么就从DB加载数据,并更新缓存; 2)Write-Through:更新缓存的同时去更新DB,每次更新都触发写DB(类似写文件,每次都sync到磁盘); 3)Write-Behind:更新缓存并不触发更新DB,而是延迟一段时间再写回DB(类似写文件,累计一定的修改之后,一起写回磁盘)。

Write-Through VS Write-Behind: 1)Write-Through模式:没有脏数据,Cache的数据始终与DB的数据一致。但由于写请求都直接透传到DB,所以DB的亚历山大!采用这个模式,要充分评估好DB侧的负载。 2)Write-Behind模式:有脏数据,Cache的数据与DB不一致。但是该模式可以很好的降低DB侧的写请求,平滑负载。但是如果Cache侧宕机或者Crash,造成的就是数据丢失。所以如何保证数据一致性,采用什么策略回写数据到DB,都是需要好好考虑的问题。

关于缓存的更新也有挺多细节可以好好考虑下,陈皓的《缓存更新套路》中,介绍了一些,可以参考下(参考文献1.1《缓存更新的套路》)。

二 缓存的主从同步

(以下针对Cache-Proxy模式讨论)

缓存是否需要主从备份?

可以从两个因素来判断缓存是否需要同步: 1.数据一致性。如果采用了Write-Beind模式,那么Cache就会有脏数据,如果此时没有主从备份,那么Cache所在进程或服务器出问题,就会造成脏数据丢失,数据回档。这种数据回档的情况业务是否容忍? 2.可用性。即使采用Write-Through模式,或者Cache仅仅是用于读请求,如果Cache侧挂掉,服务是否会受影响而中断?这种业务中断是否容忍?

缓存如何进行主从同步?

之前写过一篇关于主从同步的文章《主从同步中的关键技术解析》,对比介绍了Redis、Mysql、TCaplus的主从同步中用到的关键技术。

缓存主从同步的解决方案更加灵活:

1.全数据同步。这种可以参考Redis、Mysql等组件的同步思路,切片数据+增量binlog的方式进行同步。例如DCache就是采用类似方式实现; 2.必要数据同步。Cache+DB作为存储,与纯存储的主从同步问题不同,见下图:

缓存数据一致性的问题,本质上是: 主Cache、备Cache、DB三者对外提供的数据一致性。所以有以下特点:

1) master中的脏数据,slave必须要有,否则切换会回档;

2)master中与DB一致的干净数据,slave可以缺少;

这样slave在升级为master的时候,不存在的数据会从DB拉取,所以就以DB为准,这样一来数据一致性可以保证。

不过,slave如果只同步master的脏数据,很可能切换的瞬间,由于缓存命中率低,导致压力瞬间透传给后端DB,造成瞬间服务不可用。所以从可用性的角度讲,建议也把主机的热数据也同步过去。

所以,我们的存储缓存Cube采用了分级同步的方式处理,优先保证数据一致性,其次是数据可用性,同步示意图如下:

优点:

1.分级同步,把危险期降到最低。脏数据同步完,slave就已经可以保证数据一致了,此时就具备最基本的切换能力。脏数据通常占比不高,从我们线上的业务看,峰值占总内存量的1/10,所以脏数据的同步最重要,耗时也较短;

2.热数据可以选择性的同步。热点数据较多的业务,可以设置同步的量大一些,甚至是把主机的所有数据都同步过去,实现主备数据完全一致。这个过程耗时较长,不过即使同步过程中主机挂掉,也不影响数据一致性,所以热点数据的同步速度可以相对慢一些。

缺点:

master和slave之间必须采用row-based binlog方式同步,不可以使用statement-based binlog。

这里再简单介绍下这两种binlog格式的差别:statement-based binlog就是一条操作语句,描述的是执行过程,例如:把某个数据记录中的字段A执行+1操作。row-based binlog描述的是操作执行的结果,例如:字段A执行+1操作之后的结果是什么。

因为slave中的数据量会比master要少,所以statement-based binlog如果在一个不存在的结果上执行,结果肯定是错误的。但是如果是row-based binlog,slave接收到的是执行后的结果,所以就可以直接存储下来。

三 空查询问题

什么是空查询?空查询简单理解就是:到缓存中查询连DB都不存在的数据的请求。

可以不夸张的说:空查询绝对是缓存的天敌。为什么?因为缓存不命中的时候,缓存侧以为是自己没有cache到这个数据,所以把请求透传到后端DB,而DB中也么有这个数据,所以最后缓存中也没被填充上,下次类似的请求过来的时候,同样会走一遍相同流程,很容易导致DB直接过载!

有人可能会问,能不能把不命中的请求也缓存下来,这样下次相同的请求过来,缓存侧就知道这个数据不存在,从而避免再到DB侧拉取数据了呢?答案是不可以!因为空查询的情况可能千差万别。

举个栗子:有一个好友查询系统负责查询玩家A是不是玩家B的好友,如果不是好友就会导致空查询。假设缓存Cache下来A和B不是好友,那么如果前端请求就是各种查询好友关系呢?岂不是缓存系统就会Cache一堆非好友关系??这种消耗肯定是不值得而且得不偿失的。

解决上述问题的一种简单方法是业务方换一种方式拉取数据:好友查询系统一次把所有玩家的好友拉取回去,然后判断是否是好友关系,这样就不会造成空查询。

CKV也遇到过类似的问题,CKV=CMem(纯内存)+TSSD(ssd硬盘),支持一种模式是可以把热点数据缓存在CMem,而把超过多少天没有访问的数据下沉到TSSD。所以CMem中就会因为空查询把请求都透传到TSSD,导致过载的案例:《开空查询对CKV的影响》,最后的解决方案就是在TSSD侧,引入bloom filter的方式,见参考文献3《布隆过滤器loom filter》。

四 总结

本文简单介绍了下分布式缓存的分类、同步和空查询等三个问题。

  1. 从分类看缓存,每个类型都有其自己的特点和优缺点,如何抉择和权衡数据一致性和可用性,是在设计缓存初就需要思考好的问题;
  2. 缓存同步方面,给出了另外一种缓存同步的思路,缓存的主从可以不完全一致,而且数据可以分级同步,效果可能更好;
  3. 空查询问题,这个问题我们在外网也遇到过的头痛问题,缓存也必须要考虑到这方面问题,才不至于因为空查询而失效。

个人经验,和大家一起分享下!

五 参考文献

1 . 关于缓存设计模式的参考文献。

1)《缓存更新的套路》陈皓

2)《Cache Usage Patterns》介绍了几种缓存模式

3)《Caching Guidance》微软云设计模式中缓存的介绍

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员的知识天地

Linux 系统结构详解

内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。部分层次结构如图1-...

54920
来自专栏linux驱动个人学习

Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)

因此当前linux的调度程序由两个调度器组成:主调度器,周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core sch...

38520
来自专栏漏斗社区

嗤!给你来点fiyocms漏洞喷雾

0x01 背景 上周发的phpcms的漏洞分析,有些伙伴觉得比较复杂,于是便诞生本篇,通过审计一些普通的cms来一步步学习代码审计的技巧。 本篇涉及的源码...

398100
来自专栏数据库

MySQL线程池问题个人整理

见识了智能合约以及以太坊的工作方式,现在我们就尝试将它部署到两种测试网络里面。

1.2K100
来自专栏古时的风筝

用python实现的百度音乐下载器-python-pyqt-改进版

之前写过一个用python实现的百度新歌榜、热歌榜下载器的博文,实现了百度新歌、热门歌曲的爬取与下载。但那个采用的是单线程,网络状况一般的情况下,扫描前100首...

31980
来自专栏程序员互动联盟

怎样快速调试linux内核?有哪些需要注意的问题?

这个问题就比较专业了,linux内核调试还是在调试内核驱动的时候用过,涉及的程度不是特别深,但是可以说下大致的思路,linux虽然贵为操作系统,但是归根到底还是...

12830
来自专栏云计算教程系列

为你的服务器增加Swap分区

避免应用程序内存不足错误的最简单方法之一是为服务器添加一些Swap空间。Swap分区在系统的物理内存不够用的时候,把物理内存中的一部分空间释放出来,以供当前运行...

41150
来自专栏开发与安全

linux系统编程之基础必备(七):read/write函数与(非)阻塞I/O的概念

一、read/write 函数 read函数从打开的设备或文件中读取数据。 #include <unistd.h> ssize_t read(int fd,...

41400
来自专栏马涛涛的专栏

使用leancloud给简历加数据库,实现留言功能

数据必须存在服务器上,这样任何设备访问服务器都可以得到数据,如果存在客户端的本地,那么其他客户端设备无法读取到.所以数据必须存储在服务器的数据库上

23650
来自专栏Youngxj

评论回复微信提醒-emlog插件

21930

扫码关注云+社区

领取腾讯云代金券