一、背景

最近公司做的一个需求中有个场景是关于分布式本地缓存刷新的。在跟领导和同事讨论技术方案的时候发现实现起来也不是很难,但是如何大规模使用或者结合不同框架中间件去实现也是有些挑战的,所以本篇文章就分布式本地缓存刷新的点进行深入探讨,对其不同方案的可行性等进行深入剖析。从一个点上来实现举一反三的实战效果。

二、需求

2.1 前提场景

  1. 多节点部署
  2. 本地缓存不容易改造成分布式缓存,或者不方便与分布式缓存配合实现多级缓存
  3. 缓存更新频率不是特别高

2.2 本地缓存刷新需求

  1. 业务系统中的每个节点的操作引发本地缓存变更之后将缓存刷新动作同步给其他节点进行刷新。
  2. 结合每个项目的不同特性给出相匹配的实现方案
  3. 满足节点动态伸缩功能(有难度)
  4. 定义通用缓存api和接口,支持rpc,http实现

三、方案

3.0 场景分析

要实现分布式本地缓存同步刷新功能,这里需要先分析一下大概有哪些组件模块参与其中,首先肯定有一个触发缓存刷新的组件或者模块,另外涉及到本地缓存的管理模块也会有对应的缓存刷新方法,然后需要一个事件模型来代表本地缓存刷新的动作,之后就是需要通过一定的机制来通知其他节点,最后就是其他节点触发本地刷新缓存的方法。整体流程如下:

更新本地jquery 更新本地缓存_java

3.1 消息订阅与发布

经过上面的分析之后,不同的实现方案就是对于如何通知其他节点来进行落地。这里先看一下基于MQ的方式来实现:

  1. 定义一个缓存刷新事件消息模型
  2. 更新本地jquery 更新本地缓存_缓存_02

  3. 消息Topic

refresh_local_cache

  1. 消息group

refresh_local_cache_GID/refresh_local_cache_${ip}
这里按Kafka和RocketMQ的实现方式做一下特殊说明:
Kafka实际上不支持广播消息,所以需要每个节点的消息GroupID是唯一的。
对于RocketMQ来说实现广播消息可以这样做,每个节点配置相同的topic和GroupId,在topic的配置处设置MessageModel.BROADCASTING:广播模式。

3.2 轮询广播

轮询广播的方式也比较简单,也可以实现异步调用,但是对于不同的框架而言可能实现方式也不太一样。
3.2.1 Springboot实现
实现一个SpringBoot Rest接口,通过HttpClientUtils调用,或者封装的调用框架实现循环调用
3.2.2 Dubbo广播调用
声明一个Dubbo接口,配置广播调用
3.2.3 SpringCloud广播
SpringCloud的广播调用可以采用配置变化的方式来走类似于Bus或者Stream的方式通知,这里相当于把缓存刷新事件模型当作一个配置,有变更就覆盖之前的配置,然后走配置发布同步的方式。

3.3 Redis

这里对于Redis的使用有以下几种方式:

  1. 把Redis当作注册中心,每个节点实例把自己的IP注册到Redis中,然后走轮询广播的方式,或者走第二步
  2. 把Redis当作消息中间件,使用Redis双端链表的数据结构,每个IP一个链表,同时每个IP只监听自己的链表内容。
  3. 基于Redis key-value的方式记录缓存刷新的事件模型,每个节点实现Redis key-value变化监听器

3.4 定时任务刷新

这里的定时任务可以使用线程池或者xxl-job,所以这里讨论两种实现方案

任务实现逻辑如下:

更新本地jquery 更新本地缓存_更新本地jquery_03

3.4.1 ScheduledThreadPoolExecutor
3.4.2 Xxl-Job
基于Xxl-Job的方式需要将job的路由策略设置为SHARD,同时速率需要根据实际业务场景设置,避免缓存更新不及时。

四、方案取舍

基于上述方案和思路基本可以实现分布式本地缓存刷新的功能了,但是各自的优缺点也要对比分析下,如下表格,分析了不同方案的优缺点:

方案说明

优点

缺点

基于消息订阅与发布的方式-Kakfa实现

集群内监听与消费解耦,借助消息中间件实现节点动态扩缩容

每个节点需要一个唯一的消费组ID,线上环境可能对于运维不是特别友好,有可能需要手动配置

实时性受限于消息消费速度

基于消息订阅与发布的方式-RocketMQ实现

集群内监听与消费解耦,借助消息中间件实现节点动态扩缩容

需要配置一次groupId,

实时性受限于消息消费速度

接口轮询的方式

不借助中间件,内部实现

实时性比较好

无法满足动态扩缩容,另外缓存变化频率比较快,节点数比较高的话可能会产生性能问题,可用性不高

Redis监听

使用Redis当作消息中间件,进行解耦

不同的实现方案都受限于Redis本身的可用性

定时任务刷新-ScheduledThreadPoolExecutor

使用内置的线程池,相对于xxl-job来说不需要引入特别的任务框架

可能受限于redis本身的可用性,另外周期性的任务实现实时性无法保障

定时任务刷新-Xxx-Job

借助于xxl-job提供的特性结合场景可以实现一定场景下的本地缓存刷新

可能受限于redis本身的可用性,另外周期性的任务实现实时性无法保障

五、总结

通过上面的方案探讨和优缺点分析发现,想做好分布式本地缓存刷新功能借助不同的中间件实现是没有太多难度的,重点是上量之后出现的性能问题和可用性问题(实时性)。这里的量有两个维度:

  1. 节点数量
  2. 一定时间内的刷新量

面对大规模的服务集群如果需要高可用性可能需要上面的多种方案结合才行。比如选消息中间件作为主方案,使用轮询的方式作为兜底方案等。