- 消息中间件为我们带来了异步特性,为系统解耦,对于大型分布式系统具有非常重大的意义
- 这样系统变得非常复杂
- 引入可扩展配置算是比较优雅的解决方案
- 降低了开发部署成本
- 并没实质性降低复杂性
引入消息中间件解耦服务调用
- 只需要向消息中间件发送消息
- 其他服务订阅该消息
- 成功解耦,不用关心多少系统需要知道登陆成功这个事件
- 各个系统互不影响
- 上面用数据库 记录中间状态,写入时数据库不可用,还是会有问题
- 对于需要感知的应用,需要定时轮询查看状态,完成操作也要修改
- 能解决问题,实现简单,但是也存在问题:
- 增加了业务库的负担
- 依赖的复杂和不安全(发短信的服务对数据库有操作权限,这样不安全)
- 扩展性不好
互联网时代的消息中间件
- JMS(java message service)是JavaEE(企业版)关于消息的规范
- ActiveMQ 等产品是对该规范的实现
- 企业内部或小型系统直接使用JMS产品是很经济的
- 大型系统有一些场景不适合
- 在大型互联网中,使用消息中间件,最基础俩特点
- 应用之间解耦
- 操作的异步
- 重点考虑:
- 消息的顺序保证
- 扩展性
- 可靠性
- 业务操作与消息发送的一致性
- 及多集群订阅者
- 消息发送一致性
- 产生消息的业务动作和消息发送的一致性
- 上述两种做法,第一种丢失消息的概率是很低的,
- 但是对于必须保证一致性的场景,上面的两种方案都不可取
- JMS 能保证消息发送的一致性吗?
- JMS 重要要素
- JMS 模型分为:Queue模型(PTP Domain模型)和Topic模型(pub/sub Domain模型)
- XA 开头的接口表示支持XA协议(分布式事务协议)
- 引入分布式事务带来的问题:
- 带来开销增加复杂性
- 对业务操作有限制,业务操作资源必须支持XA协议
- 引入分布式事务带来的问题:
- JMS 重要要素
- 最总一致性方案:
- 定时重复反向流程,重复查询就可以了
- 大多数情况下,反向流程是不需要工作的
- 新方案开销:仅仅增加了一次网络通信、一次更新消息状态,开销并不大
- 如何解决消息中间件和使用者之间的强依赖问题?
- 思路有三:
- 加强消息中间件可靠性,使之100%可靠
- 消息中间件影响业务进行的部分增强可靠性
- 业务表和消息表放在一个库,业务应用底层调用数据库同一个事务操作这俩表,保证一致性
- 影响有三:
- 业务库承载消息数据
- 消息中间件去访问业务库
- 业务操作的对象是一个数据库,支持事务的存储,满足消息的存储
- 变通方案
- 较多的逻辑从消息中间件的服务端挪到了消息中间件的客户端,并且在业务应用上执行
- 较多的逻辑从消息中间件的服务端挪到了消息中间件的客户端,并且在业务应用上执行
- 提供弱依赖支持,能够较好的保证一致性
- 利用本地磁盘方案
- 如果消息中间件不可用,而且写入本地磁盘也坏了,消息就丢了
- 两种用法:
- 容灾方案,平时不用,出现问题才使用
- 直接使用,可以控制调用发送消息接口的时间,比如做批处理
- 业务应用和发送消息一致性带来的俩限制:
- 需要确定要发送消息的内容
- 需要实现对业务的检查(实现反向流程)
- 思路有三:
消息模型对消息接收的影响
- JMS Queue 模型(点对点模型):
- peer to peer 点对点
- 消息发送出来不能确定会被谁消费,但只有一个应用回去消费这条消息
- JMS Topic模型
- 发送消息和topic内部逻辑与Queue 模型一样
- 接收消息很不相同,每个应用都能接收到所有的消息
- 每个连接connection 都有唯一clientId
- 下图是多链接的情况:
- 应用3有两个连接
- 换做topic 模型
- 模型需要满足的条件:
- 一个进程可以有多个connection连接到消息Server
- topic模型,实际应用中每个集群节点可能会很大,这个发送那么多重复数据负担太大
- 整合这俩模型:
- 面向集群用topic模型,具体应用使用queue模型来分发
- 级联方案:
- 面向集群用topic模型,具体应用使用queue模型来分发
消息订阅者订阅消息的方式:
- 持久订阅和非持久订阅
- 要做到可靠,选择持久订阅
- 接收者应用停止,消息保留,一旦上线再次发送
保证消息可靠性
- 三个阶段都可靠,才能保证消息可靠:
- 发送阶段要有明确的返回成功,才代表成功;
- 失败、超时、异常都代表没成功
- 消息存储(持久化存储)
- 基于现有可以选择分布式文件系统、关系型数据库、NOSQL
- 使用关系型数据库,不会那么严格按照范式,更多地使用冗余和宽表
- 比如 学生id和学生名称,很多时候要一起展示
- 单表查询比多表联查快的多,名称字段经常作为冗余字段存在
- 消息中间件存储,一条消息存储一条数据
- 表设计:
- 单条消息订阅集群比较多时,更新投递次数频繁,把该字段放到消息表
- 无法给投递列表里面的单独的接收者建立索引,损失了这个维度的灵活性
- 堆积消息多的时候,不能针对特定集群调度,处理效率低
- 基于双机内存保持数据的可靠
- 内存速度远超磁盘,但是断电消失,可考虑双机内存
- 一旦一台出问题,停止另一台的写操作,并把数据落盘
- 这种适用于大部分消息到了消息中间件后能大部分很快消费掉
- 表设计:
- 发送阶段要有明确的返回成功,才代表成功;
- 消息中间件扩容
- 消息中间件本身没有持久状态扩容简单
- 让发送者接收者感知到,有新的消息中间件加入集群,可使用软负载配置
- 不同消息中间件使用相同存储,同一个消息中间件使用多个存储、
- 消息存储中加入ServerId 来记录消息来自哪台机器
- 需要注意:
- 如果某个中间件长时间不可用,考虑加入新机器对应他的ServerId
- 没处理完的消息,分给别的机器处理
- 消息存储扩容
- 不存在复杂查询,服务端主动调度,绕开了根据消息id 取消息
- 消息投递可靠性保证
- 业务处理完再确认
- 不要吃掉异常,再确认,这样出现异常消息就丢了
- 投递处理优化
- batch操作
- 一个应用上多个订阅者订阅同一个消息
订阅者视角,消息重复的产生和应对
- 消息发送端重复发送
- 解决:相同消息使用同一个id
- 消息中间件向外投递重复
- 中间件不能及时更新已投递状态,可用分布式事务来解决,但是代价高
- 也可要消息接受者来处理,进行幂等操作:
- 幂等是一个数学概念
- 多次重复操作和一次操作效果一样
JMS消息确认方式:
- AUTO_ACKNOWLEDGE
- 接收到自动确认
- CLIENT_ACKNOWLEDGE
- 调用acknowledge() 函数确认
- DUPS_ACKNOWLEDGE
- 客户端处理函数执行完再确认
- connection 创建queue 或topic 时设置
消息投递其他属性:
- 优先级
- 订阅者消息处理顺序和分级订阅
- 自定义属性
- 局部顺序
- 需要一个属性区分和哪些消息一起排队
保证顺序的消息队列的设计
- 单机多队列
- 变推(push)为拉(pull)
- 多个物理队列
单机多队列问题与优化
- 队列多,查询性能差
- 根据队列做索引
- 好处:
- 坏处:
- 克服坏处:
- 作缓存
- 由于是顺序读取,可采用预读策略
- 本地消息存储可靠性:
- 把单个消息中间件机器变成Master-slave形式
- 发送消息向中间件,等slave复制完毕再返回”成功“
- 队列扩容:
推(Push)和拉(Pull)对比:
来源:oschina
链接:https://my.oschina.net/u/3847203/blog/2874235