《Java面试自救指南》(专题八)中间件(含Redis、Netty、RocketMQ、Dubbo等)

Redis的五大数据结构

Redis有5个基本数据结构:string(动态字符数组)、list(双向链表)、hashsetzset

  1. 简单介绍下常用命令。

(1)数据库操作:

SELECT 3 #选择第几个redis数据库
DBSIZE #查看数据库容量
keys * #查看所有redis键名
flushdb #清空当前数据库
flushall #清空所有数据库
EXISTS keyname #判断是否存在
EXPIRE keyname 10 #设置十秒后过期
type keyname
expire ireader 60 # 1表示设置成功,0表示变量ireader不存在
ttl ireader # 还有50秒的寿命,返回-2表示变量不存在,-1表示没有设置过期时间
del ireader # 删除成功返回1

(2)redis事务:

multi
set a aaa、
exec  //取消DISCARD WATCH可监控key值,若改变,则取消执行EXEC

(3)string必知命令:

set money 100
get money
setrange ireader 28 wooxian #指定位置替换
getrange ireader 28 34
strlen ireader
append ireader .hao
incrby money 20
decrby money 20
incr money
decr money
setex keyname 30 "hello techguide" #带过期时间设置
setnx keyname “hello techguide” #set if not exist
set lock 4854857 ex 3000 nx	#redis分布式锁使用的加锁语句
mset k1 k2 k3 v1 v2 v3
mget k1 k2 k3
getset k1 v4

(4)List必知命令:

lpush list one
rpush list two
lpop list
rpop list
lrange list 0 -1 #取出全部内容
lindex list -1 #下标取值
lrem list 1 one #移除一个one
ltrim list 1 3 #截取并保留的范围
linsert list before one three #one前插入three
llen list

(5)hash

  • 原理:

在实现结构上hash使用二维结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值。

  • 扩容:

当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。所以Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

  • 缩容

Redis的hash结构不但有扩容还有缩容,从这一点出发,它要比Java的HashMap要厉害一些,Java的HashMap只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。

hset ireader go fast
hmset ireader java fast python slow #一次设置多个键值对
hget ireader go
hmget ireader go python
hgetall ireader
hkeys ireader
hvals ireader
hdel ireader java #可删除多个
hexists ireader go 

(6)set和sorted set

HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

sorted set(zset)底层实现hash和跳跃列表。其中每一个元素value有一个权重score,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

sadd set "666"
smembers set #全部元素
sismember set "666" #判断是否有
scard set #元素个数
srem set "666"
spop set #随机弹出

zadd key score member
zadd ireader 4.0 python #通过zadd指令可以增加一到多个value/score对,score放在前面
zcard ireader
zrem ireader go pytho #可删除多个
> zscore ireader python
"5"
> zrangebyscore ireader 0 5
1) "go"
2) "java"
3) "python"
> zrangebyscore ireader -inf +inf withscores
1) "go"
2) "1"
3) "java"
4) "4"
5) "python"
6) "5"

有序集合对象的编码可以是ziplist或者skiplist。同时满足以下条件时使用ziplist编码:

  • 元素数量小于128个
  • 所有member的长度都小于64字节

ziplist编码的有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。

skiplist编码的有序集合底层是一个命名为zset的结构体,而一个zset结构同时包含一个字典和一个跳表。跳表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。

跳表是可以实现二分查找的有序链表。每个元素插入时随机生成它的level,并且每往上一层概率减半,最底层是一条包含所有元素的单向链表,如果一个元素出现在level(x),那么它肯定出现在x以下的level中,每个索引节点包含两个指针,一个向下,一个向右。

跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近。
在这里插入图片描述

链接:
https://juejin.cn/post/6844903644798664712
https://www.jianshu.com/p/9d8296562806

Redis为什么快?

  1. I/O多路复用

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

Reactor模型(事件驱动)事件处理器。在这里插入图片描述以select为例,select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。

select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。

当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。

总结以上就是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

  1. 纯内存操作

  2. 单线程

单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案,避免了CPU不必要的上下文切换和竞争锁的消耗。

  1. 简单高效的数据结构

链接:
https://juejin.cn/post/6978280894704386079
https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348
https://draveness.me/redis-io-multiplexing/

redis雪崩、击穿、穿透

在这里插入图片描述
介绍下布隆过滤器:

布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
在这里插入图片描述
当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。

由于哈希冲突,x和y可能有相同的位置,所以布隆过滤器有可能误判,即查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据,但后者这个特性就足以解决缓存穿透的问题了。

链接:https://xiaolincoding.com/redis/cluster/cache_problem.html

Redis缓存淘汰策略

当内存空间不够时,redis会根据设置的淘汰策略将一部分数据删除,淘汰策略大致可以作用于全范围数据或者仅作用于设置了过期时间的数据。

淘汰策略描述
volatile-ttl表示在设置可过期时间的键值对中,根据过期时间的先后进行淘汰数据,越早被过期的数据,越先被淘汰。
volatile-random在设置了过期时间的键值对中,随机淘汰数据。
volatile-lru会根据lru算法进行数据的淘汰
volatile-lfu会根据lfu算法进行数据的淘汰
allkeys-random在全部的键值对数据中,进行数据的随机淘汰。
allkeys-lru在全部的键值对数据中,根据lru算法进行数据的淘汰。
allkeys-lfu在全部的键值对数据中,根据lfu算法进行数据的淘汰。

redis在实际删除失效主键时又有两种方式:

  1. 消极方法(passive way)

在主键被访问时如果发现它已经失效,那么就删除它。redis在实现GET、MGET、HGET、LRANGE等所有涉及到读取数据的命令时都会调用 expireIfNeeded,它存在的意义就是在读取数据之前先检查一下它有没有失效,如果失效了就删除它。

  1. 积极方法(active way)

周期性地探测,发现失效就删除。消极方法的缺点是,如果key 迟迟不被访问,就会占用很多内存空间,所以才有积极方式。

  1. 主动删除:

当内存超过maxmemory限定时,触发主动清理策略,该策略由启动参数的配置决定

Redis持久化策略

  1. RDB 持久化

RDB 持久化(也称作快照持久化)是指将内存中的数据生成快照保存到磁盘里面,保存的文件后缀是 .rdb。rdb 文件是一个经过压缩的二进制文件,当 Redis 重新启动时,可以读取 rdb 快照文件恢复数据。

RDB 文件是一个单文件的全量数据,很适合数据的容灾备份与恢复,通过 RDB 文件恢复数据库耗时较短,通常 1G 的快照文件载入内存只需 20s 左右。

优点:

  • 是一个压缩过的非常紧凑的文件,保存着某个时间点的数据集,适合做数据的备份、灾难恢复
  • 可以最大化 Redis 的性能,在保存 RDB 文件,服务器进程只需 fork 一个子进程来完成 RDB 文件的创建,父进程不需要做 IO 操作
  • 与 AOF 持久化方式相比,恢复大数据集的时候会更快

缺点:

  • RDB 的数据安全性是不如 AOF 的,保存整个数据集是个重量级的过程,根据配置可能要几分钟才进行一次持久化,如果服务器宕机,那么就可能丢失几分钟的数据
  • Redis 数据集较大时,fork 的子进程要完成快照会比较耗费 CPU 和时间
  1. AOF 持久化

AOF 会把 Redis 服务器每次执行的写命令记录到一个日志文件中,当服务器重启时再次执行 AOF 文件中的命令来恢复数据。

默认情况下 AOF 功能是关闭的,Redis 只会通过 RDB 完成数据持久化的。开启 AOF 功能需要 redis.conf 文件中将 appendonly 配置项修改为 yes

AOF 文件的写入流程可以分为以下 3 个步骤:

(1)命令追加(append):将 Redis 执行的写命令追加到 AOF 的缓冲区 aof_buf
(2)文件写入(write)和文件同步(fsync):AOF 根据对应的策略将 aof_buf 的数据同步到硬盘
(3)文件重写(rewrite):定期对 AOF 进行重写,从而实现对写命令的压缩。 手动调用重写日志bgrewriteaof

优点:

  • 数据更完整,安全性更高,秒级数据丢失(取决于 fsync 策略,如果是 everysec,最多丢失 1 秒的数据)
  • AOF 文件是一个只进行追加的命令文件,且写入操作是以 Redis 协议的格式保存的,内容是可读的,适合误删紧急恢复

缺点:

  • 对于相同的数据集,AOF 文件的体积要远远大于 RDB 文件,数据恢复也会比较慢
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。不过在一般情况下, 每秒 fsync 的性能依然非常高

MySQL和Redis的区别(各自使用的场景以及原因)

  1. 类型上

从类型上来说,mysql是关系型数据库,redis是缓存数据库

  1. 作用上
  • mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢
  • redis用于存储使用较为频繁的数据到缓存中,读取速度快
  1. 需求上

mysql和redis因为需求的不同,一般都是配合使用。

  1. 存放位置
  • MySQL:数据放在磁盘
  • Redis:数据放在内存

5)适合存放数据类型

Redis适合放一些频繁使用,比较热的数据,因为是放在内存中,读写速度都非常快,一般会应用在下面一些场景:排行榜(sorted set)、计数器、消息队列推送、好友关注、粉丝.

关于redis事务的补充:

  • 一次性:一次执行多个redis操作命令,相当于打包的批量执行脚本
  • 顺序性:顺序执行
  • 排他性:在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

但是redis事务不具备原子性,即收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行,并且无隔离级别。

Mongo原理

MongoDB是基于分布式文件存储的数据库,由C++语言编写。MongodDB是一个介于关系数据库与非关系数据库之间的产品,是非关系型数据库中功能最丰富,最像关系数据库。

  • 面向集合(Collection)和文档(document)的存储,以JSON格式的文档保存数据。

  • 高性能,支持Document中嵌入Document减少了数据库系统上的I/O操作以及具有完整的索引支持,支持快速查询

  • 高效的传统存储方式:支持二进制数据及大型对象

  • 高可用性,数据复制集,MongoDB 数据库支持服务器之间的数据复制来提供自动故障转移(automatic failover)

  • 高可扩展性,分片(sharding)将数据分布在多个数据中心,MongoDB支持基于分片键创建数据区域.

  • 丰富的查询功能, 聚合管道(Aggregation Pipeline)、全文搜索(Text Search)以及地理空间查询(Geospatial Queries)

  • 支持多个存储引擎,WiredTiger存储引擎(B+ tree)、In-Memory存储引擎

在这里插入图片描述

链接:
https://javaguide.cn/database/mongodb/mongodb-questions-01.html
底层是b+树:https://zhuanlan.zhihu.com/p/519658576
牛客收藏夹:https://www.nowcoder.com/users/565006049/collects

RocketMQ优缺点

消息队列(mq)的核心思想是将耗时的任务异步化,通过消息队列缓存任务,从而实现消息发送方和接收方的解耦,使得任务的处理能够异步、并行,从而提高系统或集群的吞吐量和可扩展性。在这个过程中,整个系统强依赖于消息队列,起到类似桥梁的作用。消息队列有着经典的三大应用场景:解耦、异步和削峰填谷

在这里插入图片描述

优点:

  • 单机吞吐量:十万级
  • 可用性:非常高,分布式架构
  • 消息可靠性:经过参数优化配置,消息可以做到0丢失
  • 功能支持:MQ功能较为完善,还是分布式的,扩展性好
  • 支持10亿级别的消息堆积,不会因为堆积导致性能下降
  • 源码是Java,方便结合公司自己的业务二次开发
  • 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况
  • RocketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验

缺点:

  • 支持的客户端语言不多,目前是Java及c++,其中c++不成熟
  • 没有在MQ核心中去实现JMS等接口,有些系统要迁移需要修改大量代码

特性:

  • 发布与订阅

发布是指某个生产者向某个topic发送消息。消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息。

  • 消息顺序

一类消息消费时能按照发送的顺序来消费

  • 消息过滤

可以根据Tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现的。

  • 消息可靠性

至少一次
每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息。

  • 回溯消费

Consumer已经消费成功的消息,由于业务上需求需要重新消费。

  • 事务消息

应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。

  • 延迟消息

是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。

  • 消息重试

Consumer消费消息失败后,要触发重试机制令消息再消费一次。

  • 消息重投

生产者在发送消息时,同步消息失败会重投;异步消息有重试;oneway没有任何保证。

RocketMQ架构

在这里插入图片描述
NameServer 负责暴露消息的 topic ,因此可以以将 NameServer 理解成一个注册中心,用来关联 topic 和对应的 broker ,即消息的存储位置。NameServer 的每个节点都维护着 topic 和 broker 的映射关系,每个节点彼此独立,无同步。在每个NameServer节点内部都维护着所有 Broker 的地址列表,所有 Topic 和 Topic 对应 Queue 的信息等。消息生产者在发送消息之前先与任意一台 NameServer 建立连接,获取 Broker 服务器的地址列表,然后根据负载均衡算法从列表中选择一台消息服务器发送消息。

Broker 主要负责消息的存储和转发,分为 master 和 slave,是一写多读的关系。broker 节点可以按照处理的数据相同划分成副本组,同一组 master 和 slave 的关系可以通过指定相同 brokerName,不同的 brokerId 来定义,brokerId 为 0 标识 master,非 0 是 slave。每个 broker 服务器会与 NameServer 集群建立长连接(注意是跟所有的 NameServer 服务器,因为 NameServer 彼此之间独立不同步),并且会注册 topic 信息到 NameServer 中。复制策略是 Broker 的 Master 与 Slave 间的数据同步方式,分为同步复制与异步复制。由于异步复制、异步刷盘可能会丢失少量消息,因此 Broker 默认采用的是同步双写的方式,消息写入 master 成功后,master 会等待 slave 同步数据成功后才向 Producer 返回成功 ACK ,即 Master 与 Slave 都要写入成功后才会返回成功 ACK 。这样可以保证消息发送时消息不丢失。

Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。

Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。

链接:
https://rocketmq.apache.org/docs/
https://segmentfault.com/a/1190000040922513

RocketMQ的消息生产、消费和存储

  1. 消息存储

RocketMQ/Kafka/RabbitMQ 等消息队列会采用顺序写的日志结构,将消息刷盘至文件系统作持久化。顺序写日志文件可以避免频繁的随机访问而导致的性能问题,而且利于延迟写入等优化手段,能够快速保存日志。
在这里插入图片描述

RocketMQ存储结构主要包括 CommitLog 和 Consume queue 两部分。

CommitLog 是物理存储,存储不定长的完整消息记录,逻辑上是完全连续的一个文件,物理上单个文件大小是 1 GB,文件名是当前文件首地址在 CommitLog 中的偏移量。只要 CommitLog 落盘,就可以认为已经接收到消息,即使 Cosume queue 丢失,也可以从 CommitLog 恢复。而所有 topic 的消息都会存储在同一个 CommitLog 中来保证顺序写。这样的结构会导致 CommitLog 读取完全变成随机读,所以需要 Consume queue 作为索引队列 (offset, size, tag),每个 topic-queue 的消息在写完 CommitLog 之后,都会写到独立的 Consume queue ,队列里的每个元素都是定长的元数据,内容包含该消息在对应 CommitLog 的 offset 和 size ,还包括 tagcode 可支持消息按照指定 tag 进行过滤。

顺序写是 MetaQ 实现高性能的基础。

基于这样的存储结构,MetaQ 对客户端暴露的主要是 Consume queue 逻辑视图,提供队列访问接口。消费者通过指定 Consume queue 的位点来读取消息,通过提交 Consume queue 的位点来维护消费进度。Concume queue 每个条目长度固定(8个字节CommitLog物理偏移量、4字节消息长度、8字节tag哈希码),单个 ConsumeQueue 文件默认最多包括 30 万个条目。这样做的好处是队列非常轻量级,Consume Queue 非常小,且在消费过程中都是顺序读取,其速度几乎能与内存读写相比,而在 page cache 和良好的空间局部性作用下,CommitLog 的访问也非常快速。

  1. 消息生产

在这里插入图片描述
发送消息时,Producer 通过负载均衡模块选择相应的 Broker 集群队列进行消息投递。

集群模式下有一点需要注意:消费队列负载机制遵循一个通用的思想,一个消息队列同时只允许被一个消费者消费,一个消费者可以消费多个消费队列。因此当 Consumer 的数量大于队列的数量,会有部分 Consumer 分配不到队列,这些分配不到队列的 Consumer 机器不会有消息到达。

  1. 消息消费

在这里插入图片描述

  • 广播消费:Producer 向一些队列轮流发送消息,队列集合称为 Topic,每一个 Consumer 实例消费这个 Topic 对应的所有队列。
  • 集群消费:多个 Consumer 实例平均消费这个 Topic 对应的队列集合。

一个 Consumer 可以对应多个队列,而一个队列只能给一个 Consumer 进行消费,Consumer 和队列之间是一对多的关系。如果有 10 个队列,11 个 consumer,consumer1~consumer10 各分配一个队列,consumer11 无队列分配。

在消费时间过程中可能会遇到消费队列、消费者增加或减少,此时需要对消费队列进行重新平衡,既重新分配 (rebalance),这就是重平衡机制,默认每隔20s触发一次。

RocketMQ怎么实现事务消息的

MetaQ 只支持同一个 queue 的顺序消息,且同一个 queue 只能被一台机器的一个线程消费,如果想要支持全局消息,那需要将该 topic 的 queue 的数量设置为 1,牺牲了可用性。

在这里插入图片描述

  1. 发送方向 MQ 服务端发送消息
  2. MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
  3. 发送方开始执行本地事务逻辑。
  4. 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
  5. 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后 MQ Server 将对该消息发起消息回查
  6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤 4 对半消息进行操作。

RocketMQ怎么保证高可用(不重不丢)

重复消费问题:

  • 发送时消息重复【消息 Message ID 不同】

MQ Producer 发送消息时,消息已成功发送到服务端并完成持久化,此时网络闪断或者客户端宕机导致服务端应答给客户端失败。如果此时 MQ Producer 意识到消息发送失败并尝试再次发送消息,MQ 消费者后续会收到两条内容相同但是 Message ID 不同的消息。

  • 投递时消息重复【消息 Message ID 相同】

MQ Consumer 消费消息场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,MQ 服务端将在网络恢复后再次尝试投递之前已被处理过的消息,MQ 消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

MetaQ 不能保证消息不重复,因此对于重复消费情况,需要业务自定义唯一标识作为幂等处理的依据。

消息丢失问题:

MetaQ 避免消息丢失的机制主要包括:重试、冗余消息存储。

在生产者的消息投递失败时,默认会重试2次

消费者消费失败时,在广播模式下,消费失败仅会返回 ConsumeConcurrentlyStatus.RECONSUME_LATER ,而不会重试

在未指定顺序消息的集群模式下,消费失败的消息会进入重试队列自动重试,默认最大重试次数为 16

在顺序消费的集群模式下,消费失败会使得当前队列暂停消费,并重试到成功为止

在这里插入图片描述
多个 BrokerName 相同的节点构成一个副本组。每个副本还拥有一个从 0 开始编号,不重复也不一定连续的 BrokerId 用来表示身份,编号为 0 的节点是这个副本组的 Leader / Primary / Master,故障时通过选举来重新对 Broker 编号标识新的身份。例如 BrokerId = {0, 1, 3},则 0 为主,其他两个为备。

RocketMQ消息可靠性(生产/存储/消费)

分布式系统中一个重要的前提假设是所有的网络传输都是不可靠的,在网络传输不可靠的情况下,保证消息的可靠传输,除了进行重试投递别无他法。常用的绝大多数消息队列RocketMQ、RabbitMQ等在消息传输上都只能保证至少传输成功一次,也即(At least once),而不能保证只传输成功一次(Exactly once)。

  • 生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端。
  • 存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上。
  • 消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上,并通过重试来最大限度的保证消息的消费。

发送端消息可靠性

消息发送一般有以下几种方式:同步发送异步发送以及单向(one way)发送,业务具体选择哪种方式进行消息发送,需要根据情况进行判断。

  1. 同步发送

同步发送是指发送端在发送消息时,阻塞线程进行等待,直到服务器返回发送的结果。发送端如果需要保证消息的可靠性,防止消息发送失败,可以采用同步阻塞式的发送,然后同步检查Brocker返回的状态来判断消息是否持久化成功。如果发送超时或者失败,则会默认重试2次,RocketMQ选择至少传输成功一次的消息模型,但是有可能发生重复投递,因为网络传输是不可靠的。

  1. 异步发送

异步发送是指发送端在发送消息时,传入回调接口实现类,调用该发送接口后不会阻塞,发送方法会立即返回,回调任务会在另一个线程中执行,消息发送结果会回传给相应的回调函数。具体的业务实现可以根据发送的结果信息来判断是否需要重试来保证消息的可靠性。

  1. 单向发送

单向发送是指发送端发送完成之后,调用该发送接口后立刻返回,并不返回发送的结果,业务方无法根据发送的状态来判断消息是否发送成功,单向发送相对前两种发送方式来说是一种不可靠的消息发送方式,因此要保证消息发送的可靠性,不推荐采用这种方式来发送消息。

存储端消息可靠性

目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。

RocketMQ消息的存储是由ConsumeQueueCommitLog配合完成 的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,ConsumeQueue是不负责存储消息的,只是负责记录它所属Topic的消息在CommitLog中的偏移量,这样当消费者从Broker拉取消息的时候,就可以快速根据偏移量定位到消息。每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。

在这里插入图片描述
RocketMQ存储模型使用本地磁盘进行存储,数据写入为producer -> direct memory -> pagecache -> 磁盘,数据读取如果pagecache有数据则直接从pagecache读,否则需要先从磁盘加载到pagecache中。

Broker端CommitLog采用顺序写,可以大大提高写入效率,同时采用不同的刷盘模式提供不同的数据可靠性保证,此外采用了ConsumeQueue中间结构来存储偏移量信息,实现消息的分发。由于ConsumeQueue结构固定且大小有限,在实际情况中,大部分的ConsumeQueue 能够被全部读入内存,可以达到内存读取的速度。此外为了保证CommitLog和ConsumeQueue的一致性, CommitLog里存储了Consume Queues 、Message Key、Tag等所有信息,即使ConsumeQueue丢失,也可以通过 commitLog完全恢复出来,这样只要保证commitLog数据的可靠性,就可以保证Consume Queue的可靠性。

  • 同步刷盘

消息写入内存的 PageCache后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。

这种方式可以保证数据绝对安全,但是吞吐量不大。

  • 异步刷盘(默认)

消息写入到内存的 PageCache中,就立刻给客户端返回写操作成功,当 PageCache中的消息积累到一定的量时,触发一次写操作,或者定时等策略将 PageCache中的消息写入到磁盘中。

这种方式吞吐量大,性能高,但是 PageCache中的数据可能丢失,不能保证数据绝对的安全。

Broker正常关闭,Broker 可以正常启动并恢复所有数据。Broker异常Crash同步刷盘可以保证数据不丢失,但异步刷盘可能导致少量数据丢失。机器无法开机(可能是cpu、主板、内存等关键设备损坏)属于单点故障,且无法恢复。解决单点故障可以采用增加Slave节点,主从异步复制仍然可能有极少量数据丢失,同步复制可以完全避免单点问题。

链接:https://developer.aliyun.com/article/781629

消费端消息可靠性

消费消息的确认机制:先消费,消费成功后再提交,可能会造成重复消费,需要由各自consumer业务方保证幂等来解决重复消费问题。

  1. 消费重试

消费者从RocketMQ拉取到消息之后,需要返回消费成功来表示业务方正常消费完成。因此只有返回CONSUME_SUCCESS才算消费完成,如果返回CONSUME_LATER则会按照不同的messageDelayLevel时间进行再次消费,时间分级从秒到小时,最长时间为2个小时后再次进行消费重试,如果消费满16次之后还是未能消费成功,则不再重试,会将消息发送到死信队列,从而保证消息存储的可靠性。

  1. 死信队列

未能成功消费的消息,消息队列并不会立刻将消息丢弃,而是将消息发送到死信队列,其名称是在原队列名称前加%DLQ%,如果消息最终进入了死信队列,则可以通过RocketMQ提供的相关接口从死信队列获取到相应的消息,保证了消息消费的可靠性。

  1. 消息回溯

回溯消费是指Consumer已经消费成功的消息,或者之前消费业务逻辑有问题,现在需要重新消费。要支持此功能,则Broker存储端在向Consumer消费端投递成功消息后,消息仍然需要保留。重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据。RocketMQ Broker提供了一种机制,可以按照时间维度来回退消费进度,这样就可以保证只要发送成功的消息,只要消息没有过期,消息始终是可以消费到的。

RocketMQ事务消息

RocketMQ提供了事务消息的功能,采用2PC(两段式协议)+补偿机制(事务回查)的分布式事务功能,通过消息队列 RocketMQ 版事务消息能达到分布式事务的最终一致。

  • 半事务消息:

暂不能投递的消息,发送方已经成功地将消息发送到了 RocketMQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。

  • 消息回查

由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列 RocketMQ 版服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。

在这里插入图片描述

  1. 生产者将消息发送至Apache RocketMQ服务端。
  2. Apache RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
  3. 生产者开始执行本地事务逻辑。
  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
  • 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。

  • 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。

  1. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查
  2. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  3. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

链接:
https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage/

RocketMQ如何保证消息顺序性

消息的顺序性分为两部分,生产顺序性和消费顺序性。

一、生产顺序性

RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。

如需保证消息生产的顺序性,则必须满足以下条件:

  • 单一生产者:消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。

  • 串行发送:Apache RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。

满足以上条件的生产者,将顺序消息发送至 Apache RocketMQ 后,会保证设置了同一消息组的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:

  • 相同消息组的消息按照先后顺序被存储在同一个队列。

  • 不同消息组的消息可以混合在同一个队列中,且不保证连续。

在这里插入图片描述
二、消费顺序性

Apache RocketMQ 通过消费者和服务端的协议保障消息消费严格按照存储的先后顺序来处理。

如需保证消息消费的顺序性,则必须满足以下条件:

  • 投递顺序

Apache RocketMQ 通过客户端SDK和服务端通信协议保障消息按照服务端存储顺序投递,但业务方消费消息时需要严格按照接收-处理-应答的语义处理消息,避免因异步处理导致消息乱序。

  • 有限重试

RocketMQ 顺序消息投递仅在重试次数限定范围内,即一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。

如果消息需要严格按照先进先出(FIFO)的原则处理,即先发送的先消费、后发送的后消费,则必须要同时满足生产顺序性和消费顺序性。

kafka和rocketMQ对比

  • Kafka 具有更高的吞吐量

Kafka 默认采用异步发送的机制,并且还拥有消息收集和批量发送的机制,这样的设置可以显著提高其吞吐量。因为当采用异步的方式发送消息时,Producer 发送的消息到达 Broker 就会返回成功。此时如果 Producer 宕机,而消息在 Broker 刷盘失败时,就会导致消息丢失,从而降低系统的可靠性。

  • RocketMQ/MetaQ 单机可以支持更多的 topic 数量。

因为 Kafka 在 Broker 端是将一个分区存储在一个文件中的,当 topic 增加时,分区的数量也会增加,就会产生过多的文件。当消息刷盘时,就会出现性能下降的情况。而 RocketMQ/MetaQ 是将所有消息顺序写入文件的,因此不会出现这种情况。

综上所述,Kafka 具有更高的吞吐量,适合应用于日志采集、大数据等领域。而 RocketMQ/MetaQ 单机支持更多的 topic,且具有更高的可靠性(一致性支持),因此适用于淘宝这样复杂的业务处理。

链接:https://mp.weixin.qq.com/s/EEkjBrVYQFwBiGQObrM_TQ

Netty模型

官方定义:
Netty is an asynchronous event-driven network application frameworkfor rapid development of maintainable high performance protocol servers & clients.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Netty的异步模型

在这里插入图片描述

Netty的核心组件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Netty的handler

在这里插入图片描述
handler在Netty里是通过双向链表进行关联的,Netty通过控制InboundHandler节点的调用来决定读事件响应链路;通过控制OutboundHandler节点的调用来决定写事件调用链路。比如读网络事件链路:head->A->B->C->tail,也可以设置B处理数据后不把读事件继续向下传递,此时B可以在自己内部选择不向下一个节点传递读事件。同样,写事件时,每个handler可以选择是否向下一个OutboundHandler类型的节点进行传递,比如业务A是参数校验的的headler,当校验失败就响应客户端,此时调用的链路为A->head。
在这里插入图片描述

Netty零拷贝技术

在这里插入图片描述

数据可以直接从read buffer 读缓存区传输到socket缓冲区,也就是省去了将操作系统的read buffer 拷贝到程序的buffer,以及从程序buffer拷贝到socket buffer的步骤,直接将read buffer拷贝到socket buffer。JDK NIO中的的transferTo() 方法就能够实现这个操作,这个实现依赖于操作系统底层的sendFile()实现。

Netty分层架构

在这里插入图片描述

  1. 网络通信层

Netty服务端:程序启动时会生成一个ServerBootstrap对象,该对象会生成一个NioServerSocketChannel来监听某一个端口的建立网络连接的事件,当网络连接建立后会监听网络连接的各种事件并通知到事件调度层。

Netty客户端:程序启动的时会生成一个Bootstrap对象,该对象会生成一个NioSocketChannel来与服务端建立网络连接的事件,当网络连接建立后会监听网络连接的各种事件并通知到事件调度层。

  1. 事件调度层

事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件(I/O 事件,信号事件,定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。事件调度层主要由EventLoopGroup和EventLoop构成。

EventLoop 负责处理 I/O 事件和调度任务。每一个NioEventLoop内部都有唯一一个Selector,通过这个Selector可以对注册的channel进行网络事件的读取;NioEventLoop 还有一个内部的任务队列,可以用来提交 Runnable 任务。这些任务会在 NioEventLoop 的线程上下文中执行,确保了任务的顺序执行。

EventLoopGroup 本质是一个线程池,负责管理EventLoop。其主要作用有从线程池挑选一个EventLoop进行channe的注册或者提交一个任务、关闭不再使用的 Channel、释放 Selector 和其他相关资源确保应用程序的干净退出。

  1. 服务编排层

服务编排层的职责是通过组装各类handler来实现网络数据流的处理。它是 Netty 的核心处理链,用以实现网络事件的动态编排和有序传播。服务编排层的核心组件包括 ChannelHandlerChannelHandlerContextChannelPipeline
在这里插入图片描述
ChannelPipeline负责将入站(inbound)和出站(outbound)事件分发给链中的各个ChannelHandler,实现了事件驱动的网络编程模型。每个ChannelHandler都有一个唯一的ChannelHandlerContext,用于与ChannelPipeline交互。

链接:
https://cloud.tencent.com/developer/article/1754078
https://blog.youkuaiyun.com/tugangkai/article/details/80560495

Dubbo原理

在这里插入图片描述

config 配置层:对外配置接口,以 ServiceConfig , ReferenceConfig 为中心,可以直接初始化配置类,也可以通过spring 解析配置生成配置类

proxy 服务代理层:服务接口透明代理,生成服务的客户端Stub 和服务器端Skeleton, 以ServiceProxy 为中心,扩展接口为 ProxyFactory

registry 注册中心层:封装服务地址的注册与发现,以服务URL 为中心,扩展接口为RegistryFactory , Registry , RegistryService

cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster , Directory , Router , LoadBalance

monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为MonitorFactory , Monitor , MonitorService

protocol 远程调用层:封装RPC 调用,以 Invocation , Result 为中心,扩展接口为Protocol , Invoker , Exporter

exchange 信息交换层:封装请求响应模式,同步转异步,以 Request , Response 为中心,扩展接口为 Exchanger , ExchangeChannel , ExchangeClient , ExchangeServer

transport 网络传输层:抽象mina 和netty 为统一接口,以 Message 为中心,扩展接口为Channel , Transporter , Client , Server , Codec

serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization , ObjectInput ,ObjectOutput , ThreadPool

在这里插入图片描述

1、服务提供者启动,开启Netty服务,创建Zookeeper客户端,向注册中心注册服务。
2、服务消费者启动,通过Zookeeper向注册中心获取服务提供者列表,与服务提供者通过Netty建立长连接。
3、服务消费者通过接口开始远程调用服务,ProxyFactory通过初始化Proxy对象,Proxy通过创建动态代理对象。
4、动态代理对象通过invoke方法,层层包装生成一个Invoker对象,该对象包含了代理对象。
5、Invoker通过路由,负载均衡选择了一个最合适的服务提供者,在通过加入各种过滤器,协议层包装生成一个新的DubboInvoker对象。
6、再将DubboInvoker对象包装成一个Reuqest对象,该对象通过序列化通过NettyClient传输到服务提供者的NettyServer端。
7、到了服务提供者这边,再通过反序列化、协议解密等操作生成一个DubboExporter对象,再层层传递处理,会生成一个服务提供端的Invoker对象.
8、这个Invoker对象会通过反射调用本地服务,获得结果再通过层层回调返回到服务消费者,服务消费者拿到结果后,再解析获得最终结果。

说下dubbo中的SPI机制

SPI(Service Provider Interface, 服务提供接口)是一种服务发现机制。SPI的本质是将接口的实现类的全限定名定义在配置文件中,由服务器读取配置文件,并加载实现类。这样就可以在运行的时候,动态为接口替换实现类。
在这里插入图片描述
Dubbo通过ExtensionLoader,对定义的接口可以加载指定的实现类。

public class DubboSPITest {
   @Test
   public void sayHello() throws Exception {
       ExtensionLoader<Robot> extensionLoader =
           ExtensionLoader.getExtensionLoader(Robot.class);
       Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
       optimusPrime.sayHello();
       Robot bumblebee = extensionLoader.getExtension("bumblebee");
       bumblebee.sayHello();
   }
}


// ExtensionLoader流程
getExtension(String name)  #根据key获取拓展对象
    -->createExtension(String name) #通过反射创建拓展实例
        -->getExtensionClasses #根据路径获取所有的拓展类
            -->loadExtensionClasses #加载拓展类
                -->cacheDefaultExtensionName #解析@SPI注解
            -->loadDirectory #方法加载指定文件夹配置文件
                -->loadResource #加载资源
                    -->loadClass #加载类,并通过 loadClass 方法对类进行缓存

链接:https://juejin.cn/post/6967265647076048926(dubbo源码分析)

简单了解ZooKeeper

链接:https://blog.youkuaiyun.com/qq_37555071/article/details/114609145

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechGuide

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值