分布式多级缓存SDK设计的思考

分布式多级缓存SDK设计的思考

  • 背景
  • 整体架构
    • 多层级组装
    • 回调埋点
    • 分区处理
    • 一致性问题
      • 缓存与数据库之间的一致性问题
      • 不同层级缓存之间的一致性问题
      • 不同微服务实例上,非共享缓存之间的一致性问题
  • 小结


之前实习期间编写过一个简单的多级缓存SDK,后面了解到一些其他的开源产品,如J2Cache,京东的JdHotKey,有赞的多级缓存SDK实现,所以本文想来总结一下我对多级缓存SDK设计的考量和开发心得。

参考的相关开源实现链接:

  • 有赞透明多级缓存解决方案(TMC)
  • J2Cache
  • hotkey
  • 实战干货 | 分布式多级缓存设计方案

背景

编写这个SDK起因于部门各个服务缓存使用上的不统一,有些没有使用缓存,有些单独使用了本地缓存或者Redis集中式缓存,还有些使用了阿里的Tair缓存,因此为了结束缓存使用混乱的局面,就有了这个多级缓存SDK Demo 。

我们期望这个多级缓存SDK能够满足以下目标:

  1. 支持自定义缓存层级和缓存层级之间的顺序,例如: 可以是Caffeine+Redis的组合,也可以是Caffeine + Tair + Redis的组合
  2. 需要与链路追踪工具Cat结合,定时上报缓存工作状态,如: 全局缓存命中率,各级缓存命中率等
  3. 需要支持灰度与开关机制,灰度用于控制走缓存比例,开关用于上下线该SDK,或者单独上下线某一级缓存
  4. 需要处理好缓存穿透,缓存击穿,缓存与数据库一致性,多级缓存间的一致性,以及分布式环境下,各个实例上非共享的L1级缓存的一致性问题。

以上四点是我目前所能想到的内容,也是我所开发的SDK支持的功能,如果大家有补充欢迎在评论区留言。

下面我将从整体架构讲起,一直聊到以上所说的细节实现。


整体架构

多级缓存SDK整体架构如何所示:

在这里插入图片描述

  1. CacheFacade 作为缓存门面对象,向用户屏蔽了内部多个模块协同工作的复杂性,同时负责编排多级缓存 get 和 set 的模版流程,并在相关位置进行回调埋点,方便后续扩展。
  2. CacheRepository 作为多级缓存实现,采用装饰器模式层级嵌套关系,缓存的 get 流程是先走低层级缓存,再走高层级缓存;set 和 del 流程是先走高层级缓存,再走低层级缓存。
  3. CacheConfig 作为配置模块,收拢了整个缓存SDK所有的配置项,同时采用SPI机制可以实现配置中心的动态切换,默认只提供了ApolloConfigProvider,用于支持Apollo作为配置中心。
  4. CachePostProcessor 顶层提供了相关默认接口实现,如果我们希望能够在缓存执行的某个流程处进行监听,可以重写相关接口实现,添加对应的拦截逻辑,然后将自身交于缓存后置处理模块管理即可。
  5. CacheCluster 负责实现多个实例之间的非共享L1级缓存的一致性,当有请求试图在某个实例上执行set或者del操作时,都需要广播告知其他实例,用于清除自身的L1级缓存。

整个缓存SDK的架构还是非常简单的,下面我将针对各处细节进行说明。


多层级组装

多级缓存SDK默认情况下会提供Caffeine+Redis的两级缓存,但是如果业务有特殊需求,不满足于此,我们也可以自定义缓存层级 :
在这里插入图片描述
为了支持自定义缓存层级,这里采用装饰器模式的层层装饰来实现多级缓存的效果,伪代码如下图所示:

public abstract class AbstractCacheRepositoryWrapper implements CacheRepository {private final CacheRepository wrappedCacheWrapper;public AbstractCacheRepositoryWrapper(CacheRepository wrappedCacheWrapper) {this.wrappedCacheWrapper = wrappedCacheWrapper;}...
}

接入方只需要为接入的缓存提供一个CacheRepository实现,并且自行完成装饰层级的嵌套组装,最后将组装得到的对象实例交由CacheFacade管理即可;如果项目使用到了Spring ,这里可以将对象实例注入容器,CacheFacade 由容器中取得即可。


回调埋点

CacheFacade 作为缓存门面对象,向用户屏蔽了内部多个模块协同工作的复杂性,同时负责编排多级缓存 get 和 set 的模版流程,并在相关位置进行回调埋点,方便后续扩展。

因为 CacheFacade 拿到的其实是已经组装完毕的多级缓存对象,如下图所示:
在这里插入图片描述
所以这里 get 和 set 请求要分为两段来看,一段是存在于缓存门面对象中设定好的模版流程,另一段是存在于AbstractCacheRepositoryWrapper中设定好的多级缓存间的get,set,del 流程。

我们先来看看缓存门面对象中设置设定好的模版流程和相关回调埋点的工作时机:

  • get 流程

在这里插入图片描述

当缓存失效需要重建的时候,为了避免多个实例并发重建,可以考虑使用逻辑过期+分布式锁确保只有一个实例会去执行重建操作。

  • list 流程
    在这里插入图片描述

  • set 流程

在这里插入图片描述

  • del 流程

在这里插入图片描述


上面可以理解为全局缓存的执行流程,下面我们来看看存在于AbstractCacheRepositoryWrapper中设定好的多级缓存间的get,set,del 流程:

  • get 流程

在这里插入图片描述

  • list 流程

在这里插入图片描述

  • set 流程

在这里插入图片描述

  • del 流程

在这里插入图片描述
上述流程图中各处埋点均以绿色标出,每当执行到埋点处时,都会去调用后置处理器链,后置处理器又分为全局后置处理器和局部后置处理器,前者工作在缓存门面设定的全局流程中,后者工作在多级缓存的局部流程中。

如果后续有扩展需求,只需要自定义一个后置处理器,加入后置处理器链中即可。


分区处理

我是从J2Cache中了解到的分区Region的思想,也在随后添加到了我自己开发的多级缓存SDK中,这里简单介绍一下为什么我们需要分区:

  1. 在实际的缓存场景中,不同的数据会有不同的 TTL 策略,例如有些缓存数据可以永不失效,而有些缓存我们希望是 30 分钟的有效期,有些是 60 分钟等不同的失效时间策略。在 Redis 我们可以针对不同的 key 设置不同的 TTL 时间。但是一般的 Java 内存缓存框架(如 Ehcache、Caffeine、Guava Cache 等),它没法为每一个 key 设置不同 TTL,因为这样管理起来会非常复杂,而且会检查缓存数据是否失效时性能极差。所以一般内存缓存框架会把一组相同 TTL 策略的缓存数据放在一起进行管理。
  2. 通过分区可以将属于不同业务的缓存隔离开来,防止相互污染,比如我们使用LRU缓存,所有业务共用一个LRU缓存,如果业务A总是大批次查询,那么可能会将其他业务热点key给淘汰出去,造成污染问题。

采用分区之后,CacheFacade 门面对象内部也就不是简单持有一个多级缓存实例对象了,而是持有一个多级缓存实例映射集合,如下图所示:

在这里插入图片描述
此时,我们的 get 和 set 等方法也都需要改造,在方法参数处添加一个 region,指明要操作哪个 region。


一致性问题

一致性问题主要考虑三点:

  1. 缓存与数据库之间的一致性问题
  2. 不同层级缓存之间的一致性问题
  3. 不同微服务实例上,非共享缓存之间的一致性问题

在这里插入图片描述


缓存与数据库之间的一致性问题

关于缓存与数据库之间的一致性问题,这里我简单介绍其中一种方案:

之前写过一篇文章讲述缓存与数据库一致性问题,这里就直接把图贴过来了

  1. 旁路缓存模式: 先更新数据库,再删除缓存
    在这里插入图片描述

可能存在的问题是: 两个并发线程,一个读,一个写,读线程发现缓存失效,去数据库查询数据,查询完后更新redis,但是更新redis前,写线程率先完成了写入操作,导致读线程最终放入redis的还是旧数据

在这里插入图片描述
不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

为了避免旁路缓存出现这个问题,我们可以采用缓存双删策略:

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?都是1秒?

  • 这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据产生,因此为了保险起见,我们需要增加删除失败的重试逻辑:

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

上述逻辑可能会造成业务代码入侵,我们可以考虑使用canal监听binlog的修改变更,将所有修改消息发送到MQ,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。


不同层级缓存之间的一致性问题

假设此时我的多级缓存层级是两层: Caffeine+Redis ,那么如何确保这两者之间的数据一致性呢 ?

首先,我们要明白一点:

  • 离服务越近的缓存源,其存储容量越小,速度越快,过期时间越短
  • 离服务越远的缓存源,其存储容量越大,速度越慢,过期时间越长

这里其实很像CPU多级缓存体系,为了保证多级缓存之间的数据一致性,需要分以下几个方面讨论:

  • 查询先从L1级缓存查起,如果L1没有,再查询L2,如果L2也没有,那么查询DB;返回阶段,会依次把上一级查询得到的结果回填到本级缓存,最终返回结果给到调用方。
  • set 操作是先设置L2级缓存,再设置L1级缓存,因为L2级缓存是共享的,设置完L2后,确保立刻对其他所有实例可见
  • del 操作是先删除L2级缓存,再删除L2级缓存,也是因为L2级缓存是共享的,删除完L2后,确保立刻对其他所有实例可见

这里是否还需要考虑其他的点,欢迎各位在评论区留言。


不同微服务实例上,非共享缓存之间的一致性问题

这里也是参考的L2Cache的思路,当我们对某个实例的非共享缓存层级执行修改或者删除操作的时候,我们需要借助消息广播,告知其他所有实例删除自己本地对应的缓存,以此确保多个实例之间的非共享缓存的一致性。

假设此时我们的多级缓存层级为: Caffeine+Redis , 当我们对实例1的本地缓存进行修改或者删除操作时,我们需要将操作涉及到的keys广播给其他所有实例;对应的实例接收到广播消息后,需要删除本地缓存中对应的keys,确保一致性。

在这里插入图片描述

Redis是集中式缓存,所以无需担心一致性问题。

这里其实和CPU多级缓存的一致性问题解决思路类似,因为CPU多级缓存中通常L1和L2级缓存都是单个核私有的,L3是共享的,所以同样存在如何实现一致性的问题。

这里消息广播可以借助于消息队列,Redis的pub/sub,zk ,etcd 或者在SDK中引入netty进行通信。


小结

这里有一点没提到,就是关于京东的JdHotKey和有赞的TMC,他们的缓存SDK设计思路更多是为了解决热点key探测与即时缓存到LocalCache,因此他们整体的架构设计就和文本不太一样了,简单来说如下图所示:

在这里插入图片描述
他们只使用到了集中式缓存Redis,只使用本地缓存进行热点key的缓存,而非全量缓存;同时为了确保强一致性,会监听redis过期key事件,当发生key过期事件时,会广播给所有实例,删除所有实例热点缓存中对应的key,确保强一致性。

本文仅为笔者个人拙见,如有理解错误,欢迎各位大佬在评论区留言指出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/139887.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

BUUCTF:[GYCTF2020]FlaskApp

Flask的网站,这里的功能是Base64编码解码,并输出 并且是存在SSTI的 /hint 提示PIN码 既然提示PIN,那应该是开启了Debug模式的,解密栏那里随便输入点什么报错看看,直接报错了,并且该Flask开启了Debug模式&am…

分类预测 | Matlab实现GA-RF遗传算法优化随机森林多输入分类预测

分类预测 | Matlab实现GA-RF遗传算法优化随机森林多输入分类预测 目录 分类预测 | Matlab实现GA-RF遗传算法优化随机森林多输入分类预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现GA-RF遗传算法优化随机森林多输入分类预测(完整源码和数据&…

ArtifactResolveException

bug描述 Caused by: org.gradle.api.internal.artifacts.ivyservice.DefaultLenientConfiguration$ArtifactResolveException: Could not resolve all files for configuration :app:debugCompileClasspath. 产生原因 一般可能是更换了新AndroidStudio导致的。依赖库未能成功…

gpt扣款失败,openai扣款失败无法使用-如何解决gpt扣款失败的问题?

gpt扣款失败,openai扣款失败无法使用。毕竟你花了钱却无法使用你所期待的服务,这种情况确实令人不快。但是, 为什么gpt扣款失败? 可能是由于支付问题导致的扣款失败。这包括信用卡额度不足、支付信息错误等等。如果你的支付信息…

数据结构与算法(C语言版)P2---线性表之顺序表

前景回顾 #mermaid-svg-sXTObkmwPR34tOT4 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-sXTObkmwPR34tOT4 .error-icon{fill:#552222;}#mermaid-svg-sXTObkmwPR34tOT4 .error-text{fill:#552222;stroke:#552222;}#…

安全基础 --- nodejs沙箱逃逸

nodejs沙箱逃逸 沙箱绕过原理:沙箱内部找到一个沙箱外部的对象,借助这个对象内的属性即可获得沙箱外的函数,进而绕过沙箱 前提:使用vm模块,实现沙箱逃逸环境。(vm模式是nodejs中内置的模块,是no…

WebGIS面试题(浙江中海达)

1、Cesium中有几种拾取坐标的方式,分别介绍 Cesium是一个用于创建3D地球和地理空间应用的JavaScript库。在Cesium中,你可以使用不同的方式来拾取坐标,以便与地球或地图上的对象进行交互。以下是Cesium中几种常见的拾取坐标的方式&#xff1a…

微服务学习(七):docker安装Mysql

微服务学习(七):docker安装Mysql 1、拉取镜像 docker pull mysql2、查看安装的镜像 docker images3、安装mysql docker run -p 3306:3306 --name mysql \ -v /mydata/mysql/log:/var/log/mysql \ -v /mydata/mysql/data:/var/lib/mysql \…

TensorFlow安装 ,在原本的虚拟环境下配置Tensorflow.

1.TensorFlow安装 ,在原本的虚拟环境下配置Tensorflowh和pytorch 2.我首先在anaconda的环境下创建了一个tensorflow文件夹 如何先进入D盘,再进入tensorflow文件夹的目录D:cd D:\Anaconda\TensorFlowSoftWarepip install tensorflow如图所示报错解决方法 …

Antdesign 4中让分页组件居中显示的方法

在Ant Design 4中分页组件默认是最右边显示的,而这个没有设置位置的属性的 解决办法: 在pagination的属性中增加: style: {textAlign: "center"} 在Ant Design 5中可以让pagination使用align: center来实现分页组件居中

小程序中如何查看会员的访问记录

​在小程序中,我们可以通过如下方式来查看会员的访问记录。下面是具体的操作流程: 1. 找到指定的会员卡。在管理员后台->会员管理处,找到需要查看访客记录的会员卡。也支持对会员卡按卡号、手机号和等级进行搜索。 2. 查看会员卡详情。点…

【自然语言处理】【大模型】MPT模型结构源码解析(单机版)

相关博客 【自然语言处理】【大模型】MPT模型结构源码解析(单机版) 【自然语言处理】【大模型】ChatGLM-6B模型结构代码解析(单机版) 【自然语言处理】【大模型】BLOOM模型结构源码解析(单机版) 【自然语言处理】【大模型】极低资源微调大模型方法LoRA以及BLOOM-LORA实现代码 【…

Cesium 空间量算——生成点位坐标

文章目录 需求分析1. 点击坐标点实现2. 输入坐标实现 需求 用 Cesium 生成点位坐标,并明显标识 分析 以下是我的两种实现方式 第一种是坐标点击实现 第二种是输入坐标实现 1. 点击坐标点实现 //点位坐标getLocation() {this.hoverIndex 0;let that this;this.view…

【链表】排序链表-力扣148题

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…

思维链(Chain-of-Thought Prompting Elicits Reasoning in Large Language Models)

概括 论文主要描述了一种用思维链的提升LLM模型推理能力的方式,并且通过实验的方式,证明了思维链在算术、常识和符号等任务方面的显著效果。仅通过540B大小的PaLM模型,通过8个思维链样例就可以实现在GSM8K上的sota效果。 具体工作 这篇论文…

OpenGLES:使用纹理绘制一张图片

一.概述 最近疏于写博客,接下来会陆续更新这段时间OpenGLES的一些开发过程。 前两篇OpenGLES的博客讲解了怎样使用OpenGLES实现相机普通预览和多宫格滤镜 在相机实现过程中,虽然使用到了纹理,但只是在生成一个纹理之后,使用纹理…

精华回顾:Web3 前沿创新者在 DESTINATION MOON 共话未来

9 月 17 日,由 TinTinLand 主办的「DESTINATION MOON: Web3 Dev Summit Shanghai 2023」线下活动在上海黄浦如约而至。 本次 DESTINATION MOON 活动作为 2023 上海区块链国际周的 Side Event,设立了 4 场主题演讲与 3 个圆桌讨论,聚集了诸多…

Goby 漏洞发布|Revive Adserver 广告管理系统 adxmlrpc.php 文件远程代码执行漏洞(CVE-2019-5434)

漏洞名称:Revive Adserver 广告管理系统 adxmlrpc.php 文件远程代码执行漏洞(CVE-2019-5434) English Name: Revive Adserver adxmlrpc.php Remote Code Execution Vulnerability (CVE-2019-5434) CVSS core: 9.0 影响资产数&a…

【自学记录】深度学习入门——基于Python的理论与实现(第3章 神经网络)

3.4.3 3层神经网络Python实现 实现的是这个网络 **init_network()**函数会进行权重和偏置的初始化,并将它们保存在字典变量network中。这个字典变量network中保存了每一层所需的参数(权重和偏置)。 **forward()**函数中则封装了将输入信号转换为输出信号的处理过程…

ReadPaper论文阅读工具

之前看文献一直用的EndNote嘛,但是突然发现了它的一个弊端,就是说每次没看完退出去之后,下次再接着看的时候它不能保留我上一次的位置信息,又要重头开始翻阅,这让我感到很烦躁哈哈。(当然也不知道是不是我哪…