从由谁来维护缓存的角度去划分Cache-Aside模式和Cache-Proxy模式。
调用方的伪代码:
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的效率低一些;网络流量较大;需要考虑网络超时等异常和操作相同节点的并发情况。
调用方的伪代码简化为:
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 . 关于缓存设计模式的参考文献。
1)《缓存更新的套路》陈皓
2)《Cache Usage Patterns》介绍了几种缓存模式
3)《Caching Guidance》微软云设计模式中缓存的介绍
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。