队列有哪些特点
1,先进先出
2,发布订阅
3,持久化
4,分布式高可用 cluster
消息队列的通讯模式
点对点:一对一、一对多、多对多、多对一等
为什么要使用MQ
异步、削峰、解耦
消息队列有什么缺点
系统可用性降低
系统复杂性升高
一致性问题
消息队列如何选型
特性 | activeMq | RabbitMq | kafka | RocketMq |
---|---|---|---|---|
PRODUCER-CONSUMER | 支持 | 支持 | 支持 | 支持 |
PUBLISH_SUBSCRIBE | 支持 | 支持 | 支持 | 支持 |
request-retry | 支持 | 支持 | - | 支持 |
API完备性 | 高 | 高 | 高 | 低 |
多语言支持 | 支持,JAVA优先 | 语言无关 | 支持java优先 | 支持 |
单机吞吐量 | 万级 | 万级 | 十万级 | 单机万级 |
消息延迟 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 |
可用性 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 |
消息丢失 | - | 低 | 理论上不会丢失 | 理论上不会丢失 |
消息重复 | - | 可控制 | 理论上会有重复 | 允许重复 |
文档的完备性 | 高 | 高 | 高 | 中 |
提供快速入门 | 有 | 有 | 有 | 无 |
首次部署难度 | - | 低 | 中 | 高 |
中小公司技术一般,推荐rabbitMQ
大型公司,基础架构研发实力较强,用rocketMQ
实时计算、日志、binlog:kafka
rabbitMq:erlang开发,对消息堆积的支持并不好,当大量消息积压的时候,会导致rabbitMq的性能急剧下降,每秒钟可以处理几万到十几万条消息
rocketMq:java开发,面向互联网集群化功能丰富,对在线业务的响应时延做了很多优化,大多数情况下可以做到好苗子的响应,每秒大概能处理几十万条消息
kafka:scala开发,面向日志功能丰富,性能最高,当每秒钟流量不多的时候,kafka的时延反而较高,所以kafka不太适合在线业务
rabbitMQ消息可靠性传输
生产者
发送方确认:开启消息确认机制publisher-confirm-type:correlated 发送到交换器 返回ack
失败通知 :publisher-returns:true、mandatory=true
broker
发送时设置消息的deliveryMode=2(springboot默认就是)
搭配ack只有持久化磁盘 才返回ack
消费者
消费者手动确认:acknowledge-mode:manual
kafka
特性
高吞吐量、低延迟:每秒可以处理几十万条消息,延迟最低只有几号秒
可扩张性:集群支持热扩展
持久性、可靠性:消息持久化到磁盘,并且支持数据备份防止数据丢失
容错性:允许几群众节点失败(若副本数量n,允许n-1节点失败)
高并发:支持上千个客户端同事读写
角色
provider:生产者 发送消息的
consumer:消费者消费消息的
brocker:节点 你看到的机器
zookeeper:注册中心,记录kafka各种信息的地方
controller:主broker,以leader身份负责整个集群,如果挂掉 zk重新选举
逻辑组件
topic:消费主题
partition:分区 每个主题 可以发送多个分区,吞吐量更大(消费者数量小于等于分区数量)
replicas:副本 每个分区可以设置多个副本,副本之间数据一致 相当于 备份,有备胎 更加可靠
leader & follow:主从
副本:AR=ISR+OSR
AR:所有副本的统称
ISR:同步中的副本 (配置 数量滞后 时间滞后)滞后过了阈值的踢到OSR
OSR:追赶中的副本 (不参与选举)
首先树立一个观点 kafka并不是 你发送了这个消息 他就能消费 首先 它会同步到副本 所谓HW就是副本中的最小的leo
主:leo>=HW>=offset 还会 保存副本的这些信息
offset:偏移量 消费方消费到那一条了 有两种配置
各个分区有已提交的offset,从提交的offset开始消费
earliest:无提交的offset从头开始消费
latest(默认使用):从尾部开始消费 (消费新产生的数据)
HW(high waterMark):客户端最多能消费到的位置(已提交 已备份 )
leo:日志末端位移,表示下一个待写入的消息的offset,这个offset实际上 没有信息,主副本 都有这个值
kafka的集群信息 存在哪里?
zookeeper,还有每个broker也会保存在本地,发生变更 通过controller通知
高版本 可能已经移除zookeeper,由kafka自己管理
kafka发送消息 、消费、位移提交
一般有三种模式:
发送消息的时候指定分区
指定key,kafka会取模 相同的key会落到 一个partition(最常用)
默认 轮询
自定义分区策略(你想怎么玩都行)
指定消费组
同组 集群消费,不同的组 广播
例如topic:test 消费组 group1,group2
两个消费组 那就是广播
如果你写两个方法 消费组都是group1,方法A,方法B都是消费test,并且消费组都是group1
那就是集群消费
位移提交
自动提交
配置:
enable-auto-commit:true ##是否自动提交
auto-commit-interval:100 ## 提交offset的延迟(接收到offset消息之后多久提交offset)
手动提交:打开 一定要提交偏移量 否则重复消费
防止丢失消息 和重复,需要手动控制偏移量的提交
低版本 kafka是怎么丢消息的?高版本kafka又是如何解决的?
有空在写,比较复杂
kafka的客户端KM有空自己玩吧,里面就是把你通过敲命令的方式创建topic、partition、副本等 可视化工具 自己页面上创建;kafka监控eagle监控kafka集群的工具
kafka底层架构
1,分段存储
第一个文件
00000000000000000000.index
00000000000000000000.log
第二个文件
00000000000000369769.index
00000000000000369769.log
第三个文件
00000000000000769769.index
00000000000000769769.log
根据offset去找的时候 方便快速找到
a,kafka直接根据文件名大小,发现他在哪个文件里,例如offset:6 他在000000.log文件里面
b,文件找到了 需要找哪个位置,根据index文件 发现6,9807说明消息藏在这里
c,从log文件的9807位置读取
d,读取多长呢?读到下一条消息的偏移量停止就行
2,日志删除
a,按照时间 超过一段时间删除过期数据
b,按照消息大小,消息数量超过一定的大小删除最旧的数据
每次删除 的最小单位:segment 也就是直接干掉文件,一删 就是一个log和一个index
3,kafka执行消息写入和读取那么快,其中一个原因就是用了零拷贝?
传统:数据 从磁盘 读入 到内核 在读入到用户空间 用户空间数据在回到内核空间 最后再复制到网卡
零拷贝:硬件层面使用DMA技术控制芯片,网卡等外部设备直接去读取内存,不用cpu来回拷贝传输
不用经过用户空间
java中的零拷贝通过:Java.nio.channels.FileChannel的transferTo实现
它底层通过native方法调用操作系统的sendFile
操作系统负责把数据从某个fd传输到另一个fd(fd file dresciption) linux下的所有设备都是一个fd 文件描述符
Mq顺序性消费方案
1,阻塞队列 + 多线程实现
LinkedBlockingQueue[] queues = new LinkedBlockingQueue[4];
@PostConstruct
void execute(){
//遍历队列数组,初始化每一个元素,同时让线程启动
for (int i = 0; i < 4; i++) {
logger.info("thread started , index = {}", i);
final int current = i;
queues[current] = new LinkedBlockingQueue();
new Thread( () -> {
try {
//循环4次,起4个线程一直监听自己的队列,不停获取数据
//取到就执行打印log,取不到阻塞等待
while (true) {
Order order = (Order) queues[current].take();
logger.info("get from queue, queue:{},order:[id={},status={}]",
current,
order.getId(),
order.getStatus());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
@KafkaListener(topics = {"sorted"},topicPattern = "0",groupId = "sorted-group-test-1")
public void onMessage(ConsumerRecord<?, ?> consumerRecord) throws InterruptedException {
Optional<?> optional = Optional.ofNullable(consumerRecord.value());
if (optional.isPresent()) {
Object msg = optional.get();
// 从kafka里获取到消息
// 注意分发方式,kafka有两个分区0和1,对应两个消费者
// 当前分区是0,也就是偶数的id,0、2、4、6会在这里被消费
Order order = JSON.parseObject(String.valueOf(msg),Order.class);
// 而队列是4个。也就是每个消费者再分到两个队列里去
// 队列另一端分别对应4个线程在等待
// 所以,按4取余数
int index = order.getId() % 4;
// logger.info("put to queue,queue={},order:[id={},status={}]",index,order.getId(),order.getStatus());
queues[index].put(order);
}
}
@KafkaListener(topics = {"sorted"},topicPattern = "1",groupId = "sorted-group-test-1")
public void onMessage1(ConsumerRecord<?, ?> consumerRecord) throws InterruptedException {
//相同的实现,现实中为另一台机器,这里用两个listener模拟
//奇数的id会被分到这里,也就是1、3、5、7
onMessage(consumerRecord);
}
`2,使用线程池
//队列数量,根据业务情况决定
//本案例,队列=4,线程=4,一一对应
ExecutorService[] queues = new ExecutorService[4];
@PostConstruct
void execute(){
//遍历队列数组,初始化每一个元素,同时让线程启动
for (int i = 0; i < 4; i++) {
logger.info("thread started , index = {}", i);
final int current = i;
queues[current] = Executors.newSingleThreadExecutor();
}
}
@KafkaListener(topics = {"sorted"},topicPattern = "0",groupId = "sorted-group-test-2")
public void onMessage(ConsumerRecord<?, ?> consumerRecord) throws InterruptedException {
Optional<?> optional = Optional.ofNullable(consumerRecord.value());
if (optional.isPresent()) {
Object msg = optional.get();
// 从kafka里获取到消息
// 注意分发方式,kafka有两个分区0和1,对应两个消费者
// 当前分区是0,也就是偶数的id,0、2、4、6会在这里被消费
Order order = JSON.parseObject(String.valueOf(msg),Order.class);
// 而队列是4个。也就是每个消费者再分到两个队列里去
// 队列另一端分别对应4个线程在等待
// 所以,按4取余数
int index = order.getId() % 4;
// logger.info("put to queue,queue={},order:[id={},status={}]",index,order.getId(),order.getStatus());
queues[index].execute(()->{
logger.info("get from queue, queue:{},order:[id={},status={}]",
index,
order.getId(),
order.getStatus());
});
}
}
@KafkaListener(topics = {"sorted"},topicPattern = "1",groupId = "sorted-group-test-2")
public void onMessage1(ConsumerRecord<?, ?> consumerRecord) throws InterruptedException {
//相同的实现,现实中为另一台机器,这里用两个listener模拟
//奇数的id会被分到这里,也就是1、3、5、7
onMessage(consumerRecord);
}
海量数据 同步场景
使用cannel 伪装成一个Mysql的slave实例 选用消息队列 发送binlog,持久化到其他数据源 es,大数据等