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

一 缓存的划分

从由谁来维护缓存的角度去划分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 删除。

编辑于

我来说两句

1 条评论
登录 后参与评论

相关文章

来自专栏PHP技术

php + mysql 分布式事务

事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元; 事务应该具有4个属性:原子性、一致性、隔离性、持续性; 原子性(atomi...

4016
来自专栏Java架构沉思录

Zookeeper总览

ZooKeeper是一款开源的 分布式应用 的 分布式协调服务 。它包含一个简单的 原语集 ,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。Zoo...

651
来自专栏架构师之路

跨公网调用的大坑与架构优化方案

第三方接口挂掉,我们的服务会受影响么? 一、缘起与大坑 很多时候,业务需要跨公网调用一个第三方服务提供的接口,为了避免每个调用方都依赖于第三方服务,往往会抽象一...

3806
来自专栏匠心独运的博客

消息中间件—RocketMQ消息存储(一)一、MQ消息队列的一般存储方式二、RocketMQ消息存储整体架构三、RocketMQ文件存储模型层次结构四、总结

文章摘要:MQ分布式消息队列大致流程在于消息的一发一收一存,本篇将为大家主要介绍下RocketMQ存储部分的架构 消息存储是MQ消息队列中最为复杂和最为重要的...

472
来自专栏java一日一条

RabbitMQ之消息确认机制(事务+Confirm)

在使用RabbitMQ的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的消息丢失,除此之外我们还会遇到一个问题,当消息的发布者在将消息发送出去之...

633

Apache ZooKeeper vs. etcd3

在实现分布式服务协调方案时,有许多出色的系统,如 Apache ZooKeeper,etcd,consul 和 Hazelcast。如果您还没有听说过分布式协调...

2202
来自专栏架构师之路

webim如何用轮询保证消息绝对实时

webim如何使用http长轮询保证消息的绝对实时性 一、webim如何实现消息推送 webim通常有三种方式实现推送通道: 1)WebSocket 2)Fla...

2827
来自专栏北京马哥教育

【图文并茂】一步步带你了解Web站点架构

1.1 http反向代理服务器 在web站点前端,我们需要搭建一个反向代理服务器,用于负责接受用户的请求,请求包括动态和静态的内容请求。一般反向代理服务器的部署...

4258
来自专栏曾令武的专栏

消息队列及常见消息队列介绍

消息队列是分布式系统中重要的组件,在很多生产环境如商品抢购等需要控制并发量的场景下都需要用到。最近组内需要做流水server的选型升级,这里对消息队列及常见的消...

5.6K4
来自专栏决胜机器学习

RabbitMQ(二) ——工作队列

RabbitMQ(二)——工作队列 (原创内容,转载请注明来源,谢谢) 一、概述 工作队列模式(work queue),是有多个消费者的情况下,可以共同消费队...

2824

扫码关注云+社区