消息中间件-RabbitMQ

把项目从单体架构 拆开 成 分布式架构(各个服务系统之间用中间件来沟通)

概念

高可靠、高可用、持久性的通过提供消息传递 和 消息的排队机制,它可以在分布式系统下扩展进程间的通讯

应用场景

  • 跨系统数据传递
  • 高并发的流量削峰
  • 数据的分发和异步处理
  • 大数据分析和传递
  • 分布式事务

实现 缓存、静态化处理数据同步,日志监控,分布式事务,抢票,下单等

为什么使用MQ

刚开始项目采用的是单体结构,把所有的业务堆积在一个项目中,但随着公司的发展拆分了项目,所以使用消息中间件作为各个模块之间的沟通工具。

一、RabbitMQ

1.1 RabbitMQ介绍

功能:负责数据的传递、存储、分发消费 三个部分
协议:使用的是自己的协 - AMQP(因为http协议太复杂且不能持久化,tcp协议太简单不满足)
AMQP协议支持 分布式事务,消息持久化,高性能,高可靠
消息的持久化:将数据存储进磁盘,而不是存储在内存中随服务器重启或断开而消息
消息的分发策略:发布订阅,轮询分发,公平分发,重发,消息拉取等

1.2安装RabbitMQ

环境:centos7/windows + erlang
有一个默认的账号/密码:guest
默认端口15672

1.3 RabbitMQ的使用

有六种机制:

  1. 简单模式(simple)

在这里插入图片描述

  1. 工作模式(work)
    在这里插入图片描述

  2. 发布订阅模式(public/subscribe)

是没有路由key的模式

在这里插入图片描述

  1. 路由模式(routing)

精确的routing-key匹配
当所有的可以都一样那就是发布订阅模式了

在这里插入图片描述

  1. 主题模式(topic)

模糊的路由匹配模式

在这里插入图片描述

  1. 参数模式(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 注意问题
  1. 为什么Rabbitmq是基于通道去处理消息而不是连接? 因为 MQ是长连接,创建通道提高性能

  2. 不可能存在没有交换机的队列,即使没有指定交换机但会默认direct类型的名字为AMQP_default交换机

  3. 消息一定是从交换机传递给队列的

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 把交换机 和 队列绑定

  1. 用代码消费指定队列的消息时,如果队列不存在,不会自助创建队列;同理交换机不存在也不会创建,会直接报错
  2. 创建交换机
    channel.exchangeDeclare(交换机名,交换机类型,持久化【true,false】);
  3. 创建队列
    channel.queueDeclare(队列名,持久化,排他性,自动删除,其他参数);
  4. 绑定交换机和队列
    channel.queueBind(队列名,交换机名,路由);

1.4.6 工作模式

  • 分位两种:

    1. 轮询模式:一个消费者每次消费一条消息,轮流消费(按均分配)
    2. 公平分发模式:根据消费者的消费能力进行公平分发,处理快的消费就多(按劳分配)
  • 公平分发一定要把应答改为手动应答,也就是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模式

只需生产者修改配置类就行

  1. FanourExchange 改成DirectExchange
  2. 队列和交换机之间有个路由的绑定
    BindingBuilder.bind(smsQueue()).to(directExchange).with(“路由key”)
  3. 发送消息加上路由

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

  1. 当消息在一个队列中变成死信之后,他能呗重新发送到另一个交换机(DLX死信交换机)中,绑定DLX死信交换机的队列称之为死信队列
  2. 消息死信的原因有三
    • 消息被拒
    • 消息过期 (消息过期 - 死信交换机 - 死信队列 - 死信消费者)
    • 队列达到最大长度
  3. 想要使用死信队列,只需要在定义队列的时候设置队列参数 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确保分布式事务,其实就是本地消息表的异步确保(对账单的形式)

在这里插入图片描述

请不要吝啬你发财的小手,点赞收藏评论,谢谢!

请不要吝啬你发财的小手,点赞收藏评论,谢谢!

请不要吝啬你发财的小手,点赞收藏评论,谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LC超人在良家

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

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

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

打赏作者

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

抵扣说明:

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

余额充值