hello,大家好,我是小黑,又和大家见面啦~~

在配置中心中,有一个经典的 pub/sub 场景:某个配置项发生变更之后,需要实时的同步到各个服务端节点,同时推送给客户端集群。

在之前实现的简易版配置中心中是通过 redis 的 pub/sub 来实现的。这种实现虽然简单,但却强依赖了 redis。

配置中心作为一个基础组件,如果能尽可能的减少外部依赖,那对使用方来说一定是更友好的。那么,有没有可能不使用 MQ 来实现 pub/sub 的场景呢?答案是肯定的。

基于 DB 的 pub/sub 方案

Apollo 在实现上述场景时,并没有选用基于 MQ 来进行实现,而是通过数据库实现了一个简单的消息队列。示意图如下:

大致实现方式如下:

  1. Admin Service 在配置发布后会往 ReleaseMessage 表插入一条消息记录
  2. Config Service 中有一个线程会每秒扫描一次 ReleaseMessage 表,看是否有新的消息记录(怎么判断是不是新消息呢,怎么保证每个 client 不会重复消费呢?)
  3. Config Service 如果发现有新的消息记录,就会通知给客户端(怎么保证通知给每个客户端呢?每个 Config Service 都通知,不会重复通知吗?)

下面,就让我们带着这几个问题来学习一下源码吧。(画外音:思路比源码更重要

DatabaseMessageSender

Admin Service 在配置发布后会调用 DatabaseMessageSender#sendMessage 方法,该方法主要做了两件事情:

  1. 创建 ReleaseMessage ,然后将其保存到数据库中
  2. 记录当前保存的 ReleaseMessage Id,将其放到 DatabaseMessageSender#toClean 队列中。

为什么要记录当前保存的 ReleaseMessage Id 呢?

DatabaseMessageSender 中有个定时任务,会去清除比当前 ID 小的 ReleaseMessage。

ReleaseMessageScanner

Config Service 中通过 ReleaseMessageScanner 组件会每秒(默认配置下)扫描一次 ReleaseMessage 表,来获取最新的消息。

有了这个基于 DB 的 pub/sub,Admin Service 在配置发布之后,每个 Config Service 都会通过 DB 来感知到这个消息,然后再通知给客户端。

那 Config Service 又是如何通知客户端的呢?

基于长轮询的实时消息

在 Apollo 的设计中,配置发生更新之后,并不是服务端主动推给客户端的,而且客户端通过长轮询的方式向服务端询问是否有配置发生了变更。大致思路为:如果在 60 秒内没有该客户端关心的配置发布,那么会返回 Http 状态码 304 给客户端;如果有该客户端关心的配置发布,请求就会立即返回,客户端从返回的结果中获取到配置变化的 namespace 后,会立即请求 Config Service 获取该 namespace 的最新配置

客户端的相关代码在 RemoteConfigLongPollService#doLongPollingRefresh,代码比较简单,感兴趣的同学可以自行查阅。

这里我们重点看一下服务端是如何实现的。

在传统的 servlet 模型中,每个请求都是由某个线程处理的,如果一个请求处理的时间较长,那么这种基于线程池的同步模型很快就会把所有线程耗尽,导致服务器无法响应新的请求。

servlet 3.0 中引入了异步支持,允许对一个请求进行异步处理,工作线程在此期间不会被阻塞,可以继续处理传入的客户端请求。

从 Spring 3.2 开始,可以使用 DeferredResult 来实现异步处理。使用 DeferredResult 时,可以设置超时,超时之后自动返回超时错误响应。同时,可以在另一个线程中,可以调用其 setResult()写入结果返回。

在 Apollo 客户端长轮询的地址为 /notifications/v2,对应的服务端代码为 NotificationControllerV2

NotificationControllerV2 中就使用了 Spring 的 DeferredResult来实现的。本文重在解决问题的思路,就不展示源码了,感兴趣的同学可以自己阅读一下源码。不过,小黑同学写了一个简单的 demo 来帮助我们理解一下 DeferredResult 的使用。

@Slf4j
@RestController
public class DeferredResultDemoController { private final Multimap<String, DeferredResult<String>> deferredResults = ArrayListMultimap.create(); @GetMapping("/info")
public DeferredResult<String> info(String key) {
// 设置 1 秒超时时间,设置超时是返回的结果
DeferredResult<String> result = new DeferredResult<>(1000L, "key not change");
// 将 result 放到 deferredResults 中, key 即为当前请求所关心的配置项
deferredResults.put(key, result);
// 如果超时,移除当前 DeferredResult,并打印日志,同时返回 DeferredResult 构造器中传入的结果
result.onTimeout(() -> {
deferredResults.remove(key, result);
log.info("time out key not change");
});
// 如果完成了,则从 deferredResults 中移除当前 DeferredResult
result.onCompletion(() -> deferredResults.remove(key, result));
return result;
} @PostConstruct
public void init() {
new Thread(() -> {
while (true) {
try {
TimeUnit.MILLISECONDS.sleep(700);
} catch (InterruptedException e) {
log.info(e.getMessage(), e);
}
// 定时任务,模拟配置更新
// 当 hello key 发生变更之后,从 deferredResults 获取到相关的 DeferredResult,通过 setResult 方法设置返回结果,同时移除 deferredResults
if (deferredResults.containsKey("hello")) {
Collection<DeferredResult<String>> results = deferredResults.removeAll("hello");
results.forEach(stringDeferredResult -> stringDeferredResult.setResult("hello key change :" + System.currentTimeMillis()));
}
}
}).start();
}
}

不使用 MQ 如何实现 pub/sub 场景?的更多相关文章

  1. AMQP协议与RabbitMQ、MQ消息队列的应用场景

    什么是AMQP? 在异步通讯中,消息不会立刻到达接收方,而是被存放到一个容器中,当满足一定的条件之后,消息会被容器发送给接收方,这个容器即消息队列,而完成这个功能需要双方和容器以及其中的各个组件遵守统 ...

  2. 高可用服务 AHAS 在消息队列 MQ 削峰填谷场景下的应用

    在消息队列中,当消费者去消费消息的时候,无论是通过 pull 的方式还是 push 的方式,都可能会出现大批量的消息突刺.如果此时要处理所有消息,很可能会导致系统负载过高,影响稳定性.但其实可能后面几 ...

  3. MQ的常见应用场景

    MQ的常见的应用场景为:解耦,异步,流量削峰 在解耦场景中: 不使用MQ的耦合场景: 使用解耦的场景为: 异步的方式: 不使用MQ的同步高延时请求场景: 使用异步化之后的接口性能优化: 没有使用mq的 ...

  4. 详解RPC远程调用和消息队列MQ的区别

    PC(Remote Procedure Call)远程过程调用,主要解决远程通信间的问题,不需要了解底层网络的通信机制. RPC框架 知名度较高的有Thrift(FB的).dubbo(阿里的). RP ...

  5. C#内存映射文件消息队列实战演练(MMF—MQ)

    一.课程介绍 本次分享课程属于<C#高级编程实战技能开发宝典课程系列>中的一部分,阿笨后续会计划将实际项目中的一些比较实用的关于C#高级编程的技巧分享出来给大家进行学习,不断的收集.整理和 ...

  6. MQ初窥门径【面试必看的Kafka和RocketMQ存储区别】

    MQ初窥门径 全称(message queue)消息队列,一个用于接收消息.存储消息并转发消息的中间件 应用场景 用于解决的场景,总之是能接收消息并转发消息 用于异步处理,比如A服务做了什么事情,异步 ...

  7. 《我想进大厂》之MQ夺命连环11问

    继之前的mysql夺命连环之后,我发现我这个标题被好多套用的,什么夺命zookeeper,夺命多线程一大堆,这一次,开始面试题系列MQ专题,消息队列作为日常常见的使用中间件,面试也是必问的点之一,一起 ...

  8. 消息中间件MQ的学习境界和路线

    在<深入理解Java类加载机制,再也不用死记硬背了>里我提到了对于一门语言的"会"的三个层次.本篇将以知识地图的形式展现学习消息中间件MQ各个层次要掌握的内容. 知识地 ...

  9. 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化

    前文目录链接参考: 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16694126.html 消息队列 ...

随机推荐

  1. CodeForces 1409E Two Platforms

    题意 有 \(n\) 个点,分别位于 \((x_i,y_i)\),求最多能用两个长度为 \(k\) 的平台接住多少个点. \(\texttt{Data Range:}n\leq 2\times 10^ ...

  2. 管理Pod(rc,rs,deployment)

    1.概述 可以把容器想像成豆荚里的豆子,把一个或多个关系紧密的豆子包在一起就是豆荚(一个Pod).在k8s中我们不会直接操作容器,而是把容器包装成Pod再进行管理. 2.管理Pod a. 使用Repl ...

  3. WC2019 自闭记

    不咕了 Day 1 2019/1/24 辣么快就到冬令营了,还沉迷于被柿子吊打的状态的菜鸡一时半会还反应不过来.我们学校这次分头去的冬令营,差点上不了车.这次做的动车居然直达广州,强啊. 然鹅还是到太 ...

  4. Jmeter——ForEach Controller&Loop Controller

    今天来分享下Jmeter中的2款循环控制器,ForEach Controller和Loop Controller,在使用上还是有所区别. ForEach Controller ForEach Cont ...

  5. rabbitmq集群搭建,镜像队列搭建

    原文地址:https://www.jianshu.com/p/11963564dd3d 教你如何从0开始搭建rabbitmq集群 一.准备工作 1.三台centos虚拟机 2.三台虚拟机都安装了doc ...

  6. Mybatis 批量更新遇到的小问题

    小问题 记一个开发过程中因为小细节的遗漏而引发的 "莫名其妙",公司中有个2B(to B)供应链项目,持久层用的是 JPA,这里我就不吐槽 JPA 了,这种 SQL 嵌入在代码里的 ...

  7. PriorityQueue原理分析——基于源码

    在业务场景中,处理一个任务队列,可能需要依照某种优先级顺序,这时,Java中的PriorityQueue(优先队列)便可以派上用场.优先队列的原理与堆排序密不可分,可以参考我之前的一篇博客: 堆排序总 ...

  8. 主动关闭 time wait结构体

    /* * This is a TIME_WAIT sock. It works around the memory consumption * problems of sockets in such ...

  9. parted会启动你的ceph osd,意外不?

    前言 如果看到标题,你是不是第一眼觉得写错了,这个怎么可能,完全就是两个不相关的东西,最开始我也是这么想的,直到我发现真的是这样的时候,也是很意外,还是弄清楚下比较好,不然在某个操作下,也许就会出现意 ...

  10. Ceph的Mon数据重新构建工具

    关于mon的数据的问题,一般正常情况下都是配置的3个mon的,但是还是有人会担心 Mon 万一三个同时都挂掉了怎么办,那么集群所有的数据是不是都丢了,关于后台真实数据恢复,有去后台取对象,然后一个个拼 ...