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

一 缓存的划分

从由谁来维护缓存的角度去划分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 条评论
登录 后参与评论

相关文章

来自专栏ThoughtWorks

Spring Batch在大型企业中的最佳实践|洞见

在大型企业中,由于业务复杂、数据量大、数据格式不同、数据交互格式繁杂,并非所有的操作都能通过交互界面进行处理。而有一些操作需要定期读取大批量的数据,然后进行一系...

3618
来自专栏双十二技术哥

组件化实践详解(二)

在上一篇文章《组件化实践详解(一)》中我们介绍了组件化实践的目标和实践步骤,本文继续说说关于组件化实践遇到的问题及思考。

924
来自专栏张善友的专栏

ASP.NET 调味品:AJAX

Karl Seguin 适用于: AJAX(异步 JavaScript 和 XML) Microsoft AJAX.NET Microsoft ASP.NET ...

1945
来自专栏ImportSource

Java9来了,快来了解下JPMS基础吧!

Java平台模块系统(JPMS)是Java SE 9的主要新功能。在本文中,Java Champion和JAX Londonspeaker的Stephen Co...

3558
来自专栏大史住在大前端

webpack4.0各个击破(6)—— Loader篇

loader是webpack的核心概念之一,它的基本工作流是将一个文件以字符串的形式读入,对其进行语法分析及转换(或者直接在loader中引入现成的编译工具,例...

901
来自专栏程序小工

PHP性能优化

PHP 运行环境的性能考虑在 php 深入学习中需要逐步强化意识,并着手实现,其中对于性能分析的相关工具也需要有一定的掌握,比如压力测试工具 Apache Be...

1633
来自专栏用户2442861的专栏

初学Redis(1)——认识Redis

http://blog.csdn.net/qtyl1988/article/details/39553339

942
来自专栏JAVA技术zhai

分享30道Redis面试题,面试官能问到的我都找到了

Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到...

1412
来自专栏一名合格java开发的自我修养

Storm同步调用之DRPC模型探讨

摘要:Storm的编程模型是一个有向无环图,决定了storm的spout接收到外部系统的请求后,spout并不能得到bolt的处理结果并将结果返回给外部请求。...

901
来自专栏大内老A

[WCF 4.0新特性] 默认绑定和行为配置

对于传统的WCF配置系统,无论是绑定的配置还是行为(服务行为和终结点行为)都必须具有一个名称。而正是通过整个配置名称,它们才能被应用到目标对象(终结点或者服务)...

17410

扫码关注云+社区