把项目从单体架构 拆开 成 分布式架构(各个服务系统之间用中间件来沟通)
概念
高可靠、高可用、持久性的通过提供消息传递 和 消息的排队机制,它可以在分布式系统下扩展进程间的通讯
应用场景
- 跨系统数据传递
- 高并发的流量削峰
- 数据的分发和异步处理
- 大数据分析和传递
- 分布式事务
实现 缓存、静态化处理数据同步,日志监控,分布式事务,抢票,下单等
为什么使用MQ
刚开始项目采用的是单体结构,把所有的业务堆积在一个项目中,但随着公司的发展拆分了项目,所以使用消息中间件作为各个模块之间的沟通工具。
一、RabbitMQ
1.1 RabbitMQ介绍
功能:负责数据的传递、存储、分发消费 三个部分
协议:使用的是自己的协 - AMQP(因为http协议太复杂且不能持久化,tcp协议太简单不满足)
AMQP协议支持 分布式事务,消息持久化,高性能,高可靠
消息的持久化:将数据存储进磁盘,而不是存储在内存中随服务器重启或断开而消息
消息的分发策略:发布订阅,轮询分发,公平分发,重发,消息拉取等
1.2安装RabbitMQ
环境:centos7/windows + erlang
有一个默认的账号/密码:guest
默认端口15672
1.3 RabbitMQ的使用
有六种机制:
- 简单模式(simple)
-
工作模式(work)
-
发布订阅模式(public/subscribe)
是没有路由key的模式
- 路由模式(routing)
精确的routing-key匹配
当所有的可以都一样那就是发布订阅模式了
- 主题模式(topic)
模糊的路由匹配模式
- 参数模式(rpc 很少用)
拓展:
#标识0个或者多个
*至少一个
1.4 原始的rmq的式代码使用
1.4.1 简单模式
1.4.1.1 生产者
第一步:创建连接工厂ConnectionFactory
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("ip地址");
cf.setport(“端口号,默认15672”);
cf.setUsername/setPassword/setvirtualHost("/")
第二步:创建连接Connection
Connection ct = cf.newConnection("生产者名");
第三步:通过连接获取通道channel
Channel channel = ct.ctreatChannel():
第四步:通过创建交换机、声明队列、绑定关系、路由机制、发消息(生产者)、接受消息(消费者)
channel.queueDeclare(队列名,是否序列化,是否排他性,是否自动删除,备注)
第五步:准备消息内容
第六步:发送消息给队列queue
channel.basicPublish("交换机,没有就是空字符串",队列名,是否持久化,消息内容.getBytes());
第七步:关闭连接
if(channel != null && channel .isopen() ) {
channel.close();
}
第八步:关闭通道
if(ct!= null && ct.isopen() ) {
ct.close();
}
1.4.1.2 消费者
第123478步骤一样,没有第5步,第六步如下
channel.basicConsume(队列名,应答机制【true是自动,false手动,推荐false】, new DeliverCallback(){
方法{ sout(“”消息接收成功“”)}
}, new CancelCllback() {
方法{ sout(“”消息接收失败“”)}
} );
1.4.1.3 注意问题
-
为什么Rabbitmq是基于通道去处理消息而不是连接? 因为 MQ是长连接,创建通道提高性能
-
不可能存在没有交换机的队列,即使没有指定交换机但会默认direct类型的名字为AMQP_default交换机
-
消息一定是从交换机传递给队列的
1.4.2 发布订阅模式
交换机得是fanout类型
消息会发往所有队列(绑定的)
生产者:channel.basicPublic(交换机名,路由""【如果交换机为“”,那路由可以设置为队列名】, null, message,getBytes());
消费者:正常同上
1.4.3 路由模式
交换机是direct类型
在交换机和各个队列之间定义各自的路由key,精确匹配
使生产者发送带有路由key的消息,由交换机发送到满足key的队列中
生产者:channel.basicPublic(交换机名,路由key, null, message,getBytes());
消费者:正常同上
1.4.4 主题模式
交换机是topic类型
是路由模式的另一种写法,只是模糊匹配
生产者:channel.basicPublic(交换机名,路由key, null, message,getBytes());
消费者:正常同上
1.4.5 把交换机 和 队列绑定
- 用代码消费指定队列的消息时,如果队列不存在,不会自助创建队列;同理交换机不存在也不会创建,会直接报错
- 创建交换机
channel.exchangeDeclare(交换机名,交换机类型,持久化【true,false】); - 创建队列
channel.queueDeclare(队列名,持久化,排他性,自动删除,其他参数); - 绑定交换机和队列
channel.queueBind(队列名,交换机名,路由);
1.4.6 工作模式
-
分位两种:
- 轮询模式:一个消费者每次消费一条消息,轮流消费(按均分配)
- 公平分发模式:根据消费者的消费能力进行公平分发,处理快的消费就多(按劳分配)
-
公平分发一定要把应答改为手动应答,也就是ack参数为false
channel.basicConsumer(队列名,false,…); -
使用channel.basicQOS(1); 来吧默认的 轮询分发变成公平分发模式,数字1表示一次分发消息的数,一遍设置为10-20
-
手动应答,放在消费者的handle方法中(zai channel.basicConsume里)
channel.basicAck(应答内容,false);
二、springboot整合RabbitMQ
这就以fanout交换机(发布订阅)举例子
2.1生产者
-
第一步:依赖
在pom.xml中导入spring-boot-starter-amqp依赖 -
第二步:application.yml配置
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 47.104.141.27
port: 15672
- 第三步:注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate
- 第四步:写配置类来完成交换机类型与队列关系的绑定
@Bean
public FanoutExchange fanoutExchange() {
// 参数:交换机名,持久化,自动删除
return new FanoutExchange("fanout_order_exchanger", true, false);
}
// 声明队列
@Bean
public Queue smsQueue() {
// 参数:队列名,持久化
return new Queue("sms.fanout.queue", true);
}
// 队列和交换机绑定
@Bean
public Binding smsBingding() {
return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
}
}
- 第五步:发消息
rabbitTemplate.convertAndSent(交换机名,路由key/队列名,消息内容);
2.2 消费者
依赖和配置同生产者一样。
对于消费者来说,只需要管理队列和消费者之间的监听关系就行 ,实现逻辑如下:
在类上加注解@RabbitListener(queues={“队列名”}) + @Service
在里面的方法上加注解@RabbitHandler表示消息的落脚点
@RabbitListener(queues={“队列名”})
@Service
public class Consumer {
@RabbitHandler
public void receivemessage(Strin message) {
}
}
2.3 其他交换机模式的写法
2.3.1 direct模式
只需生产者修改配置类就行
- FanourExchange 改成DirectExchange
- 队列和交换机之间有个路由的绑定
BindingBuilder.bind(smsQueue()).to(directExchange).with(“路由key”) - 发送消息加上路由
2.3.2 topic模式
同2.3.1一样修改成自己的交换机类型,绑定路由
2.4 拓展
上面是在配置类上绑定队列和交换机的,其实可以直接通过在 消费者上注解来绑定,但是不建议,不方面死信队列、延时队列等特殊的mq方法的使用;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "队列名", durable = "持久化 true", autodelete = "false"),
exchange = @Exchange(value= "交换机名",type = ExchangeType.Topic),
key="#.sms.#"
))
三、RabbitMQ的高级使用
3.1过期时间TTL
表示可以对消息设置过期时间,在这个时间内的都可以被消费者消费,过了这个时间的消息将自动被删除。
有两个方式对消息和队列设置过期时间TTL:
- 方式一:在配置类中设置队列的时候设置队列的属性,让该队列中所有消息都有相同的过期时间
// 声明过期队列
@Bean
public Queue smsQueue() {
// 设置过期时间ttl
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000); // 毫秒
// 参数:队列名,持久化
return new Queue("sms.fanout.queue", true, false,false, args);
}
-
方式二:发送消息的时候对消息进行设置,可以对不同的消息设置不同的ttl
在生产者发送消息前(rabbitTemplate.convertAndSent();)设置
MessagePostProcessor mpp = new MessagePostProcessor () {
@Override
public Message postProcessMessage (Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
rabbitTemplate.convertAndSent(交换机名, 路由key, 消息, mpp );
问: 如果队列和消息都设置了ttl过期时间,以过期时间最短的为准
方法一比方法二更好,过期队列的消息可以转移到死信队列中。
3.2 死信队列DLX
- 当消息在一个队列中变成死信之后,他能呗重新发送到另一个交换机(DLX死信交换机)中,绑定DLX死信交换机的队列称之为死信队列
- 消息死信的原因有三
- 消息被拒
- 消息过期 (消息过期 - 死信交换机 - 死信队列 - 死信消费者)
- 队列达到最大长度
- 想要使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange(x小写), 来指定死信交换机即可
使用:
3.2.1 第一步:定义一个死信交换机和死信队列(正常定义就行)
@Bean
public FanoutExchange dxlExchange() {
// 参数:交换机名,持久化,自动删除
return new FanoutExchange("fanout_order_exchanger", true, false);
}
// 声明队列
@Bean
public Queue dxlQueue() {
// 参数:队列名,持久化
return new Queue("sms.fanout.queue", true);
}
// 队列和交换机绑定
@Bean
public Binding smsBingding() {
return BindingBuilder.bind(dxlQueue()).to(dxlExchange());
}
}
3.2.2 第二步:在配置类的过期队列中绑定死信交换机和路由
// 声明过期队列
@Bean
public Queue smsQueue() {
// 设置过期时间ttl
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000); //过期时间, 毫秒
args.put("x-max-length", 5); // 设置队列长度
args.put("x-dead-letter-exchange", 死信交换机名); // 绑定死信交换机
args.put("x-dead-letter-routing-key", "*.dead.*"); // 绑定死信交换机的路由,fanout的可以不用写这个
// 参数:队列名,持久化
return new Queue("sms.fanout.queue", true, false,false, args);
}
3.2.3 第三步:去rmq页面删除原先的过期队列,重启项目
因为,队列一但创建好,再去修改配置不会自动更新。
所以,要不删除重来,要不新建一个过期队列。
3.3 延时队列
过期队列 + 死信队列 = 延时队列
举例说明:订单支付
下单后,发送一个消息到过期时间为30分钟的延时队列中,30分钟后消息没被消费就跑到死信队列,做撤销订单的处理
3.4持久化机制
- RabbitMQ的持久化机制 是存储在磁盘中的,而不是内存。
- 默认RabbitMQ站电脑内存的0.4(若为8G的电脑内存,那就占了8*0.4=3.2G),超过这个值就会阻塞爆红预警,可以通过修改配置文件 rabbitmq.conf(vm_memory_high_watermark.relative= 0.6)
- 默认,当内存中的消息大于阈值的50%,就会把多余的消息存入磁盘,可以修改配文来修改这个阈值
3.5 集群搭建
erlang语言天生支持集群
举例说明:在一台机器上搭建一个rabbitmq集群(单机多实例的方式)
第一步:检查MQ是否在运行,运行了则停止
ps aux|grep rabbitmq
stop rabbitmq-server
第二步:启动第一个节点rabbit-1
第三步:启动第一个节点rabbit-2
第四步:用ps aux|grep rabbitmq 查看这几个节点是否运行成功
第五步:rabbit-1作为主节点,rabbit-2作为从节点
第六步:用sudo rabbitmqctl cluster_status -n -rabbit-1验证集群是否生效
第七步:修改application.yml的配置
3.6 分布式事务
-
在不同系统之间如何保证数据一致性的解决方案 ----- 分布式事务
-
操作位于不同的节点上,需要保证事务的ACID特性;例如:下单,库存和订单在不同节点(独立的jvm和数据库)上,则就设计分布式事务(数据一致性),如果库存异常,则要回滚等
-
spring提供的声明式事务只能控制当前的JVM,不支持跨jvm的控制
-
使用RabbitMQ确保分布式事务,其实就是本地消息表的异步确保(对账单的形式)
请不要吝啬你发财的小手,点赞收藏评论,谢谢!
请不要吝啬你发财的小手,点赞收藏评论,谢谢!
请不要吝啬你发财的小手,点赞收藏评论,谢谢!