1.分布式消息队列架构设计分析
1.1. 分布式消息队列(MQ)应用场景
-
异步缓冲
有些业务操作可以进行异步的,只要做到最终一致性即可,不用强一致性,这种业务场景就可以使用MQ来做
场景举例:用户注册以后,需要发送注册邮件和注册的提示短信。- 串行的方式
- 并行方式
(1) 串行方式:
(2) 并行方式
在正常情况下,我们的邮件和短信都需要等待一个确认返回,才知道整个过程是否完成,目前的解决方式还是强一致性的,如果发邮件或发短信服务出现问题,这个时候服务就无法确保正常了
如果引入了消息队列:
正常情况下只要将后续消息发送到MQ,业务就认为完成了,与后续的操作完全解耦
-
服务解耦
- 强依赖:使用dubbo或springcloud进行服务的调用和连接都是强依赖
- 弱依赖:可以选择使用MQ来做中间的连接
- 不代表弱依赖就可以失败
- 如果不能失败就要保证上游的消息发布端数据投递的可靠性
场景举例:用户下单后,订单需要更新库存
强依赖下会出现的问题:
1)假如库存系统无法访问,则订单减库存失败,从而导致订单生成失败
2)订单模块和库存模块是强耦合的
3)如果启用一个线程做离线操作,只是做了异步访问,访问只是提升速度,是否正常调用成功是无法保证的
通过弱依赖来解决以上问题:
1)订单生产成功写入消息到消息队列(保证消息的可靠投递)
2)库存系统通过订阅消息获取下单信息,库存系统根据下单信息进行库存操作
3)如果库存系统出现异常,库存消费消息失败的情况下消息就重回队列了,等待下次发送
-
削峰和填谷
- 当我们下游服务处理不过来的时候,就可以将这些消息缓存在一个地方,逐步处理
- 将短暂一段时间的业务积压在后面缓慢执行就是削峰和填谷的过程
场景举例:比如我们的秒杀活动
-
消息通讯
- 点对点通讯
- 发布订阅模式
1.2. 分布式消息队列应用需要思考的内容
如果业务场景需要使用MQ,需要关注以下几个方面
- 生产端的可靠性投递
- 如果消息和钱有关,这个消息一定不能丢失
- 需要做到生产端100%投递,就需要和业务数据保证原子性
- 消费端的幂等
- 生产端如果要做到可靠性投递,可能会有重复投递
- 消费端消费了两次或多次这个数据可能会不一致
- 所以消费端一定要做到同一个请求消费多次得到的结果一样
- MQ本身需要考虑的问题
- HA:高可用
- 低延迟
- 可靠性:确保数据是完整的
- 堆积能力:这是MQ能扛下你的业务量级的保证
- 扩展性:是否能够天然支持横向扩展无感知扩容
1.3. 业界主流分布式消息队列分析
1.3.1.技术选型
需要关注的地方
- 各个MQ的性能,优缺点,相应的业务场景
- 比如ActiveMQ适合传统业务公司,不适合大型高并发海量数据的业务场景
- 比如RabbitMQ的横向扩展能力就不是非常强
- 集群架构模式的分析:分布式、可扩展、高可用、可维护性
- 综合成本、集群规模、人员学习成本
- 如果消息的可靠性要求和依赖不是特别高,就可以使用kafka
- kafka可以在很廉价的机器上有着很高的吞吐量和性能表现
- 未来的规划和方向思考
1.3.2. RabbitMQ集群架构原理解析
- 主备模式
master-slave结构,可以理解为热备份,master负责读写,master宕机后切换到slave - 镜像模式(Mirror)
业界主流使用比较多的模式
RabbitMQ集群非常经典的就是镜像模式,保证数据100%不丢失
高可用、数据同步低延迟、奇数个节点
镜像队列集群的缺陷是无法进行很好的横向扩容,因为每个节点都是一个完整的互相复制的节点,并且镜像节点过多也会增加MQ的负担,一个数据写入就要复制到多个节点,吞吐量也会降低 - 多活模式
类似远程拷贝,做一个异地的容灾和双活,或者是数据转储的功能
2. RabbitMQ应用实战
2.1. RabbitMQ核心概念的掌握
RabbitMQ是一个开源的消息代理和队列服务器,RabbitMQ是基于AMQP协议的,RabbitMQ的优点如下:
- 采用Erlang语言进行开发作为底层语言实现:Erlang有着和原生Socket一样的延迟,所以性能非常高
- 开源、性能优秀,稳定性保障
- 提供可靠性消息投递模式(confirm)、返回模式(return)
- 与SpringAMQP完美整合,API丰富
- 集群模式比较丰富,表达式配置,HA模式,镜像队列模型
- 保证数据不丢失的前提做到高可靠性,可用性
高级消息队列协议-AMQP(Advanced Message Queuing Protocol)
AMQP核心部件的组成结果如下
消息的生产者将消息投递到Server上,经过Virtual host到Exchange就可以了,消息者只需要和Message Queue进行监听和绑定,从而实现了队列级别的解耦,生产者不需要关心消息到哪个队列,只需要将消息投递给Exchange,消费者只需要监听队列即可,他们之间的关系是通过路由来进行关联的,Exchage和Message Queue之间绑定关系通过Routing key进行关联,消息发送到Exchange上然后通过某种路由规则把消息路由到某个Message Queue上
AMQP的专有名词解释
- Server:又称为Broker
- Connection:连接,应用程序和broker之间的网络连接
- Channel:网络信道,一个网络会话的任务
- Message:消息
- Virtual host:虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个Virtual host里面可以有若干个Exchage和Queue,但同一个Virtual host里面不能有相同名称的Exchage和Queue
- Exchange:交换机,接收消息,根据路由键转发消息到绑定队列
- Binding:Exchage和Queue之间的虚拟连接,binding中可以包含routing key
- Routing key:一个路由规则
- Queue:保存具体的消息的容器
RabbitMQ消息的流转
主要定义
- Broker: 简单来说就是消息队列服务器实体
- Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
- Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
- Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
- Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
- VHost: 虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
- Producer: 消息生产者,就是投递消息的程序
- Consumer: 消息消费者,就是接受消息的程序
- Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。
工作模式:
1、简单模式 HelloWorld : 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
2、工作队列模式 Work Queue: 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)
3、发布订阅模式 Publish/subscribe: 需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
4、路由模式 Routing: 需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
5、通配符模式 Topic: 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
2.2. RabbitMQ的安装和使用
操作系统centOS 7.x
# 先修改主机名称
vi /etc/hostname
HOST232
# 在hosts里增加主机和当前ip的映射
vi /etc/hosts
192.168.0.232 HOST232
# 重启服务器:reboot
# 1.安装需要的辅助工具
yum -y install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
# 2.下载安装包
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
# 3.按照顺序安装rpm,使用rpm一键安装
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
# 4.修改用户登录和连接心跳,使用rabbitMQ自己的管理控制台
vi /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
将 loopback_users 中的 <<"guest">>,修改为 guest
将 {heartbeat, 60} 修改为 {heartbeat, 10} # 用于进行心跳的间隔
# 5.启动rabbitmq的服务端
/etc/init.d/rabbitmq-server start | stop | status | restart
rabbitmq-server start &
# 6.查看MQ端口是否启用:yum -y install lsof
lsof -i:5672
# 7.安装控制台插件
/etc/init.d/rabbitmq-plugins list # 查看都有哪些插件
rabbitmq-plugins enable rabbitmq_management # 启用插件
lsof -i:15672 # 查看管理后台是否启动
# 8.登录管理控制台
http://39.103.140.196:15672/ # 登录的用户名密码都是guest
Disc就是指磁盘存储消息,如果想要以内存方式存储在启动rabbitMQ时加上 --ram 即可,能够提升效率
常用命令
# 关闭应用
rabbitmqctl stop_app
# 启动应用
rabbitmqctl start_app
# 节点状态
rabbitmqctl status
# 添加用户密码
rabbitmqctl add_user username password
# 修改用户密码
rabbitmqctl change_password username password
# 列出所有用户
rabbitmqctl list_users
# 删除用户
rabbitmqctl delete_user username
# 列出用户权限
rabbitmqctl list_user_permissions username
# 清除用户权限
rabbitmqctl clear_permissions -p vhostpath username
# 设置用户权限
# 三个*对应:configure write read
rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
rabbitmqctl set_permissions -p / gavin ".*" ".*" ".*"
# 列出所有虚拟主机
rabbitmqctl list_vhosts
# 创建虚拟主机
rabbitmqctl add_vhost vhostpath
# 列出虚拟主机的权限
rabbitmqctl list_permissions -p vhostpath
# 删除虚拟主机
rabbitmqctl delete_vhost vhostpath
# 查看所有队列
rabbitmqctl list_queues
# 清除队列里的消息
rabbitmqctl -p vhostpath purge_queue queueName
# 清除所有数据
rabbitmqctl reset # 这个动作最好在MQ服务停掉后操作
3. RabbitMQ整合Springboot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
建立yaml配置
spring:
rabbitmq:
host: 39.103.140.196
username: guest
password: guest
virtual-host: /
connection-timeout: 15000
3.2. 发送一个消息
1、先创建一个要发送的实体对象
@Data
public class OrderInfo implements Serializable {
private String id;
private String order_name;
//消息id是用来生成一个消息的唯一id,通过消息id能找到这个消息的业务信息
private String message_id;
}
2、编写发送类
@Component
public class OrderSender {
@Autowired
RabbitTemplate rabbitTemplate;
public void sendOrder(OrderInfo orderInfo) throws Exception{
//4个参数
//exchagen:发送消息的交换机
//routingkey
//object:具体的消息对象
//correlationData:消息唯一id
CorrelationData correlationData = new CorrelationData();
correlationData.setId(orderInfo.getMessage_id());
rabbitTemplate.convertAndSend("order-exchange","order.abc",orderInfo,correlationData);
}
}
这个时候是否可以发送消息了?
不行,因为order-exchange和他绑定的队列还没有创建
-
去rabbitmq的控制台创建order-exchange
Exchange的相关属性说明 -
Name:exchange的名称
-
Type类型说明
- direct:exchange在和queue进行binding时会设置routingkey,只有routingkey完全相同,exchange才会将消息转发到对应的queue上,相当于点对点
- fanout:直接将消息路由到所有绑定的队列中,无需对消息的routingkey进行匹配操作,因为不绑定routingkey,所有也是消息转发最快的(广播方式)
- topic:此类型的exchange和direct差不多,但direct类型要求routingkey完全相同,而topic可以使用通配符:‘’,‘#’
其中‘’表示匹配一个单词,‘#’则表示匹配没有或者多个单词 - header:路由规则是根据header来判断
- 总结:一般direct和topic用来具体的路由信息,如果用广播就使用fanout,header类型用的比较少
-
Durability:Durable是持久化到磁盘的意思
-
Auto Delete:如果设置为yes,那么最后一个绑定到exchange上的队列被删除后,exchange也会自动删除
-
Internal:如果为yes则表明该exchange是rabbitmq内部使用,不提供给外部系统应用,一般是在自己编写erlang语言做定制化扩展时使用
-
Arguments这个是扩展AMQP协议时自定义使用的内容
去rabbitmq的控制台创建order-queue
Exchange和Queue通过Binding关联,由routingkey进行路由
在exchange里和queue里都可设置binding,在exchange里设置一下
* : routingkey就可以写成 order.*,order.abc / order.123 / order.dfg 这三个都可以路由到order-queue,但是order.123.abc就不行,因为order后面的单词又多个
# : 代表匹配了多个单词,order.abc / order.123.abc 这两个都可以匹配
3 测试
@RestController
public class RabbitController {
@Autowired
OrderSender orderSender;
@GetMapping("sender")
public String sender(){
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId("10001");
orderInfo.setMessage_id("ms10001");
orderInfo.setOrder_name("测试的消息");
try {
orderSender.sendOrder(orderInfo);
}catch (Exception ex){
ex.printStackTrace();
}
return "--消息发送成功--";
}
}
注意:
- 一个exchange可以绑定多个queue,只要routingkey一样,一个消息就会发送到多个queue上
- exchange绑定一个queue,无论binding多少个routingkey,只要符合这个routingkey规则的消息都会发送到这个队列中,接收的时候无论从哪个routingkey过来的消息,连接这个队列的消费端都会消费掉,相当于多个消息规则对应一个队列
3.3. 接收一个消息
spring:
rabbitmq:
host: 39.103.170.186
port: 5672
username: guest
password: guest
virtual-host: /
connection-timeout: 15000
listener:
simple:
concurrency: 5 # 初始化并发数
max-concurrency: 10 # 最大并发数
auto-startup: true # 自动开启监听
prefetch: 1 # 每个连接同一时间最多处理几个消息,限流设置
acknowledge-mode: manual # 签收模式为手动签收
接收消息的实现类
@Component
public class OrderReceiver {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-queue",durable = "true"),
exchange = @Exchange(value = "order-exchange",durable = "true",type = "topic"),
key = "order.*"
)
)
@RabbitHandler
public void onOrderMessage(@Payload OrderInfo orderInfo,
@Headers Map<String,Object> headers,
Channel channel) throws Exception{
//消费者操作
System.out.println("-----消息收到开始消费-----");
System.out.println("Order Name:"+orderInfo.getOrder_name());
Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
//因为是手动签收,所以需要进行ACK一下
//deliveryTag是签收标识,false是不支持批量签收
channel.basicAck(deliveryTag,false);
}
}
4. 消息接收的ACK和NACK机制
- ACK刚刚在上面的代码里已经体验过了,如果把消息消费设置成手工模式,在MQ没有接收到ACK信息时消息就是unacked状态的,在这个状态下消费端不会再次接收这个消息,如果因为在执行过程中出现异常而没有ACK的动作,消息时不会重试
- 如果系统出现宕机导致没有ACK操作,消息仍停留在MQ中,消息不会发给消费端的,只有消费端服务重启后才会再次接收到这个消息
- NACK就是业务过程中因业务异常而未执行成功的消息就可以通过NACK重回队列的队首,这个可以用在重试机制中,如果业务出错异常就调用NACK,必须要设置重回队列的次数,记录重回超过N次后,就进行ACK操作不要在重回了,不重回之后将消息放入其他补偿队列中,后续进行人工补偿,如果不设置重试次数则会导致队列一直处在NACK的状态导致后面的消息阻塞。
5. 消息队列的TTL
什么是TTL
- TTL是Time To Live的缩写,也就是生存时间
- RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
- RabbitMQ也支持队列的过期时间,从消息进入这个队列开始计算,只要消息存活时间超过了队列配置的超时时间,消息就会自动清除
实现消息队列内部的过期时间等
- x-message-ttl:单位毫秒,该消息队列里所有消息从进入开始计算时间,打到配置的值后自动清除
- x-expires:单位毫秒,队列处于不活跃状态多少毫秒后将自动删除
- x-max-length:队列最多存放多少条消息,如果达到阈值,新消息进入会将最早的一条删除
- x-max-length-bytes:队列最大存放的容量,如果达到阈值,新消息进入后将会删除最早一条,如果还存不下则继续删除第二条
实现消息本身的TTL
@Component
public class OrderSender {
@Autowired
RabbitTemplate rabbitTemplate;
public void sendOrder(OrderInfo orderInfo) throws Exception{
//4个参数
//exchagen:发送消息的交换机
//routingkey
//object:具体的消息对象
//correlationData:消息唯一id
CorrelationData correlationData = new CorrelationData();
correlationData.setId(orderInfo.getMessage_id());
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");
return message;
}
};
rabbitTemplate.convertAndSend("order-exchange","order.abc",orderInfo,messagePostProcessor,correlationData);
}
}
6. 死信队列的详解和触发机制
首先看一下什么是死信:当一个消息无法被消费时就会变成死信,死信怎么形成的需要分析一下
什么是死信队列:DLX,Dead-Letter-Exchange
当消息在一个队列中无法被消费时就会成为死信,这个时候能被重新publish到新的Exchange就是DLX死信队列
通过控制台创建队列并指定成为死信队列
这个时候order-queue这个队列绑定的死信已经有了,但具体的死信交换机和死信队列还没有创建,需要创建一下
其实我们的死信队列就是一个正常队列,参数x-dead-letter-exchange,x-dead-letter-routing-key这些都只是绑定死信一个关系而已,实质上死信队列相关的:Exchange、queue、binding、routingkey都要自己创建
通过代码创建队列的同时关联死信队列
@Component
public class OrderReceiver {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-queue",durable = "true",autoDelete = "false",arguments = {
@Argument(name = "x-dead-letter-exchange",value = "dead-exchange"),
@Argument(name = "x-dead-letter-routing-key",value = "dead.key")
}),
exchange = @Exchange(value = "order-exchange",durable = "true",type = "topic"),
key = "order.*"
)
)
@RabbitHandler
public void onOrderMessage(@Payload OrderInfo orderInfo,
@Headers Map<String,Object> headers,
Channel channel) throws Exception{
//消费者操作
System.out.println("-----消息收到开始消费-----");
System.out.println("Order Name:"+orderInfo.getOrder_name());
Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
//因为是手动签收,所以需要进行ACK一下
//deliveryTag是签收标识,false是不支持批量签收
if("5".equals(orderInfo.getId())){
//消费失败消息重回队列
channel.basicNack(deliveryTag,false,true);
}else {
channel.basicAck(deliveryTag, false);
}
}
}
7. 镜像集群模式的构建
需要配置和搭建三台RabbitMQ的服务,每台机器上都需要
- RabbitMQ服务
- 管理控制台服务
三台服务器RabbitMQ服务和控制台都启动起来,看一下三个节点的overview都是单机的
# 0.搭建单机服务需要注意以下几点
vi /etc/hostname # 修改主机名称
vi /etc/hosts # 配置三台机器的映射
192.168.0.233 RMQ233
192.168.0.234 RMQ234
192.168.0.235 RMQ235
# 记得重启一下机器reboot
# 1.先停止三个节点的服务
rabbitmqctl stop
# 2.进行文件同步操作
# 将一台机器选择作为master,这里使用RMQ233,将它的服务中的cookie文件同步到另外两台机器上
# cookie文件在/var/lib/rabbitmq目录下
scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.234:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.235:/var/lib/rabbitmq/
# 3.启动集群,所有机器都要执行启动
rabbitmq-server -detached
# 4.slave加入集群操作,相当于寻址,slave加入master
slave1: rabbitmqctl stop_app
slave1: rabbitmqctl join_cluster rabbit@RMQ233
slave1: rabbitmqctl start_app
slave2: rabbitmqctl stop_app
slave2: rabbitmqctl join_cluster rabbit@RMQ233
slave2: rabbitmqctl start_app
# 5.移除slave节点
# 在需要移除的节点上停掉节点
rabbitmqctl stop_app
# 在master节点上执行移除
rabbitmqctl forget_cluster_node rabbit@RMQ234
# 6.设置集群的名称
# 在集群的任意节点可以进行设置
rabbitmqctl set_cluster_name rabbitmq_cluster_0812
# 7.查看集群状态
# 在集群的任意节点可以
rabbitmqctl cluster_status
如果要把集群节点设置成内存的方式
# 将235设置成ram形式
slave235:rabbitmqctl stop_app
master:rabbitmqctl forget_cluster_node rabbit@RMQ235
slave235:rabbitmqctl join_cluster --ram rabbit@RMQ233
slave235:rabbitmqctl start_app
最后一步就是配置镜像队列
- 目的是将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态一致
- 消息发送到master也会同步所有slave
# 在任意节点
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'