分布式多级缓存设计方案bb

设计背景

概念

先简单解释下什么是分布式多级缓存,所谓分布式简单理解就是异地跨机房服务应用部署;所谓多级缓存,这里狭义语义指定的是应用服务级别的缓存,通常泛指RedisMemcached等;所谓多级缓存,这里是将JVM级的驻留缓存和外部依赖的缓存服务相比而言的。RedisMemcached等都提供了性能优越的缓存服务,在高并发场景下作为提高吞吐量、优化服务性能的利器立下了汗马功劳。
进行分布式多级缓存设计的初衷是:利用多数据副本保证数据的可用性,同时通过不同数据源特点提供更高性能、更多场景数据差异化的支持。

场景

一般情况下,缓存我们只使用Redis作为唯一缓存就可以满足大多数业务场景。这里我们不考虑一般的业务场景,现在试图将服务场景复杂化去进行设计,进一步提高对服务性能的追求。
举例一个业务场景,假设应用服务每天需要提供亿级别调用量的查询业务,在最原始阶段,外部业务提供有效入参请求服务接口返回业务数据即可,然而在之后需求迭代中,增加了对调用方权限校验(渠道校验、授权码校验、入参许可校验)和对返回业务数据的保护(涉及脱敏和非授权字段的过滤排除),业务逻辑瞬间丰富和复杂起来。

一般场景

notion image

复杂场景

notion image
以上复杂场景下,需要解决如下几个问题:
  • 数据对比
    • 大量的数据需要进行核验有效性,增加了服务响应的线性时间,可以试图通过哈希表存储避免线性遍历带来的性能问题,通过空间换时间达到O(1)时间复杂度
  • 数据读取
    • 校验数据是相对稳定且数据量较小的,可以将其预加载配置数据到缓存中,减少高频次对数据库层的读取以提高性能
    • 一般而言,Redis作为二级缓存即可满足,由于Redis数据读取也是一层网络传递消耗,为了追求性能极致和服务SLA的更高要求,增加了应用缓存作为一级缓存直接做数据返回
  • 数据存储
    • 配置数据量小,变更低频,读取高频,适合驻留使用一级缓存
    • 业务数据量相对较大,变更不可控,读取高频,适合存储使用二级缓存

技术调研

这里是基于Java语言实现的,其他语言也可以参考匹配对应技术栈来完成技术调研和设计。
对于二级缓存,选择了功能强大的Redis
对于一级缓存,也就是本地缓存有很多选择性。通常,在Java语言中我们会选择HashMap或线程安全的ConcurrentHashMap作为JVM缓存容器来存储数据。这里推荐可以尝试Caffeine,它是一套封装良好天生为本地缓存服务的框架,提供了诸多缓存特性,号称 ”本地缓存天花板“

存储设计

一级缓存 · 服务能力设计

定义本地缓存服务的能力定义,如下

一级缓存 · 存储区域化扩展

由于缓存都是Key-Value形式存储,只能支持Key单维度数据存储,为了提供更为便捷和可扩展的数据存储与读取场景,引入了Region分区使得缓存支持多维度业务。其实这里每个缓存实现内部都持有一个可见性的Map<Region,LocalCache<Object,Object>>,每个Region都是单例的只会被初始化一次,可以简单理解为两个嵌套Map的数据结构,数据的存取都是基于Region分区来进行读取的,一般拆分两个维度可以满足大部分场景,如果复杂的数据结构可以考虑继续对Value进行序列化。
notion image

ConcurrentHashMap本地缓存的实现

Caffeine本地缓存的实现

二级缓存 · 数据存储设计

由于Redis提供了非常高效、便捷的数据结构,数据存储及选取的数据结构如下:
数据名称
数据类型
存储数据结构
业务字段-1
配置数据
Hash
业务字段-2
配置数据
Hash
业务字段-3
配置数据
Hash
业务富信息-1
业务数据
String(JSON序列化)
业务富信息-2
业务数据
String(JSON序列化)
业务富信息-3
业务数据
String(JSON序列化)

流程设计

缓存架构设计

notion image
我们将全视图从上到下拆分为调用方→缓存层→持久层→数据库的核心数据交互主线,此外还有涉及业务数据变更的用户操作、涉及配置或运营数据变更的管理员操作,以及对缓存服务监控的定时服务等。
「缓存层」 是整个缓存架构方案的核心。主要依赖JVM做配置数据的一级缓存存储,依赖Redis做业务数据存储及配置数据的兜底。
由于应用部署是分布式的,JVM的数据一致性依赖Zookeeper进行实现,通过对Path进行监听,数据变更都会触发Path变化从而产生event驱动JVM重新拉去数据以保证JVM缓存数据一致。虽然Zookeeper是一个CP的实现,但是JVM分布式缓存这里采用一种AP实现,由于ZookeeperJVM缓存与DB存储数据唯一通信的信道,一旦出现网络或中间件异常,会出现无法通信无法变更数据的情况,对于这种极端情况,目前采用两种策略进行控制,一是应用启动后会有一个定时轮询的守护线程监控数据情况保证即使在脏数据下服务也部分可用,二是JVM由于监听了ZookeeperPath变更及Session事件,对于失联情况可以选择异常报警或超时失联做服务下线保护,这里分布式通信是一个非常复杂的业务场景,仅提供一个较为可行的实现思路,具体实现可以根据业务场景做更为精细化、高可用保障的实现逻辑。
「数据层」 主要做业务数据变更的缓存移除,确保缓存数据保持一致。这里通过切面环绕业务方法实现缓存移除或更新。

缓存拦截流程

notion image
  • 「Step - 1」 业务请求先请求缓存是否存在业务数据,若存在直接返回
  • 「Step - 2」 若缓存中为empty则说明业务数据为空,这里是为了防止缓存穿透做的空值缓存
  • 「Step - 3」 若缓存值为空,避免缓存击穿会首先设置缓存为empty,而后请求DB,为了避免多个请求同一时刻穿透到DB,需要竞态获取分布式锁,获取锁成功的请求可以顺利抵达数据库进行数据获取,如果查询到数据则立刻更新缓存,无数据则不修改缓存继续保持empty并返回空数据,释放分布式锁
  • 「Step - 4」 当业务方法涉及业务数据的变更时,进行切面环绕,保持第一时间清除缓存,保证缓存与DB数据一致性

缓存加载流程

notion image
  • 「Step - 1」 数据加载首先判断Redis缓存中是否存在数据,若存在直接将Redis作为数据源进行数据获取加载JVM
  • 「Step - 2」Redis数据为空则请求DB进行数据拉取,为了避免同一时刻集群JVM频繁请求和拉取DB数据,这里做了分布式锁控制,同一时刻只发起一次数据拉取操作之后更新Redis,未获取分布式锁JVM进行异步轮询Redis完成最终数据加载

缓存更新流程

notion image
  • Redis缓存更新直接通过业务方法触发进行存储、移除设置即可。
  • JVM缓存的更新主要通过Zookeeper来做分布式协调,当数据库配置数据产生变化,随机触发Zookeeper迭代数据版本,JVM集群订阅Zookeeper数据变更事件触发版本对比后进行数据拉取,进入缓存加载流程保持数据更新

小结

「多数据源分层」 数据以瀑布流形式分层级存在,一级缓存追求强劲内存级读性能支持,二级缓存虽然性能略逊于一级缓存但是借助Redis的强大特性支持能对业务数据进行较好的治理和存储扩展,数据库是持久化的最终归宿充当源数据作用,整体上是一个分而治之的实现思想。
「多数据源管理」 分布式系统最大的特点就是多数据副本,要基于CAP进行技术方案选型做取舍,案例中业务接受短时间数据不一致场景下的AP实现方案。对于多数据源的治理中,协调者的角色非常重要,案例中选用了Zookeeper,未来还可以根据业务情况进行扩展,对比其他竞品ETCDConsul进行改造和替换。
作者:大摩羯先生链接:https://juejin.cn/post/7116764681242411015来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP