进阶高阶IoT架构-教你如何简单实现一个消息队列
前言
消息队列是软件系统领域用来实现系统间通信最广泛的中间件。基于消息队列的方式是指由应用中的某个系统负责发送消息,由关心这条消息的相关系统负责接收消息,并在收到消息后进行各自系统内的业务处理。消息可以非常简单,比如只包含文本字符串;也可以很复杂,比如包含字节流、字节数组,还可以包含嵌入对象,甚至是Java对象(经过序列化的对象)。
消息在被发送后可以立即返回,由消息队列来负责消息的传递,消息发布者只管将消息发布到消息队列而不用管谁来取,消息使用者只管从消息队列中取消息而不管是谁发布的,这样发布者和使用者都不用知道对方的存在(见下图)。
为何要用消息队列
从上面的描述可以看出,消息队列(MQ)是一种系统间相互协作的通信机制。那么什么时候需要使用消息队列呢?
举个例子。某天产品人员说“系统要增加一个锅炉设备报警功能,当锅炉设备温度大于260度后,用户能收到邮件”。在实际场景中这种需求很常见,开发人员觉得这个很简单,就是提供一个判断逻辑,当锅炉设备温度大于260度进行判断,然后发送邮件,最好返回报警信息以警示。
该功能上线运行了一段时间后,产品人员说“设备高温后收到邮件的响应有点慢,很多人都提出这个意见,能不能优化一下”。开发人员首先想到的优化方案是将锅炉设备温度判断逻辑与发送邮件分开执行,怎么分呢?可以单独开启线程来做发送邮件的事情。
没多久,产品人员又说“现在设备高温并收到邮件的响应是快了,但有用户反映没收到报警邮件,能不能在发送邮件的时候先保存所发送邮件的内容,如果邮件发送失败了则进行补发”。
看着开发人员愁眉苦脸的样子,产品人员说“在邮件发送这块平台部门已经做好方案了,你直接用他们提供的服务就行”。开发人员一听,赶紧和平台部门沟通,对方的答复是“我们提供一个类似于邮局信箱的东西,你直接往这个信箱里写上发送邮件的地址、邮件标题和内容,之后就不用你操心了,我们会直接从信箱里取消息,向你所填写的邮件地址发送响应邮箱”。
这个故事讲的就是使用消息队列的典型场景---异步处理。消息队列还可用于解决解耦、流量削峰、日志收集等问题。
简单实现一个消息队列
回到消息队列这个术语本身,它包含了两个关键词: 消息和队列。消息是指在应用间传送的数据,消息的表现形式是多样的,可以简单到只包含文本字符串,也可以复杂到有一个结构化的对象定义格式。对于队列,从抽象意义上来理解,就是指消息的进和出。从时间顺序上说,进和出并不一定是同步进行的,所以需要一个容器来暂存和处理消息。因此,一个典型意义上的消息队列,至少需要包含消息的发送、接受和暂存功能。
- Broker: 消息处理中心,负责消息的接受、存储、转发等。
- Producer: 消息生产者,负责产生和发送消息和消息处理中心。
- Consumer: 消息消费者,负责从消息处理中心获取消息,并进行相应的处理。
可以看到,消息队列服务的核心是消息处理中心,它至少要具备消息发送、消息接受和消息暂存功能。所以,我们就从消息处理中心开始逐步搭建一个消息队列。
消息处理中心
先看一下消息处理中心类(InMemoryStorage)的实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
/**
* @author james mu
* @date 2020/7/27 21:47
*/
public final class InMemoryStorage {
//保存消息数据的容器,<topic,消息阻塞队列> 键值对
private final ConcurrentHashMap<String, BlockingQueque<QueueMsg>> storage;
private static InMemoryStorage instance;
private InMemoryStorage() {
storage = new ConcurrentHashMap<>();
}
//利用双重检查加锁(double-checked locking),首先检查是否示例已经创建了,如果尚未创建,"才"进行同步。这样以来,只有第一次会同步,这正是我们想要的。
public static InMemoryStorage getInstance() {
if (instance == null) {
synchronized (InMemoryStorage.class) {
if (instance == null) {
instance = new InMemoryStorage();
}
}
}
return instance;
}
//保存消息到主题中,若topic对应的value为空,会将第二个参数的返回值存入并返回
public boolean put(String topic, QueueMsg msg) {
return storage.computeIfAbsent(topic, (t) -> new LinkedBlockingDeque<>()).add(msg);
}
//获得主题中的消息
public <T extends QueueMsg> List<T> get(String topic) {
//判断map中是否包含此topic
if (storage.containsKey(topic)) {
List<T> entities;
//从此主题对应的阻塞队列中出队一个元素
T first = (T) storage.get(topic).poll();
if (first != null) {
entities = new ArrayList<>();
entities.add(first);
List<QueueMsg> otherList = new ArrayList<>();
//移动阻塞队列中最大999个元素到arrayList中
storage.get(topic).drainTo(otherList, 999);
for (QueueMsg other : otherList) {
entities.add((T) other);
}
} else {
entities = Collections.emptyList();
}
}
return Collections.emptyList();
}
//删除此map中所有的键值对
public void cleanup() {
storage.clear();
}
}
作为一个消息处理中心中,至少要有一个数据容器用来保存接受到的消息。
Java中的队列(Queue)是提供该功能的一种简单的数据结构,同时为简化队列操作的并发访问处理,我们选择了它的一个子类LinkedBlockingDeque。该类提供了对数据的插入、获取、查询等操作,其底层将数据以链表的形式保存。如果用 offer方法插入数据时队列没满,则数据插入成功,并立 即返回:如果队列满了,则直接返回 false。 如果用 poll方法删除数据时队列不为空, 则返回队 列头部的数据;如果队列为空,则立刻返回 null。
消息格式定义
队列消息接口定义(QueueMsg)
/**
* @author james mu
* @date 2020/7/27 22:00
*/
public interface QueueMsg {
//消息键
String getKey();
//消息头
QueueMsgHeaders getHeaders();
//消息负载byte数组
byte[] getData();
}
队列消息头接口定义(QueueMsgHeaders)
import java.util.Map;
/**
* @author james mu
* @date 2020/7/27 21:55
*/
public interface QueueMsgHeaders {
//消息头放入
byte[] put(String key, byte[] value);
//消息头通过key获取byte数组
byte[] get(String key);
//消息头数据全部读取方法
Map<String, byte[]> getData();
}
队列消息格式(ProtoQueueMsg)
/**
* @author jamesmsw
* @date 2021/2/19 2:23 下午
*/
public class ProtoQueueMsg implements QueueMsg {
private final String key;
private final String value;
private final QueueMsgHeaders headers;
public ProtoQueueMsg(String key, String value) {
this(key, value, new DefaultQueueMsgHeaders());
}
public ProtoQueueMsg(String key, String value, QueueMsgHeaders headers) {
this.key = key;
this.value = value;
this.headers = headers;
}
@Override
public String getKey() {
return key;
}
@Override
public QueueMsgHeaders getHeaders() {
return headers;
}
@Override
public byte[] getData() {
return value.getBytes();
}
}
默认队列消息头(DefaultQueueMsgHeaders)
import java.util.HashMap;
import java.util.Map;
/**
* @author james mu
* @date 2020/7/27 21:57
*/
public class DefaultQueueMsgHeaders implements QueueMsgHeaders {
protected final Map<String, byte[]> data = new HashMap<>();
@Override
public byte[] put(String key, byte[] value) {
return data.put(key, value);
}
@Override
public byte[] get(String key) {
return data.get(key);
}
@Override
public Map<String, byte[]> getData() {
return data;
}
}
消息生产者
import iot.technology.mqtt.storage.msg.QueueMsg;
import iot.technology.mqtt.storage.queue.QueueCallback;
/**
* @author james mu
* @date 2020/8/31 11:05
*/
public class Producer<T extends QueueMsg> {
private final InMemoryStorage storage = InMemoryStorage.getInstance();
private final String defaultTopic;
public Producer(String defaultTopic) {
this.defaultTopic = defaultTopic;
}
public void send(String topicName, T msg) {
boolean result = storage.put(topicName, msg);
}
}
消息消费者
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author james mu
* @date 2020/8/31 11:23
*/
@Slf4j
public class Consumer<T extends QueueMsg> {
private final InMemoryStorage storage = InMemoryStorage.getInstance();
private volatile Set<String> topics;
private volatile boolean stopped;
private volatile boolean subscribed;
private final String topic;
//虚构函数
public Consumer(String topic) {
this.topic = topic;
stopped = false;
}
public String getTopic() {
return topic;
}
public void subscribe() {
topics = Collections.singleton(topic);
subscribed = true;
}
//批量订阅主题
public void subscribe(Set<String> topics) {
this.topics = topics;
subscribed = true;
}
public void unsubscribe() {
stopped = true;
}
//不断读取topic集合下阻塞队列中的数据集合
public List<T> poll(long durationInMillis) {
if (subscribed) {
List<T> messages = topics
.stream()
.map(storage::get)
.flatMap(List::stream)
.map(msg -> (T) msg).collect(Collectors.toList());
if (messages.size() > 0) {
return messages;
}
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
if (!stopped) {
log.error("Failed to sleep.", e);
}
}
}
return Collections.emptyList();
}
}
至此,一个简单的消息队列中就实现完毕了。
有的同学可能会质疑我上面设计的实战性,不用担心,在下一节中,我将带大家通过阅读高达8k+的Thingsboard的内存型消息队列源码,看下是否和我上面的设计一致。
进阶高阶IoT架构-教你如何简单实现一个消息队列的更多相关文章
- 手把手教你用redis实现一个简单的mq消息队列(java)
众所周知,消息队列是应用系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构.目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Zero ...
- Python 函数进阶-高阶函数
高阶函数 什么是高阶函数 高阶函数就是能够把函数当成参数传递的函数就是高阶函数,换句话说如果一个函数的参数是函数,那么这个函数就是一个高阶函数. 高阶函数可以是你使用def关键字自定义的函数,也有Py ...
- 浅谈JS高阶函数
引入 我们都知道函数是被设计为执行特定任务的代码块,会在某代码调用它时被执行,获得返回值或者实现其他功能.函数有函数名和参数,而函数参数是当调用函数接收的真实的值. 今天要说的高阶函数的英文为High ...
- Kotlin高阶函数实战
前言 1. 高阶函数有多重要? 高阶函数,在 Kotlin 里有着举足轻重的地位.它是 Kotlin 函数式编程的基石,它是各种框架的关键元素,比如:协程,Jetpack Compose,Gradle ...
- python--函数式编程 (高阶函数(map , reduce ,filter,sorted),匿名函数(lambda))
1.1函数式编程 面向过程编程:我们通过把大段代码拆成函数,通过一层一层的函数,可以把复杂的任务分解成简单的任务,这种一步一步的分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计的基本单元. ...
- JavaScript高阶函数 map reduce filter sort
本文是笔者在看廖雪峰老师JavaScript教程时的个人总结 高阶函数 一个函数就接收另一个函数作为参数,这种函数就称之为高阶函数 1.高阶函数之map: ...
- [Effective JavaScript 笔记]第19条:熟练掌握高阶函数
高阶函数介绍 高阶函数曾经是函数式编程的一个概念,感觉是很高深的术语.但开发简洁优雅的函数可以使代码更加简单明了.过去几年中脚本语言采用了这些个技术,揭开了函数式编程的最佳惯用法的神秘面纱.高阶函数就 ...
- python 把函数作为参数 ---高阶函数
把函数作为参数 在2.1小节中,我们讲了高阶函数的概念,并编写了一个简单的高阶函数: def add(x, y, f): return f(x) + f(y) 如果传入abs作为参数f的值: add( ...
- 聊聊React高阶组件(Higher-Order Components)
使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡.一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低 ...
随机推荐
- docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像
docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像 一.docker通过dockerfile构建JDK最小镜像 1.1 下载JRE 1.2 解压JRE,删除相关不需要文件 ...
- 一块网卡配2IP地址
我们知道在Linux下网卡被称为eth0,eth1,eth2.....,所有网卡的配置文件都存储在 /etc/sysconfig/network-script/下,文件名是以ifcfg-eth0,if ...
- 终于有人把Elasticsearch原理讲透了!学习的第一篇总览全局
诗词大会引出的话题 随着央视诗词大会的热播,小史开始对诗词感兴趣,最喜欢的就是飞花令的环节. 但是由于小史很久没有背过诗词了,飞一个字很难说出一句,很多之前很熟悉的诗句也想不起来. 倒排索引 吕老师: ...
- 从问题入手,深入了解JavaScript中原型与原型链
从问题入手,深入了解JavaScript中原型与原型链 前言 开篇之前,我想提出3个问题: 新建一个不添加任何属性的对象为何能调用toString方法? 如何让拥有相同构造函数的不同对象都具备相同的行 ...
- 使用 with as 优化SQL
当我们书写一些结构相对复杂的SQL语句时,可能某个子查询在多个层级多个地方存在重复使用的情况,这个时候我们可以使用 with as 语句将其独立出来,极大提高SQL可读性,简化SQL~ with as ...
- HDU6311 Cover【欧拉路径 | 回路】
HDU6311 Cover 题意: 给出\(N\)个点的简单无向图,不一定联通,现在要用最少的路径去覆盖所有边,并且每条边只被覆盖一次,问最少路径覆盖数和各条路径 \(N\le 10^5\) 题解: ...
- 【noi 2.6_9288】&【hdu 1133】Buy the Ticket(DP / 排列组合 Catalan+高精度除法)
题意:有m个人有一张50元的纸币,n个人有一张100元的纸币.他们要在一个原始存金为0元的售票处买一张50元的票,问一共有几种方案数. 解法:(学习了他人的推导后~) 1.Catalan数的应用7的变 ...
- Codeforces Round #660 (Div. 2) Captain Flint and Treasure 拓扑排序(按照出度、入读两边拓扑排序)
题目链接:Captain Flint and Treasure 题意: 一种操作为 选一个下标 使得ans+=a[i] 且 把a[b[i]]+a[i] 要求每个下标都进行一种这样的操作,问怎么样的 ...
- hdu3247Resource Archiver (AC自动机+最短路+状压dp)
Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 100000/100000 K (Java/Others) Total Submis ...
- 文件的读写(cpp)
文件的读写(cpp) c++中要进行文件的读入,首先要包含一个头文件 fstream . 输出到文件 为打开一个可供输出的文件需要定义一个ofstream 对象并将文件名传入: std::ofstre ...