数据引擎:统一缓存数据平台

任何一个互联网公司都会有几个核心盈利的业务,我们经常会给基础核心业务做一些增值服务,以此来扩大我们的服务范围以及构建产业链及产业生态,但是这些增值服务需要核心项目的数据及交互才能更好地提供服务。但核心系统如果对增值业务系统做太多的耦合适配,就会导致业务系统变得十分复杂,如何能既让增值服务拿到核心系统的资源,又能减少系统之间的耦合?这节课我会重点带你了解一款内网主动缓存支撑的中间件,通过这个中间件,可以很方便地实现高性能实体数据访问及缓存更新。

回顾临时缓存的实现

我们先回顾下之前展示的临时缓存实现

// 尝试从缓存中直接获取用户信息 
userinfo, err := Redis.Get("user_info_9527") 
if err != nil { 
return nil, err 
} 
//缓存命中找到,直接返回用户信息 
if userinfo != nil { 
return userinfo, nil 
} 
//没有命中缓存,从数据库中获取 
userinfo, err := userInfoModel.GetUserInfoById(9527) 
if err != nil { 
return nil, err 
} 
//查找到用户信息 
if userinfo != nil { 
//将用户信息缓存,并设置TTL超时时间让其60秒后失效 
  Redis.Set("user_info_9527", userinfo, 60) 
return userinfo, nil 
} 
// 没有找到,放一个空数据进去,短期内不再访问数据库 
// 可选,这个是用来预防缓存穿透查询攻击的 
Redis.Set("user_info_9527", "", 30) 
return nil, nil

上述代码演示了临时缓存提高读性能的常用方式:即查找用户信息时直接用 ID 从缓存中进行查找,如果在缓存中没有找到,那么会从数据库中回源查找数据,找到数据后,再将数据写入缓存方便下次查询。相对来说这个实现很简单,但是如果我们所有业务代码都需要去这么写,工作量还是很大的。即便我们会对这类实现做一些封装,但封装的功能在静态语言中并不是很通用,性能也不好。那有没有什么方式能统一解决这类问题,减少我们的重复工作量呢?

实体数据主动缓存

之前我们在第二节课讲过实体数据最容易做缓存,实体数据的缓存 key 可以设计为前缀 + 主键 ID 这种形式 。通过这个设计,我们只要拥有实体的 ID,就可以直接在缓存中获取到实体的数据了。为了降低重复的工作量,我们对这个方式做个提炼,单独将这个流程做成中间件,具体实现如下图:

我们借助 canal 对 MySQL 数据库的 binlog 日志展开监控操作。一旦数据库中有数据发生变更,消息监听端便会即刻接收到相应的变更通知。鉴于变更消息里涵盖了变更的表名以及所有发生变更数据的主键 ID,基于此,我们能够凭借这些主键 ID 返回数据库主库去查询出最新的实体数据。随后,依据实际需求对查询到的数据加以加工处理,进而将其推送至缓存之中。

从以往的经验来分析,许多刚刚发生变动的数据在短期内被读取的可能性极大,所以这样的实现方式往往能够实现较为可观的缓存命中率。

另外,当数据被缓存之后,会依照所设定的配置确定一个 TTL(生存时间)。倘若缓存数据在一段时间内未被读取,那么就会依据 LRU(最近最少使用)策略将其从缓存中淘汰掉,如此一来便能够节省缓存空间。

不过,经过仔细考量就会察觉到这种设计存在一定的缺陷。具体而言,要是业务系统无法从缓存当中获取到所需的数据,那么依旧需要回到数据库去查找数据,并且还得再次把查找到的数据放置到缓存里,这显然与我们最初的设计意图不相符合。基于此,我们还需要配备一个专门的缓存查询服务,详情可参照下图:

一、缓存未命中时的处理流程

如上图所示,在查找缓存的过程中,倘若未能找到所需数据,此时中间件会依据 Key 来识别出待查数据所属的数据库表以及对应的处理脚本。接着,按照预先配置好的要求执行该脚本,以此对数据库展开查询操作并进行数据加工。完成数据获取后,中间件会将这些数据回填到缓存当中,最后再把查询结果返回。

二、查询服务的优化建议

为了有效提高查询效率,建议查询服务采用类似 Redis 的纯文本长链接协议。并且,该查询服务还应当支持批量获取功能,就如同 Redis 的 mget 实现方式那样。要是我们的数据支撑架构较为复杂,而且一次查询涉及的数据量很大,那么可以将其设置为批量并发处理的模式,这样能够提升系统的吞吐性能。

三、缓存穿透问题的应对措施

在落地缓存服务时,存在一些实际操作方面的技巧需要我们关注。当查询缓存时发现数据不存在,就可能引发请求缓存穿透的问题,一旦请求量过大,核心数据库便会面临崩溃的风险。

为了预防此类问题的发生,我们需要在缓存中添加一个特殊标志。如此一来,当查询服务查不到数据时,就能够直接返回数据不存在的提示信息。

同时,我们还需要考虑到万一真的出现缓存穿透问题时,应当如何对数据库的并发数加以限制。在此建议使用 SingleFlight 来合并并行请求,无需采用全局锁,只需在每个服务范围内实现即可。

四、多表数据查询与缓存刷新的要求

有时,我们要查询的数据会分布在数据库的多个表内,这种情况下,可能需要将多个表的数据进行组合,或者需要对多个缓存进行刷新操作。所以,这就要求我们的缓存服务能够提供定制脚本,只有这样才可以实现对业务数据的有效刷新。

五、缓存同步问题的排查辅助

另外,由于涉及数据库和缓存这两个系统之间的同步操作,为了更便于排查缓存同步过程中出现的问题,建议在数据库中和缓存中都记录下数据最后更新的时间,以便后续进行对比分析。

六、业务数据查找的便捷性

到这里,我们所构建的服务就基本完整了。当业务有按 id 查找数据的需求时,直接调用数据中间件就能够获取到最新的数据,无需再进行重复的实现操作,从而使得开发过程变得简单许多。

L1 缓存及热点缓存延期

此前我们所设计的缓存中间件,在应对大多数临时缓存需求的场景时,是能够发挥其作用的。然而,一旦遇到大并发查询的场景,倘若缓存出现了缺失或者过期这类状况,那么将会给数据库带来极大的压力。所以,针对这一情况,我们有必要对该服务继续加以改进。

改进的具体方式为对查询次数进行统计,以此来判定被查询的 key 是否属于热点缓存。这里可以举个例子来进一步说明,比如我们能够通过划分时间块的方式进行异步统计,统计在 5 分钟这样一个时间段内缓存 key 被访问的具体次数,要是在单位时间内其被访问的次数超过了依据业务实际情况所设定的次数,那么该缓存 key 就可被认定为热点缓存。

关于具体的热点缓存统计以及续约流程,大家可以参照下图所示内容:

一、热点缓存处理流程

对照流程图能够清晰地看到,热点统计服务在获取了被认定为热点的 key 之后,会依据统计次数的大小进行区分处理。对于那些访问频率极高的 key,会定期通过脚本将其推送到 L1 缓存中(L1 缓存可以部署在每台业务服务器上,或者每几台业务服务器共用一个 L1 缓存)。当业务进行数据查询时,业务的查询 SDK 驱动会依据热点 key 配置,检测当前 key 是否属于热点 key。若是热点 key,则会前往 L1 缓存获取数据;若不是热点缓存,则会去集群缓存获取数据。

而对于相对频率较高的 key 热点缓存服务,只会定期通知查询服务刷新对应的 key,或者进行 TTL 刷新续期的操作。

二、热点缓存退热处理

当我们所查询的数据退热后,该数据在时间块的访问统计数值会随之下降。此时,L1 热点缓存推送或者 TTL 续期操作会停止继续进行,不久之后数据便会因 TTL 过期而失效。

三、缓存中间件的发展与不足

增加了这个功能之后,这个缓存中间件就可以更名为数据缓存平台了。不过,它与真正意义上的平台还存在一些差距。因为这个平台目前仅能够提供实体数据的缓存功能,对于需要灵活加工推送的数据,还无法很好地支持,一些业务结构代码仍然需要人工来实现。

关系数据缓存

一、改进消息监听服务

首先要对消息监听服务加以改进,将其打造为 Kafka Group Consumer 服务的形式,并且要使其具备可动态扩容的特性。通过这样的改造,能够有效提升系统的并行数据处理能力,从而使其可以应对更为庞大数量的并发修改操作。

二、引入多种数据引擎

其次,针对数据量级更高的缓存系统而言,我们可以引入多种不同的数据引擎,让它们共同为系统提供多样化的数据支撑服务。以下是对各数据引擎的简要介绍:

  1. lua 脚本引擎:它就好比是数据推送的 “发动机”,在数据处理过程中发挥着重要作用。具体来说,通过回顾第十七节课的内容大家可以了解到,它能够助力我们把数据动态同步到多个数据源,实现数据在不同数据源之间的灵活流转。
  2. Elasticsearch :这一引擎主要负责提供全文检索功能。当我们需要在海量数据中快速、精准地查找包含特定关键词的信息时,Elasticsearch 就能大显身手,满足我们的检索需求。
  3. Pika :它主要负责提供大容量 KV 查询功能。可能对于大家来说,Pika 相对比较陌生,其实它可以被理解为是 RocksDB 的加强版,其功能也是围绕着提供高效的大容量键值对(KV)查询服务来展开的。
  4. ClickHouse :该引擎着重负责提供实时查询数据的汇总统计功能。在面对需要对实时数据进行快速汇总统计分析的业务场景时,ClickHouse 能够及时给出准确的结果,满足业务对于实时数据处理的要求。
  5. MySQL 引擎:它的主要职责是支撑新维度的数据查询。当我们需要从一些新的角度或者维度去查询数据时,MySQL 引擎就能够凭借其自身的特性和功能,为我们提供相应的数据查询支持。

大家不难发现,在之前的课程当中,除了 Pika 可能会让大家感觉稍微有些陌生之外,其余的几个引擎其实都有过涉及。这里只是对各个引擎擅长的方面进行了概括性的描述,并没有逐一展开详细讲解。如果各位对这些引擎感兴趣,想要深入探究的话,可以自行开展进一步的研究,去深入了解不同引擎分别适合应用在哪些具体的业务场景之中。

多数据引擎平台

一个理想状态的多数据引擎平台是十分庞大的,需要投入很多人力建设,它能够给我们提供强大的数据查询及分析能力,并且接入简单方便,能够大大促进我们的业务开发效率。为了让你有个整体认知,这里我特意画了一张多数据引擎平台的架构图,帮助你理解数据引擎和缓存以及数据更新之间的关系,如下图所示:

可以看到,这时基础数据服务已经做成了一个平台。MySQL 数据更新时,会通过我们订阅的变更消息,根据数据加工过滤进程,将数据推送到不同的引擎当中,对外提供数据统计、大数据 KV、内存缓存、全文检索以及 MySQL 异构数据查询的服务。具体业务需要用到核心业务基础数据时,需要在该平台申请数据访问授权。如果还有特殊需要,可以向平台提交数据加工 lua 脚本。高流量的业务甚至可以申请独立部署一套数据支撑平台。

总结

我们一起学习了统一缓存数据平台的实现方案,有了这个中间件,研发效率会大大提高。在使用数据支撑组件之前,是业务自己实现的缓存以及多数据源的同步,需要我们业务重复写大量关于缓存刷新的逻辑,如下图:

而使用数据缓存平台后,我们省去了很多人工实现的工作量,研发同学只需要在平台里做好配置,就能坐享中间件提供的强大多级缓存功能、多种数据引擎提供的数据查询服务,如下图所示:

们回顾下中间件的工作原理。首先我们通过 Canal 订阅 MySQL 数据库的 binlog,获取数据的变更消息。然后,缓存平台根据订阅变更信息实现触发式的缓存更新。另外,结合客户端 SDK 及缓存查询服务实现热点数据的识别,即可实现多级缓存服务。

可以说, 数据是我们系统的心脏,如数据引擎能力足够强大,能做的事情会变得更多。数据支撑平台最大的特点在于,将我们的数据和各种数据引擎结合起来,从而实现更强大的数据服务能力。

大公司的核心系统通常会用多引擎组合的方式,共同提供数据支撑数据服务,甚至有些服务的服务端只需做配置就可以得到这些功能,这样业务实现更轻量,能给业务创造更广阔的增值空间。

6