高性能
2024-03-18 12:38:49 0 举报
AI智能生成
高性能
作者其他创作
大纲/内容
CDN
定义
CDN 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 内容分发网络 。
内容:指的是静态资源比如图片、视频、文档、JS、CSS、HTML。
分发网络:指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问。
CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。
全站加速(腾讯云 ECDN、阿里云 DCDN)既可以加速静态资源又可以加速动态资源,内容分发网络(CDN)主要针对的是 静态资源 。
原理
静态资源缓存到 CDN
预热
将源站的资源同步到 CDN 的节点中。
减少了对源站的访问压力,提高了访问速度。
回源
访问的资源可能不在 CDN 节点中,这时 CDN 节点将请求源站获取资源。
此时比未接入 CDN 访问慢。
刷新
如果资源有更新,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点回源站获取最新资源。
指标
命中率 和 回源率 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。
最合适的 CDN 节点
GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。
CDN 会通过 GSLB 找到最合适的 CDN 节点。
寻找过程
浏览器向 DNS 服务器发送域名请求。
DNS 服务器向根据 CNAME( Canonical Name ) 别名记录向 GSLB 发送请求。
GSLB 返回性能最好(通常距离请求地址最近)的 CDN 节点(边缘服务器,真正缓存内容的地方)的地址给浏览器。
浏览器直接访问指定的 CDN 节点。
寻找标准
GSLB 会根据请求的 IP 地址、CDN 节点状态(比如负载情况、性能、响应时间、带宽)等指标来综合判断具体返回哪一个 CDN 节点的地址。
防止资源被盗刷
Referer 防盗链
根据 HTTP 请求的头信息里面的 Referer 字段对请求进行限制。
时间戳防盗链
时间戳防盗链的 URL 通常会有两个参数一个是签名字符串,一个是过期时间。
签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。
签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。
时间戳防盗链的实现也比较简单,并且可靠性较高,推荐使用。并且,绝大部分 CDN 服务提供商都提供了开箱即用的时间戳防盗链机制。
还可以 Referer 防盗链 + 时间戳防盗链 组合使用,同时还可以 IP 黑白名单配置、IP 访问限频配置等机制来防盗刷。
选择
基于成本、稳定性和易用性考虑,建议选择专业的云厂商(如阿里云、腾讯云、华为云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。
负载均衡
定义
将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力以及可靠性。
负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段,不论是单体架构的系统还是微服务架构的系统几乎都会用到。
分类
服务端负载均衡
应用
主要应用在 系统外部请求 和 网关层 之间,可以使用 软件 或者 硬件 实现。
硬件负载均衡
通过专门的硬件设备(比如 F5、A10、Array )实现负载均衡功能。
优缺点
优势是性能很强且稳定,缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万。。。
软件负载均衡
通过软件(比如 LVS、Nginx、HAproxy )实现负载均衡功能。
优缺点
性能虽然差一些,但价格便宜。像基础款的 Linux 服务器也就几千,性能好一点的 2~3 万的就很不错了。
根据 OSI 模型划分
二层负载均衡
非重点
三层负载均衡
非重点
四层负载均衡
工作在 OSI 模型第四层,也就是传输层,这一层的主要协议是 TCP/UDP。
负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。
也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。
负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。
也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。
七层负载均衡
工作在 OSI 模型第七层,也就是应用层,这一层的主要协议是 HTTP 。
这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),
然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。
也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 反向代理服务器 。
这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),
然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。
也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 反向代理服务器 。
总结
四层负载均衡性能很强,七层负载均衡功能更强。但对于绝大部分业务场景来说,两者差异基本可以忽略不计。
实际工作中,通常会使用 Nginx 来做七层负载均衡,LVS(Linux Virtual Server 虚拟服务器, Linux 内核的 4 层负载均衡)来做四层负载均衡。
不过,LVS 这个绝大部分公司真用不上,像阿里、百度、腾讯、eBay 等大厂才会使用到,用的最多的还是 Nginx。
客户端负载均衡
应用
主要应用于系统内部的不同的服务之间,可以使用现成的负载均衡组件来实现。
原理
在客户端负载均衡中,客户端会自己维护一份服务器的地址列表,发送请求之前,客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。
框架
Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。
Dubbo 属于是默认自带了负载均衡功能。
Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项。
比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被启用)。
算法
随机法
最简单粗暴的负载均衡算法。
权重
未加权重
所有的服务器被访问到的概率都是相同的。
适合于服务器性能相近的集群,其中每个服务器承载相同的负载。
加权重
权重越高的服务器被访问的概率就越大。
适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
缺陷
部分机器在一段时间之内无法被随机到,毕竟是概率算法,就算是大家权重一样, 也可能会出现这种情况。
轮询法
挨个轮询服务器处理,也可以设置权重。
权重
未加权重
每个请求按时间顺序逐一分配到不同的服务器处理。
适合于服务器性能相近的集群,其中每个服务器承载相同的负载。
加权重
权重越高的服务器被访问的次数就越多。
适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
改进
在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。
两次随机法
在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。
优点
两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。
哈希法
将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。
在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。
一致性 Hash 法
和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。
常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。
核心
将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。
当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。
当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。
最小连接法
当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。
最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。
然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。
然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。
最少活跃法
最少活跃法和最小连接法类似,但要更科学一些。
以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。
活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。
活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。
最快响应时间法
不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。
客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。
这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。
七层负载均衡实现
DNS 解析
DNS 解析是比较早期的七层负载均衡实现方式,非常简单。
原理
在 DNS 服务器中为同一个主机记录配置多个 IP 地址,这些 IP 地址对应不同的服务器。
当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。
当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。
反向代理
客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。
对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。
反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。
反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
Nginx 就是最常用的反向代理服务器,它可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上。
HTTP 重定向
不常用。
客户端负载均衡实现
客户端负载均衡可以使用现成的负载均衡组件来实现。
Netflix Ribbon
老牌负载均衡组件,由 Netflix 开发,功能比较全面,支持的负载均衡策略也比较多。
策略
RandomRule:随机策略。
RoundRobinRule(默认):轮询策略。
WeightedResponseTimeRule:权重(根据响应时间决定权重)策略。
BestAvailableRule:最小连接数策略。
RetryRule:重试策略(按照轮询策略来获取服务,
如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,
如果超过指定时间依然没获取到服务实例则返回 null)
如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,
如果超过指定时间依然没获取到服务实例则返回 null)
AvailabilityFilteringRule:可用敏感性策略(先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例)。
ZoneAvoidanceRule:区域敏感性策略(根据服务所在区域的性能和服务的可用性来选择服务实例)。
Spring Cloud Load Balancer
Spring 官方为了取代 Ribbon 而推出的,功能相对更简单一些,支持的负载均衡也少一些。
策略
RandomLoadBalancer:随机策略。
RoundRobinLoadBalancer(默认):轮询策略。
ServiceInstanceListSupplier 的实现类同样可以让其支持类似于 Ribbon 的负载均衡策略。
建议
如非必需 Ribbon 某个特有的功能或者负载均衡策略的话,就优先选择 Spring 官方提供的 Spring Cloud Load Balancer。
不推荐 Ribbon 原因
Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。
Netflix 公司开源的 Feign、Ribbon、Zuul、Hystrix、Eureka 等知名的微服务系统构建所必须的组件,Netflix 公司引领了 Java 技术栈下的微服务发展。
在 2018 年,Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。
数据库优化
读写分离
为了将对数据库的读写操作分散到不同的数据库节点上。 这样就能够小幅提升写性能,大幅提升读性能。
步骤
部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制。
系统将写请求交给主数据库处理,读请求交给从数据库处理。
方式
代理方式
可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。
中间件
MySQL Router(官方, MySQL Proxy 的替代方案)、Atlas(基于 MySQL Proxy)、MaxScale、MyCat。
在 MySQL 8.2 的版本中,MySQL Router 能自动分辨对数据库读写/操作并把这些操作路由到正确的实例上。
组件方式
可以通过引入第三方组件来帮助我们读写请求。(推荐)
推荐使用 sharding-jdbc ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。
主从复制原理
根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。
MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。
过程
主库将数据库中数据的变化写入到 binlog。
从库连接主库。
从库会创建一个 I/O 线程向主库请求更新的 binlog。
主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收。
从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
canal
阿里开源工具,可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。
原理
模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。
主从同步延迟
原因
根本
从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据。
从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。
场景
从库机器性能比主库差
解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。
从库处理的读请求过多
解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,
或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。
或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。
大事务
解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。
从库太多
解决方案是减少从库的数量,或者将从库分为不同的层级,让上层的从库再同步给下层的从库,减少主库的压力。
网络延迟
解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。
单线程复制
MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了 多线程复制,MySQL 5.7 还进一步完善了多线程复制。
复制模式
MySQL 默认的复制是异步的,必然会存在延迟问题。
全同步复制不存在延迟问题,但性能太差了。
半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。
MySQL 5.5 开始,MySQL 以插件的形式支持 semi-sync 半同步复制。并且,MySQL 5.7 引入了 增强半同步复制 。
全同步复制不存在延迟问题,但性能太差了。
半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。
MySQL 5.5 开始,MySQL 以插件的形式支持 semi-sync 半同步复制。并且,MySQL 5.7 引入了 增强半同步复制 。
方案
强制将读请求路由到主库处理
可以将那些必须获取最新数据的读请求都交给主库处理。(最常用)
延迟读取
对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。
分库分表
定义
分库
将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。
垂直分库:把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
水平分库:把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
分表
对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
垂直分表:对数据表列的拆分,把一张列比较多的表拆分为多张表。
水平分表:对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。
水平拆分只能解决单表数据量大的问题,为了提升性能,通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。
场景
单表的数据达到千万级别以上,数据库读写速度比较缓慢。
数据库中的数据占用的空间越来越大,备份时间越来越长。
应用的并发量太大。
分片算法
主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
哈希分片:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。比较适合随机读写的场景,不太适合经常需要范围查询的场景。
范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据。
适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
融合算法:灵活组合多种分片算法,比如将哈希分片和范围分片组合。
问题
join 操作
同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。
建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。
解决
可以采用多次查询业务层进行数据组装的方法。但需要考虑业务上多次查询的事务性的容忍度。
事务问题
同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
解决
需要引入分布式事务。
分布式 ID
分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。
解决
需要引入分布式 ID。
跨库聚合查询问题
分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。
引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。
方案
Apache ShardingSphere
一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。
ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。
首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。
现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,也是比较推荐这种方式!
数据迁移
停机迁移
简单高效,推荐。
双写方案
针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。
原理
对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中,从而保证新库里的数据是最新的。
在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。
如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
重复上一步的操作,直到老库和新库的数据一致为止。
在项目中实施双写还是比较麻烦的,很容易出问题。可以借助数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
数据冷热分离
定义
指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。
热数据:指经常被访问和修改且需要快速访问的数据。
冷数据:指不经常访问,对当前项目价值较低,但需要长期保存的数据。
冷热数据区分
时间维度区分:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。
访问评率区分:将高频访问的数据视为热数据,低频访问的数据视为冷数据。
优点
热数据的查询性能得到优化(用户的绝大部分操作体验会更好)。
节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上)。
缺点
系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)。
统计效率低(统计的时候可能需要用到冷库的数据)。
冷数据迁移
业务层代码实现:当有对数据进行写操作时,触发冷热分离的逻辑,判断数据是冷数据还是热数据,冷数据就入冷库,热数据就入热库。
这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。
这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。
任务调度:利用 xxl-job 或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。
这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。
这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。
监听数据库的变更日志 binlog :将满足冷数据条件的数据从 binlog 中提取出来,然后复制到冷库中,并从热库中删除。
这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
冷数据存储
冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。
方案
中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致)。
大厂:Hbase(常用)、RocksDB、Doris、Cassandra。
如果公司成本预算足的话,也可以直接上 TiDB 这种分布式关系型数据库,直接一步到位。
TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。
使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。
TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。
使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。
深度分页
查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。
优化
范围查询
当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案。
这种优化方式限制比较大,且一般项目的 ID 也没办法保证完全连续。
子查询
先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。
不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。
并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
延迟关联
跟子查询的优化思路一样:都是把条件转移到主键索引树,减少回表的次数。不同点是,延迟关联使用了 INNER JOIN(内连接) 包含子查询。
覆盖索引
索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。
优点
避免 InnoDB 表进行索引的二次查询,也就是回表操作。
可以把随机 IO 变成顺序 IO 加快查询效率。
不过,当查询的结果集占表的总行数的很大一部分时,可能就不会走索引了,自动转换为全表扫描。
当然了,也可以通过 FORCE INDEX 来强制查询优化器走索引,但这种提升效果一般不明显。
当然了,也可以通过 FORCE INDEX 来强制查询优化器走索引,但这种提升效果一般不明显。
消息队列
定义
一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。
由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。
参与消息传递的双方称为 生产者 和 消费者 ,生产者负责发送消息,消费者负责处理消息。
中间件
中间件(Middleware),又译中间件、中介层,一类为应用软件服务的软件,应用软件是为用户服务的,用户不会接触或者使用到中间件。
作用
通过异步处理提高系统性能(减少响应所需时间)
削峰/限流
先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
降低系统耦合性
对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。
实现分布式事务
RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。
问题
系统可用性降低: 系统可用性在某种程度上降低。
系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
一致性问题: 消息的真正消费者并没有正确消费消息,这样就会导致数据不一致的情况。
JMS
定义
JMS(JAVA Message Service)Java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。
JMS API 是一个消息服务的标准或者说是规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。
JMS API 使分布式通信耦合度更低,消息服务更加可靠以及异步性。
ActiveMQ(已被淘汰) 就是基于 JMS 规范实现的。
消息格式
StreamMessage:Java 原始值的数据流。
MapMessage:一套名称-值对。
TextMessage:一个字符串对象。
ObjectMessage:一个序列化的 Java 对象。
BytesMessage:一个字节的数据流。
消息模型
点到点(P2P)模型
使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。
发布/订阅(Pub/Sub)模型
使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者。
AMQP
定义
AMQP(Advanced Message Queuing Protocol)高级消息队列协议,应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。
基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。
RabbitMQ 就是基于 AMQP 协议实现的。
JMS 和 AMQP
AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。
在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。
在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。
JMS 支持 TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。
由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。
RPC 和 消息队列
从用途来看:RPC 主要用来解决两个服务的远程通信问题,不需要了解底层网络的通信机制。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。
从通信方式来看:RPC 是双向直接网络通讯,消息队列是单向引入中间载体的网络通讯。
从架构上来看:消息队列需要把消息存储起来,RPC 则没有这个要求,因为前面也说了 RPC 是双向直接网络通讯。
从请求处理的时效性来看:通过 RPC 发出的调用一般会立即被处理,存放在消息队列中的消息并不一定会立即被处理。
RPC 和 消息队列 本质上是网络通讯的两种不同的实现机制,两者的用途不同,万不可将两者混为一谈。
消息模型
队列模型
使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。
缺点
将生产者产生的消息分发给多个消费者,不好解决。
发布-订阅模型
发布订阅模型/主题模型(Pub-Sub) 使用主题(Topic) 作为消息通信载体,类似于广播模式。
消息的生产者称为 发布者(Publisher) ,消息的消费者称为 订阅者(Subscriber) ,存放消息的容器称为 主题(Topic) 。
发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。
发布-订阅模型主要是为了解决队列模型存在的问题。
在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以,发布 - 订阅模型在功能层面上是可以兼容队列模型的。
Kafka、RocketMQ、RabbitMQ 采用的都是发布 - 订阅模型。但底层设计不一样,如 Kafka 的 分区 ,RocketMQ 的 队列 ,RabbitMQ 的 Exchange 。
技术选型
Kafka
定义
Kafka 是 LinkedIn 开源的一个分布式流式处理平台,Apache 顶级项目,早期用于处理海量的日志,后面才慢慢发展成一款功能全面的高性能消息队列。
Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信的服务器和客户端组成,可以部署在在本地和云环境中的裸机硬件、虚拟机和容器上。
在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用。
在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。
在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。
如果要使用 KRaft 模式,建议选择较高版本的 Kafka(Kafka 3.3.1 及以上),低版本此功能还不完善。
关键功能
消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。
容错的持久方式存储记录消息流:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。
流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。
核心概念
Producer(生产者) : 产生消息的一方。
Consumer(消费者) : 消费消息的一方。
Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。
Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。
Partition(分区) : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,
并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。
并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。
Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。
多副本机制
Kafka 为分区(Partition)引入了多副本(Replica)机制。
分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。
发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。
发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
生产者和消费者只与 leader 副本交互。可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。
当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。
当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。
优点
Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力(负载均衡)。
Partition 可以指定对应的 Replica 数,这也极大地提高了消息存储的安全性,提高了容灾能力,不过也相应的增加了所需要的存储空间。
Zookeeper 和 Kafka
ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
Broker 注册:在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,
即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去。
即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去。
Topic 注册:同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。
比如创建一个名为 my-topic 的主题并且它有两个分区,zookeeper 创建文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1。
比如创建一个名为 my-topic 的主题并且它有两个分区,zookeeper 创建文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1。
负载均衡:Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力。
对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。
当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。
当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。
当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。
当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
消息模型
发布 - 订阅模型。
底层:分区。
消息安全
消费顺序
原理
每次添加消息到 Partition(分区) 的时候都会采用尾加法。 Kafka 只能为我们保证 Partition(分区) 中的消息有序。
消息在被追加到 Partition(分区) 的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
方法
1 个 Topic 只对应一个 Partition。
(推荐)发送消息的时候指定 key/Partition。
消息不丢失
生产者丢失消息
生产者(Producer) 调用 send 方法(异步)发送消息之后,消息可能因为网络问题并没有发送过去。
方法
异步调用 send 后,通过 get() 方法获取调用结果,但会变为同步操作,不推荐。
异步调用 send 后,添加回调函数形式获取结果。若失败,检查原因后重新发送即可。推荐设定一个 retries(重试次数)。
消费者丢失消息
当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。
方法
手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。
消息理论上会被消费两次。
Kafka 弄丢了消息
假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,
但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。
但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。
方法
设置 acks = all。
acks 是 Kafka 生产者(Producer) 很重要的一个参数,默认为 1。
当 acks = 1 时,代表我们的消息被 leader 副本接收之后就算被成功发送。
当 acks = all 时,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应。最高级别且最安全的,但延迟会很高。
acks 是 Kafka 生产者(Producer) 很重要的一个参数,默认为 1。
当 acks = 1 时,代表我们的消息被 leader 副本接收之后就算被成功发送。
当 acks = all 时,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应。最高级别且最安全的,但延迟会很高。
设置 replication.factor >= 3。
为了保证 leader 副本能有 follower 副本能同步消息,一般会为 topic 设置 replication.factor >= 3。
这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
为了保证 leader 副本能有 follower 副本能同步消息,一般会为 topic 设置 replication.factor >= 3。
这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
设置 min.insync.replicas > 1。
一般情况下我们还需要设置 min.insync.replicas > 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。
min.insync.replicas 默认值为 1 ,在实际生产中应尽量避免默认值 1。
为保证整个 Kafka 服务的高可用性,需确保 replication.factor > min.insync.replicas 。一般推荐设置成 replication.factor = min.insync.replicas + 1。
一般情况下我们还需要设置 min.insync.replicas > 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。
min.insync.replicas 默认值为 1 ,在实际生产中应尽量避免默认值 1。
为保证整个 Kafka 服务的高可用性,需确保 replication.factor > min.insync.replicas 。一般推荐设置成 replication.factor = min.insync.replicas + 1。
设置 unclean.leader.election.enable = false。
Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false
当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false
当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
消息不重复消费
原因
服务端侧已经消费的数据没有成功提交 offset(根本原因)。
Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。
方法
消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。
将 enable.auto.commit 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。
手动提交 offset 时机
处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样。
拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。
重试机制
消费失败
在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。
重试规则
默认配置下会进行最多 10 次 的重试,每次重试的时间间隔为 0,即立即进行重试。
如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。
如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。
自定义重试规则
只需要在 DefaultErrorHandler 初始化的时候传入自定义的 FixedBackOff 即可。
重新实现一个 KafkaListenerContainerFactory ,调用 setCommonErrorHandler 设置新的自定义的错误处理器就可以实现。
重新实现一个 KafkaListenerContainerFactory ,调用 setCommonErrorHandler 设置新的自定义的错误处理器就可以实现。
重试失败后告警
自定义重试失败后逻辑,需要手动实现,重写 DefaultErrorHandler 的 handleRemaining 函数,加上自定义的告警等操作。
DefaultErrorHandler 只是默认的一个错误处理器,Spring Kafka 还提供了 CommonErrorHandler 接口。
手动实现 CommonErrorHandler 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
手动实现 CommonErrorHandler 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
重试失败后再处理
死信队列(Dead Letter Queue,简称 DLQ) 是消息中间件中的一种特殊队列。
它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。
它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。
@RetryableTopic 是 Spring Kafka 中的一个注解,它用于配置某个 Topic 支持消息重试,更推荐使用这个注解来完成重试。
当达到最大重试次数后,如果仍然无法成功处理消息,消息会被发送到对应的死信队列中。
对于死信队列的处理,既可以用 @DltHandler 处理,也可以使用 @KafkaListener 重新消费。
对于死信队列的处理,既可以用 @DltHandler 处理,也可以使用 @KafkaListener 重新消费。
应用场景
消息队列:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
数据处理: 构建实时的流数据处理程序来转换或处理数据流。
优势
极致的性能:基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
生态系统兼容性无可匹敌:Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。
RocketMQ
定义
RocketMQ 是一个 队列模型 的消息中间件,具有高性能、高可靠、高实时、分布式的特点。采用 Java 语言开发的分布式的消息系统。
阿里开源的一款云原生“消息、事件、流”实时数据处理平台,借鉴了 Kafka,已经成为 Apache 顶级项目。
核心特性
云原生:生与云,长与云,无限弹性扩缩,K8s 友好。
高吞吐:万亿级吞吐保证,同时满足微服务与大数据场景。
流处理:提供轻量、高扩展、高性能和丰富功能的流计算引擎。
金融级:金融级的稳定性,广泛用于交易核心链路。
架构极简:零外部依赖,Shared-nothing 架构。
生态友好:无缝对接微服务、实时计算、数据湖等周边生态。
消息模型
发布 - 订阅模型。
底层:队列。
RocketMQ 通过使用在一个 Topic 中配置多个队列并且每个队列维护每个消费者组的消费位置 实现了 主题模式/发布订阅模式 。
角色
Producer Group 生产者组:代表某一类的生产者,比如有多个秒杀系统作为生产者,合在一起就是一个 Producer Group 生产者组,一般生产相同的消息。
Consumer Group 消费者组:代表某一类的消费者,比如有多个短信系统作为消费者,合在一起就是一个 Consumer Group 消费者组,一般消费相同的消息。
Topic 主题:代表一类消息,比如订单消息,物流消息等等。
角色
Broker:主要负责消息的存储、投递和查询以及服务高可用保证。就是消息队列服务器,生产者生产消息到 Broker ,消费者从 Broker 拉取消息并消费。
一个 Topic 分布在多个 Broker上,一个 Broker 可以配置多个 Topic ,它们是多对多的关系。
一个 Topic 分布在多个 Broker上,一个 Broker 可以配置多个 Topic ,它们是多对多的关系。
NameServer:注册中心 ,主要提供 Broker 管理 和 路由信息管理。就是 Broker 注册后,消费者和生产者从中获取信息来与 Broker 通信。
Producer:消息发布的角色,支持分布式集群方式部署。就是生产者。
Consumer:消息消费的角色,支持分布式集群方式部署。
支持以 push 推,pull 拉 两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
支持以 push 推,pull 拉 两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
消息类型
普通消息
一般应用于微服务解耦、事件驱动、数据集成等场景,这些大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。
生命周期
初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。
消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。
消息删除:RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
定时消息
可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。
优势
精度高、开发门槛低:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发,无需业务去重。
高性能可扩展:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。RocketMQ 的定时消息具有高并发和水平扩展的能力。
生命周期
初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息单独存储在定时存储系统中,等待定时时刻到达。
待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。
消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。
消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。
消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
顺序消息
仅支持使用 MessageType 为 FIFO 的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。
和普通消息发送相比,顺序消息发送必须要设置消息组。(推荐实现 MessageQueueSelector 的方式)。要保证消息的顺序性需要单一生产者串行发送。
单线程使用 MessageListenerConcurrently 可以顺序消费,多线程环境下使用 MessageListenerOrderly 才能顺序消费。
事务消息
高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
发送消息
不建议单一进程创建大量生产者
Apache RocketMQ 的生产者和主题是多对多的关系,支持同一个生产者向多个主题发送消息。
对于生产者的创建和初始化,建议遵循够用即可、最大化复用原则,如果有需要发送消息到多个主题的场景,无需为每个主题都创建一个生产者。
不建议频繁创建和销毁生产者
Apache RocketMQ 的生产者是可以重复利用的底层资源,类似数据库的连接池。
不需要在每次发送消息时动态创建生产者,且在发送结束后销毁生产者。这样频繁的创建销毁会在服务端产生大量短连接请求,严重影响系统性能。
消费者分类
PushConsumer
高度封装的消费者类型,消费消息仅仅通过消费监听器监听并返回结果。消息的获取、消费状态提交以及消费重试都通过 RocketMQ 的客户端 SDK 完成。
监听执行结果
返回消费成功:以 Java SDK 为例,返回ConsumeResult.SUCCESS,表示该消息处理成功,服务端按照消费结果更新消费进度。
返回消费失败:以 Java SDK 为例,返回ConsumeResult.FAILURE,表示该消息处理失败,需要根据消费重试逻辑判断是否进行重试消费。
出现非预期失败:例如抛异常等行为,该结果按照消费失败处理,需要根据消费重试逻辑判断是否进行重试消费。
错误方式
消息还未处理完成,就提前返回消费成功结果。此时如果消息消费失败,RocketMQ 服务端是无法感知的,因此不会进行消费重试。
在消费监听器内将消息再次分发到自定义的其他线程,消费监听器提前返回消费结果。
此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。
此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。
PushConsumer 严格限制了消息同步处理及每条消息的处理超时时间,适用于以下场景:
消息处理时间可预估:如果不确定消息处理耗时,经常有预期之外的耗时的消息,PushConsumer 的可靠性保证会频繁触发消息重试机制造成大量重复消息。
无异步化、高级定制场景:PushConsumer 限制了消费逻辑的线程模型,由客户端 SDK 内部按最大吞吐量触发消息处理。该模型开发逻辑简单,但是不允许使用异步化和自定义处理流程。
消息处理时间可预估:如果不确定消息处理耗时,经常有预期之外的耗时的消息,PushConsumer 的可靠性保证会频繁触发消息重试机制造成大量重复消息。
无异步化、高级定制场景:PushConsumer 限制了消费逻辑的线程模型,由客户端 SDK 内部按最大吞吐量触发消息处理。该模型开发逻辑简单,但是不允许使用异步化和自定义处理流程。
SimpleConsumer
一种接口原子型的消费者类型,消息的获取、消费状态提交以及消费重试都是通过消费者业务逻辑主动发起调用完成。
应用
消息处理时长不可控:如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。
建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。
建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。
需要异步化、批量消费等高级定制场景:SimpleConsumer 在 SDK 内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景。
需要自定义消费速率:SimpleConsumer 是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率。
PullConsumer
主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。
一旦获取了批量消息,应用就会启动消费过程。Pull指的是客户端主动向服务端请求,拉取数据。
一旦获取了批量消息,应用就会启动消费过程。Pull指的是客户端主动向服务端请求,拉取数据。
分组
生产者分组
RocketMQ 服务端 5.x 版本开始,生产者是匿名的,无需管理生产者分组(ProducerGroup);
对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。
对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。
消费者分组
多个消费行为一致的消费者的负载均衡分组。消费者分组不是具体实体而是一个逻辑资源。通过消费者分组实现消费性能的水平扩展以及高可用容灾。
策略
消费者分组中的订阅关系、投递顺序性、消费重试策略是一致的。
订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。
投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。
消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。
RocketMQ 服务端 5.x 版本:消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。
RocketMQ 服务端 3.x/4.x 历史版本:消费逻辑由消费者客户端接口定义,因此,需要在消费者客户端设置时保证同一分组下的消费者的消费行为一致。
RocketMQ 服务端 3.x/4.x 历史版本:消费逻辑由消费者客户端接口定义,因此,需要在消费者客户端设置时保证同一分组下的消费者的消费行为一致。
消费安全
顺序消费
RocketMQ 在主题上是无序的、它只有在队列层面才是保证有序的。
普通顺序
消费者通过同一个消费队列收到的消息是有顺序的 ,不同消息队列收到的消息则可能是无顺序的。
普通顺序消息在 Broker 重启情况下不会保证消息顺序性 (短暂时间) 。
严格顺序
消费者收到的 所有消息 均是有顺序的。严格顺序消息 即使在异常情况下也会保证消息的顺序性 。
如果你使用严格顺序模式,Broker 集群中只要有一台机器不可用,则整个集群都不可用。
MQ 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。
可以使用 Hash 取模法 来保证将同一语义下的消息放入同一个队列。
队列选择算法
轮询算法
轮询算法就是向消息指定的 topic 所在队列中依次发送消息,保证消息均匀分布
RocketMQ 默认队列选择算法。
最小投递延迟算法
每次消息投递的时候统计消息投递的延迟,选择队列时优先选择消息延时小的队列,导致消息分布不均匀。
解决消息分布不均匀
按照 producer.setSendLatencyFaultEnable(true); 设置即可。
特殊情况处理
发送异常
选择队列后会与 Broker 建立连接,通过网络请求将消息发送到 Broker 上,如果 Broker 挂了或者网络波动发送消息超时此时 RocketMQ 会进行重试。
重新选择其他 Broker 中的消息队列进行发送,默认重试两次,可以手动设置。
producer.setRetryTimesWhenSendFailed(5);
消息过大
消息超过 4k 时 RocketMQ 会将消息压缩后在发送到 Broker 上,减少网络资源的占用。
重复消费
幂等
其任意多次执行所产生的影响均与一次执行的影响相同。
实现
这个还是需要结合具体的业务的。
可以使用 写入 Redis 来保证,因为 Redis 的 key 和 value 就是天然支持幂等的。
使用 数据库插入法 ,基于数据库的唯一键来保证重复数据不会被插入多条。
分布式事务
在 RocketMQ 中使用的是 事务消息加上事务反查机制 来解决分布式事务问题的。
消息队列中的分布式事务是本地事务和存储消息到消息队列才是同一个事务。
这样也就产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务就行了。
这样也就产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务就行了。
消息堆积
当流量到峰值的时候是因为生产者生产太快,可以使用一些 限流降级 的方法。
增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增,不过 同时你还需要增加每个主题的队列数量 。(最快速)
如果消费者消费过慢的话,可以先检查消费者否是出现了大量的消费错误 ,或打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
回溯消费
Consumer 已经消费成功的消息,由于业务上需求需要重新消费,在RocketMQ 中,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留 。
高性能读写
传统 IO 方式
传统的 IO 读写其实就是 read + write 的操作。
过程
用户调用 read()方法,开始读取数据,此时发生一次上下文从用户态到内核态的切换。
将磁盘数据通过 DMA 拷贝到内核缓存区。
将内核缓存区的数据拷贝到用户缓冲区,这样用户,也就是我们写的代码就能拿到文件的数据。
read()方法返回,此时就会从内核态切换到用户态。
当我们拿到数据之后,就可以调用 write()方法,此时上下文会从用户态切换到内核态。
CPU 将用户缓冲区的数据拷贝到 Socket 缓冲区。
将 Socket 缓冲区数据拷贝至网卡。
write()方法返回,上下文重新从内核态切换到用户态。
整个过程发生了 4 次上下文切换和 4 次数据的拷贝,这在高并发场景下肯定会严重影响读写性能,故引入了零拷贝技术。
零拷贝技术
mmap
一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
简单地说就是内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。
基于 mmap IO 读写其实就变成 mmap + write 的操作,也就是用 mmap 替代传统 IO 中的 read 操作。
过程
当用户发起 mmap 调用的时候会发生上下文切换 1,进行内存映射,然后数据被拷贝到内核缓冲区,mmap 返回,发生上下文切换 2;
随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。
随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。
发生 4 次上下文切换和 3 次 IO 拷贝操作。
sendfile
sendfile()跟 mmap()一样,也会减少一次 CPU 拷贝,但是它同时也会减少两次上下文切换。
过程
用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,
之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。
之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。
sendfile 并没有文件的读写操作,而是直接将文件的数据传输到 target 目标缓冲区,也就是说,sendfile 是无法知道文件的具体的数据的;
但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。
但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。
基于零拷贝技术,可以减少 CPU 的拷贝次数和上下文切换次数,从而可以实现文件高效的读写操作。
RocketMQ 内部主要是使用基于 mmap 实现的零拷贝,用来读写文件,这也是 RocketMQ 为什么快的一个很重要原因。
刷盘机制
刷盘
同步刷盘
在同步刷盘中需要等待一个刷盘成功的 ACK ,对 MQ 消息可靠性来说是一种不错的保障,但性能上会有较大影响 ,一般地适用于金融等特定业务场景。
异步刷盘
异步刷盘是开启一个线程去异步地执行刷盘操作。
消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了 MQ 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了 MQ 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
异步刷盘只有在 Broker 意外宕机的时候会丢失部分数据,可以设置 Broker 的参数 FlushDiskType 来调整刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
复制
同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 Borker 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。
同步复制
也叫 “同步双写”,也就是说,只有消息同步双写到主从节点上时才返回写入成功 。
异步复制
消息写入主节点之后就直接返回写入成功 。
问题
复制方式无法保证 严格顺序。
解决:采用 Dledger,要求写入消息时,要求至少消息复制到半数以上的节点之后,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。
解决:采用 Dledger,要求写入消息时,要求至少消息复制到半数以上的节点之后,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。
对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了可用性 。
主要原因是 RocketMQ 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了。
主要原因是 RocketMQ 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了。
存储机制
存储架构角色
CommitLog
消息主体以及元数据的存储主体,存储 Producer 端写入的消息主体内容,消息内容不是定长的。
单个文件大小默认 1G ,文件名长度为 20 位,左边补零,剩余为起始偏移量。
消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
ConsumeQueue
消息消费队列,引入的目的主要是提高消息消费的性能。
RocketMQ 是基于主题 Topic 的订阅模式,消息消费是针对主题进行的,如果要遍历 commitlog 文件中根据 Topic 检索消息是非常低效的。
Consumer 即可根据 ConsumeQueue 来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,
保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset ,消息大小 size 和消息 Tag 的 HashCode 值。
保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset ,消息大小 size 和消息 Tag 的 HashCode 值。
consumequeue 文件可以看成是基于 topic 的 commitlog 索引文件,
故 consumequeue 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
故 consumequeue 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
IndexFile
IndexFile(索引文件)提供了一种可以通过 key 或时间区间来查询消息的方法。
整个消息存储的结构,最主要的就是 CommitLoq 和 ConsumeQueue 。而 ConsumeQueue 你可以大概理解为 Topic 中的队列。
RocketMQ 采用的是 混合型的存储结构 ,即为 Broker 单个实例下所有的队列共用一个日志数据文件来存储消息。
在同样高并发的 Kafka 中会为每个 Topic 分配一个存储文件。
在同样高并发的 Kafka 中会为每个 Topic 分配一个存储文件。
应用
大规模消息流处理:RocketMQ 能够处理大量的消息流,并提供高吞吐量和低延迟的消息传递能力。
因此,它适用于需要处理大规模消息流的场景,如实时日志处理、实时数据分析和监控系统等。
因此,它适用于需要处理大规模消息流的场景,如实时日志处理、实时数据分析和监控系统等。
异步通信:RocketMQ 的发布-订阅模式和队列模式可以实现异步通信,将消息发送方和接收方解耦,提高系统的可伸缩性和弹性。
它适用于需要异步通信的场景,如异步任务处理、解耦系统组件和微服务架构等。
它适用于需要异步通信的场景,如异步任务处理、解耦系统组件和微服务架构等。
高可靠性和顺序性要求:RocketMQ 提供了可靠性投递和消息顺序保证的特性,
适用于对消息的可靠性和顺序性要求较高的场景,如金融交易系统、订单处理和支付系统等。
适用于对消息的可靠性和顺序性要求较高的场景,如金融交易系统、订单处理和支付系统等。
数据集成和异构系统集成:RocketMQ 可以作为数据集成和异构系统集成的中间件,将不同系统之间的数据进行传递和转换。
它适用于需要数据集成和系统间通信的场景,如数据同步、消息驱动的架构和异构系统集成等。
它适用于需要数据集成和系统间通信的场景,如数据同步、消息驱动的架构和异构系统集成等。
RabbitMQ
定义
采用 Erlang 语言实现 AMQP 的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。
在易用性、扩展性、可靠性和高可用性等方面有着卓著表现。
特点
可靠性: RabbitMQ 使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。
灵活的路由: 在消息进入队列之前,通过交换器来路由消息。
对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。
针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。
针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
扩展性: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
高可用性: 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
支持多种协议: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。
多语言客户端: RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。
易用的管理界面: RabbitMQ 提供了一个易用的用户界面,使用户可以监控和管理消息、集群中的节点等。
插件机制: RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制。
消息模型
发布 - 订阅模型。
底层:Exchange。
核心概念
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。
Producer(生产者)
生产消息的一方(邮件投递者)。
消息
消息头(标签 Label)
由可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
消息体
消息体也可以称为 payLoad,消息体是不透明的。
生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。
Consumer(消费者)
消费消息的一方(邮件收件人)。消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
Exchange
生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。
在 RabbitMQ 中,消息并不是直接被投递到 Queue(消息队列) 中的,
中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中。
中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中。
Exchange(交换器) 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,
如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
生产者将消息发给交换器的时候,一般会指定一个 RoutingKey(路由键),
用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
RabbitMQ 中通过 Binding(绑定) 将 Exchange(交换器) 与 Queue(消息队列) 关联起来,在绑定的时候一般会指定一个 BindingKey(绑定建),
这样 RabbitMQ 就知道如何正确将消息路由到队列了。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,
所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
这样 RabbitMQ 就知道如何正确将消息路由到队列了。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,
所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。
在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。
BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。
BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
类型
fanout
fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,
不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
direct
direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。
如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。
如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。
topic
topic 类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,
也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同。
也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同。
约定
RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),
如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”。
如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”。
BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串。
BindingKey 中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
示例
路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2。
路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中。
路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中。
路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中。
路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。
headers(不推荐)
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),
对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),
对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
Queue
RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
Queue(消息队列) 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。
一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
RabbitMQ 中消息只能存储在 队列 中,这一点和 Kafka 这种消息中间件相反。
Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。
RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。
RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,
而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
RabbitMQ 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
Broker
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。
大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
可以看做 RabbitMQ 的服务节点。一般情况下一个 Broker 可以看做一个 RabbitMQ 服务器。
AMQP
定义
RabbitMQ 就是 AMQP 协议的 Erlang 的实现(当然 RabbitMQ 还支持 STOMP2、 MQTT3 等协议 )。
AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。
RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。
协议层
Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。
组件
交换器 (Exchange):消息代理服务器中用于把消息路由到队列的组件。
队列 (Queue):用来存储消息的数据结构,位于硬盘或内存中。
绑定 (Binding):一套规则,告知交换器消息应该将消息投递给哪个队列。
队列类型
死信队列
DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。
当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
原因
消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
消息 TTL 过期。
队列满了,无法再添加。
延迟队列
延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
AMQP 协议以及 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过 TTL 和 DLX 模拟出延迟队列的功能。
实现方式
通过 RabbitMQ 本身队列的特性来实现,需要使用 RabbitMQ 的死信交换机(Exchange)和消息的存活时间 TTL(Time To Live)。
在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。插件依赖 Erlang/OPT 18.0 及以上。
优先级队列
RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。
可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
工作模式
简单模式
work 工作模式
pub/sub 发布订阅模式
Routing 路由模式
Topic 主题模式
消息
传输
由于 TCP 链接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈,所以 RabbitMQ 使用信道的方式来传输数据。
信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接,且每条 TCP 链接上的信道数量没有限制。
就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,
这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。
信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接,且每条 TCP 链接上的信道数量没有限制。
就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,
这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。
安全
可靠性
消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。
方式
生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。
顺序性
拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;
或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
高可用
RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的。
模式
单机模式
Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式。
普通集群模式
在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。
镜像集群模式
RabbitMQ 的高可用模式。
跟普通集群模式不一样,在镜像集群模式下,创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,
就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。
然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,
也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。
然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,
也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
优点
任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。
缺点
这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重。
过期失效
RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。
解决
批量重导,即手动补回来。
Disruptor
定义
Disruptor 是一个开源的高性能内存队列,诞生初衷是为了解决内存队列的性能和内存安全问题,由英国外汇交易公司 LMAX 开发。
基于 Disruptor 开发的系统 LMAX(新的零售金融交易平台),单线程就能支撑每秒 600 万订单。
Disruptor 提供的功能优点类似于 Kafka、RocketMQ 这类分布式队列,不过,其作为范围是 JVM(内存)。
Disruptor 主要解决了 JDK 内置线程安全队列的性能和内存安全问题。
Kafka 和 Disruptor
Kafka:分布式消息队列,一般用在系统或者服务之间的消息传递,还可以被用作流式处理平台。
Disruptor:内存级别的消息队列,一般用在系统内部中线程间的消息传递。
应用
Log4j2:Log4j2 是一款常用的日志框架,它基于 Disruptor 来实现异步日志。
SOFATracer:SOFATracer 是蚂蚁金服开源的分布式应用链路追踪工具,它基于 Disruptor 来实现异步日志。
Storm : Storm 是一个开源的分布式实时计算系统,它基于 Disruptor 来实现工作进程内发生的消息传递(同一 Storm 节点上的线程间,无需网络通信)。
HBase:HBase 是一个分布式列存储数据库系统,它基于 Disruptor 来提高写并发性能。
核心概念
Event:你可以把 Event 理解为存放在队列中等待消费的消息对象。
EventFactory:事件工厂用于生产事件,我们在初始化 Disruptor 类的时候需要用到。
EventHandler:Event 在对应的 Handler 中被处理,你可以将其理解为生产消费者模型中的消费者。
EventProcessor:EventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。
Disruptor:事件的生产和消费需要用到 Disruptor 对象。
RingBuffer:RingBuffer(环形数组)用于保存事件。
WaitStrategy:等待策略。决定了没有事件可以消费的时候,事件消费者如何等待新事件的到来。
Producer:生产者,只是泛指调用 Disruptor 对象发布事件的用户代码,Disruptor 没有定义特定接口或类型。
ProducerType:指定是单个事件发布者模式还是多个事件发布者模式(发布者和生产者的意思类似,我个人比较喜欢用发布者)。
Sequencer:Sequencer 是 Disruptor 的真正核心。
此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。
此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。
等待策略
BlockingWaitStrategy:基于 ReentrantLock+Condition 来实现等待和唤醒操作,实现代码非常简单,是 Disruptor 默认的等待策略。
虽然最慢,但也是 CPU 使用率最低和最稳定的选项生产环境推荐使用。
虽然最慢,但也是 CPU 使用率最低和最稳定的选项生产环境推荐使用。
BusySpinWaitStrategy:性能很好,存在持续自旋的风险,使用不当会造成 CPU 负载 100%,慎用。
LiteBlockingWaitStrategy:基于 BlockingWaitStrategy 的轻量级等待策略,在没有锁竞争的时候会省去唤醒操作,但是作者说测试不充分,因此不建议使用。
TimeoutBlockingWaitStrategy:带超时的等待策略,超时后会执行业务指定的处理逻辑。
LiteTimeoutBlockingWaitStrategy:基于TimeoutBlockingWaitStrategy的策略,当没有锁竞争的时候会省去唤醒操作。
SleepingWaitStrategy:三段式策略,第一阶段自旋,第二阶段执行 Thread.yield 让出 CPU,第三阶段睡眠执行时间,反复的睡眠。
YieldingWaitStrategy:二段式策略,第一阶段自旋,第二阶段执行 Thread.yield 交出 CPU。
PhasedBackoffWaitStrategy:四段式策略,第一阶段自旋指定次数,第二阶段自旋指定时间,第三阶段执行 Thread.yield 交出 CPU,
第四阶段调用成员变量的waitFor方法,该成员变量可以被设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy三个中的一个。
第四阶段调用成员变量的waitFor方法,该成员变量可以被设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy三个中的一个。
快的原因
RingBuffer(环形数组) : Disruptor 内部的 RingBuffer 是通过数组实现的。
由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。
由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。
避免了伪共享问题:CPU 缓存内部是按照 Cache Line(缓存行)管理的,一般的 Cache Line 大小在 64 字节左右。
为了确保目标字段独占一个 Cache Line,会在目标字段前后增加了 64 个字节的填充(前 56 后 8 ),可以避免 伪共享(False Sharing)问题。
为了确保目标字段独占一个 Cache Line,会在目标字段前后增加了 64 个字节的填充(前 56 后 8 ),可以避免 伪共享(False Sharing)问题。
无锁设计:Disruptor 采用无锁设计,避免了传统锁机制带来的竞争和延迟。
Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。
Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。
总结
Disruptor 之所以能够如此快,是基于一系列优化策略的综合作用,既充分利用了现代 CPU 缓存结构的特点,又避免了常见的并发问题和性能瓶颈。
Pulsar
定义
下一代云原生分布式消息流平台,最初由 Yahoo 开发 ,已经成为 Apache 顶级项目。
Pulsar 集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,
具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。
具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。
特性
是下一代云原生分布式消息流平台。
Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。
极低的发布延迟和端到端延迟。
可无缝扩展到超过一百万个 topic。
简单的客户端 API,支持 Java、Go、Python 和 C++。
主题的多种订阅模式(独占、共享和故障转移)。
通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。
由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。
基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。
分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。
ActiveMQ
目前已经被淘汰,不推荐使用,不建议学习。
选择
ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用,已经被淘汰了。
RabbitMQ 在吞吐量方面虽然稍逊于 Kafka、RocketMQ 和 Pulsar,但是由于它基于 Erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。
但也因为基于 Erlang 开发,国内很难进行源码级的研究和定制,如果业务场景对并发量要求不高(十万级、百万级),RabbitMQ 是首选。
但也因为基于 Erlang 开发,国内很难进行源码级的研究和定制,如果业务场景对并发量要求不高(十万级、百万级),RabbitMQ 是首选。
RocketMQ 和 Pulsar 支持强一致性,对消息一致性要求比较高的场景可以使用。
RocketMQ 阿里出品,Java 系开源项目,源代码可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。
Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。
同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。
Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略。
天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。
同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。
Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略。
天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。
0 条评论
下一页