RabbitMQ
1. RabbitMQ介绍
- 分布式系统中的重要组件,主要解决,异步处理,应用解耦,流量削峰等问题
- 异步处理
- 应用解耦
- 作为典型的生产者消费者模型,实现了订单系统和库存系统的应用解耦。
- 流量削峰
- 对高并发场景进行流量控制,抛弃超过长度的队列。
1.1 各组件功能
-
Publisher:消息的生产者。
-
Virtual Host:虚拟主机。各个主机拥有自己的配置(队列、交换机)。
-
Exchange:交换器:接受消息,转发给队列。
-
Banding:绑定。将消息绑定至路由。
-
Queue:消息队列。
-
Channel:信道,复用TCP连接,独立的双向数据流通道。包括旗下的Connection(一个TCP连接)
-
Consumer:消息的消费者
2. RabbitMQ使用
2.1 测试连接
- 一个专门获取连接的工具类。
public class ConnectionUtil {
public static Connection getConnection() throws Exception{
ConnectionFactory factory = new ConnectionFactory();
//设置连接信息
factory.setHost("IP");
factory.setPort(5672);
factory.setVirtualHost("/lagou");
factory.setUsername("zz");
factory.setPassword("123456");
//获得连接
Connection connection = factory.newConnection();
return connection;
}
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
System.out.println(connection);
//amqp://zz@IP:5672//lagou
}
}
2.2 RabbitMQ模式
- 点对点模式:
- 消息队列(queue),发送者(sender),接收者(receiver)
- 发送方与接受方不存在依赖关系。
1.简单模式
-
单纯的存储、转发消息。
-
生产者:
public class sender { public static void main(String[] args) throws Exception { //获取连接 Connection connection = ConnectionUtil.getConnection(); //创建信道 Channel channel = connection.createChannel(); //声明队列 //队列名、是否持久化、是否排外、是否自动删除(连接为0时删除)、队列参数 channel.queueDeclare("no1", false, false, false, null); //发布消息 String msg = new String("hello2 RabbitMQ"); //交换机名、队列名、消息属性、字节内容 channel.basicPublish("", "no1", null, msg.getBytes()); System.out.println("已发送:" + msg); //资源释放 channel.close(); connection.close(); } }
-
消费者:
public class Recer { public static void main(String[] args) throws Exception { //获取连接 Connection connection = ConnectionUtil.getConnection(); //创建信道 Channel channel = connection.createChannel(); //从信道中获取消息 DefaultConsumer consumer = new DefaultConsumer(channel) { @Override //交付处理(收件人信息,包裹上的快递标签,协议的配置,消息) public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println(new String(body));//body为消息内容 } }; //监听队列 ACK机制 true自动确认 channel.basicConsume("no1", true, consumer); } }
2. 工作队列模式
- 多个消费者共同处理消息。
-
这里生产者发布100条消息,不同消费者处理速度不同。
-
生产者:
//省略获取连接、创建信道、声明队列、与关闭资源
for (int i = 0; i < 100; i++) {
String msg = new String("hello " + i);
//发布100条消息到no1队列上
channel.basicPublish("", "no1", null, msg.getBytes());
System.out.println("已发送:" + msg);
}
-
消费者(两台消费者):
- 在两个消费者中定义了静态变量I计数。
- 消费者中也声明队列,在此处的作用是获取!
- 尽管不同的消费者处理速度不同,但RebbitMQ并不清楚,如果要做到按效率分配,需要指定::channel.basicQos(1);并配合手动确认。
public class Recer1 { static int i = 1; // 记录执行次数 public static void main(String[] args) throws Exception { //获取队列 channel.queueDeclare("no1", false, false, false, null); //指定分配方式(按效率来) channel.basicQos(1); //手动确认 hannel.basicAck(envelope.getDeliveryTag(), false); //手动确认 channel.basicConsume("no1", false, consumer); } }
避免消息堆积:
- 如上述方式,多消费者监听同一队列(no1)
- 消费者处理消息时,使用多线程异步消费。
3.发布订阅模式(fanout)
-
声明路由关键字:fanout
-
P为生产者、红色为具体队列、bind为绑定。
-
X是路由:将生产者发布的信息,绑定到不同的队列。
-
运行过程:启动生产者(创建路由)、启动消费者、再次启动生产者。
- 生产者:
- 声明路由
public class sender {
public static void main(String[] args) throws Exception {
//声明路由
/*fanout:
不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该
路由绑定的所有队列上)*/
channel.exchangeDeclare("exchange","fanout");
String msg="hello 消息订阅模式";
channel.basicPublish("exchange","",null,msg.getBytes());
}
}
- 消费者:
- 声明队列,将队列绑定至路由
//获取连接、创建信道
//声明队列
channel.queueDeclare("no3", false, false, false, null);
/*
参数1:队列名
参数2:交换器名称
参数3:路由键(暂时无用,""即可)
*/
//将消息队列绑定路由
channel.queueBind("no3", "exchange", "");
4.路由模式(direct)
-
声明路由关键字:direct。
-
根据路由键,定向分发消息给不同的队列。
- 生产者:
//声明路由
// direct:根据路由键进行定向分发消息
channel.exchangeDeclare("exchange_direct","direct");
String msg="hello 路由模式";
channel.basicPublish("exchange_direct","delete",null,msg.getBytes());
- 消费者1:
channel.queueDeclare("no1",false,false,false,null);
//绑定路由.路由键是delete绑定到队列1
channel.queueBind("no1","exchange_direct","delete");
- 消费者2:
channel.queueDeclare("no2",false,false,false,null);
//绑定路由.路由键是insert绑定到队列1
channel.queueBind("no2","exchange_direct","insert");
- 一样的,运行要先创建路由。
5.通配符模式(topic)
-
声明路由关键字:topic
-
路由键支持模糊匹配:
*:一个字符
#:N
6.持久化
-
在消费者未确认前、未消费前。防止信息丢失。
-
路由和队列均持久化
-
生产者:
channel.exchangeDeclare("exchange_topic","topic",true);//路由持久化 String msg="hello 通配符模式"; //参数3:消息持久化 channel.basicPublish("exchange_topic","user.select", MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
-
消费者:
channel.queueDeclare("no5",true,false,false,null); //绑定路由 路由键:user.开头 channel.queueBind("no5","exchange_topic","user.#");
2.3 Spring整合Rabbit
1. 生产者Spring.xml
- 配置连接
<!--1.配置连接-->
<rabbit:connection-factory
id="connectionFactory"
host="IP"
port="5672"
username="zz"
password="123456"
virtual-host="/lagou"
publisher-confirms="true"//:启动生产者确认机制
/>
- 配置队列、队列管理、路由(队列绑定)
<!--2.配置队列-->
<rabbit:queue name="queue1"/>
<!--3.对队列进行管理-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--4.配置路由 将队列绑定到交换机-->
<rabbit:topic-exchange name="spring_topic_exchange">
<rabbit:bindings>
<rabbit:binding queue="queue1" pattern="msg.#" ></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!-- 5. 配置消息对象json转换类 -->
<bean id="jsonMessageConverter"
class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>
- Rabbit模板对象、消息确认机制
<!--6.配置RabbitTemplate-->
<rabbit:template
id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"
/>
2. 生产者发送消息
public class Sender {
public static void main(String[] args) {
//加载配置文件
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
//从容器中获得模板对象
RabbitTemplate template = context.getBean(RabbitTemplate.class);
//消息发送
HashMap<String, String> map = new HashMap();
map.put("name", "张三");
map.put("age", "18");
//转换并发送
template.convertAndSend("msg.user", map);
context.close();
}
}
3.消费者Spring.xml
-
配置连接、配置队列、配置队列管理。前三步骤与生产端一致。
-
注解扫描、配置监听、
<!--4.注解扫描-->
<context:component-scan base-package="listener"/>
<!--5.配置监听-->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener ref="consumerListener" queue-names="queue1"/>
</rabbit:listener-container>
4. 消费者接收消息
- 实现MessageListene接口,并重写onMessage()方法,方法内对消息处理。
public class ConsumerListener implements MessageListener {
//json装换类 序列化与反序列化 使用最多的类,用来转换json的
private static final ObjectMapper MAPPER = new ObjectMapper();
public void onMessage(Message message) {
try {
//message->json
JsonNode jsonNode = MAPPER.readTree(message.getBody());
String name = jsonNode.get("name").asText();
String age = jsonNode.get("age").asText();
System.out.println("获取到:" + name + age);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 项目启动,启动Spring配置文件,并用System.in.read();命令防止程序结束
2.4 消息确认机制
- 原始的信道事务方式,使用
channel.txSelect(): 开启事务
channel.txCommit() :提交事务
channel.txRollback() :回滚事务
命令控制。在Spring的封装下,使用Confirm机制。
1. Spring.xml
<!--配置连接时指定确认机制-->
<rabbit:connection-factory
publisher-confirms="true"
/>
<!--配置rabbitmq的模版,添加确认回调处理类-->
<rabbit:template
confirm-callback="msgSendConfirmCallback"
/>
<!--7.消息确认实体类-->
<bean class="confirm.MsgSendConfirm" id="msgSendConfirmCallback"/>
2.确认处理类
@Component
public class MsgSendConfirm implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
/*
correlationData 消息id
b 确认状态
s 失败原因
*/
if (b) {
System.out.println("确认成功!");
} else {
System.out.println("发送失败!");
}
}
}
2.5 消费端限流
- 注意:对生产端限流是不科学的,应该对消费端限流,用于保持消费端的稳定
1.Spring.xml
- prefetch:一次性消费的消息数量。未确认前不往队列中派送消息。
<!--5.配置监听-->
<!-- prefetch="3" 一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于N 个消息,一旦有 N 个消息还没有ack,则该 consumer 将阻塞,直到消息被ack-->
<!-- acknowledge-mode: manual 手动确认-->
<rabbit:listener-container connection-factory="connectionFactory"
prefetch="3" acknowledge="manual">
<rabbit:listener ref="consumerListener" queuenames="test_spring_queue_1" />
</rabbit:listener-container>
2.6 过期时间TTL
- TTL:time to live。还能活多久?
1. 设置队列
- 创建队列时指定过期时间
<!--队列中设置过期时间-->
<rabbit:queue name="queue_ttl" auto-declare="true"> <rabbit:queue-arguments>
<entry key="x-message-ttl" value-type="long" value="5000"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
2. 设置消息
- 在创建消息时指定
RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
// 创建消息配置对象
MessageProperties messageProperties = new MessageProperties();
// 设置消息过期时间
messageProperties.setExpiration("6000");
// 创建消息
Message message = new Message("6秒后自动删除".getBytes(),
messageProperties);
// 发送消息
rabbitTemplate.convertAndSend("msg.user", message);
- 同时设置队列、消息的过期时间时,小值生效。
2.7 死信队列
- DLX(Dead Letter Exchanges)死信交换机。
- 未被及时消费的消息分配到DLX死信交换机中。
- 未被及时消费的原因
- 消息被拒绝
- 消息超时未被消费
- 达到队列最大长度。
1.Spring.xml
- 配置连接、配置模板对象、创建定向交换机、设置超时队列、创建死信交换机、死信队列
<!--1.配置连接-->
<rabbit:connection-factory
id="connectionFactory"
host="IP"
port="5672"
username="zz"
password="123456"
virtual-host="/lagou"
publisher-confirms="true"
/>
<rabbit:admin connection-factory="connectionFactory"/>
<!--6.配置RabbitTemplate-->
<rabbit:template
id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="exchange1"
/>
<!--定向交换机(测试死信队列)-->
<rabbit:direct-exchange name="exchange1">
<rabbit:bindings>
<rabbit:binding key="dlx_ttl" queue="ttl_quque"/>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--设置超时队列-->
<rabbit:queue name="ttl_quque">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value-type="long" value="5000" />
<entry key="x-dead-letter-exchange" value="dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--死信交换机-->
<rabbit:direct-exchange name="dlx_exchange">
<rabbit:bindings>
<rabbit:binding key="dlx_ttl" queue="qlx_quque"/>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--死信队列-->
<rabbit:queue name="qlx_quque"/>
2.9 延迟队列
- 延迟队列:过期+死信。
<!-- 监听死信队列 -->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3"
acknowledge="manual">
<rabbit:listener ref="consumerListener" queue-names="dlx_queue" />
</rabbit:listener-container>
3.Rabbit集群
3.1 RabbitMQ安装启动
# erlang
rpm -ivh erlang-21.3.8.16-1.el7.x86_64.rpm
# socat
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
# RabbitMQ
rpm -ivh rabbitmq-server-3.8.6-1.el7.noarch.rpm
#启动管理插件
rabbitmq-plugins enable rabbitmq_management
# 启动RabbitMQ
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service
3.2 测试
# 创建账号
rabbitmqctl add_user laosun 123456
# 设置用户角色
rabbitmqctl set_user_tags laosun administrator
# 设置用户权限
[root@localhost opt]# rabbitmqctl set_permissions -p "/" laosun ".*" ".*" ".*"
- 端口:5672(客户端连接) 15672(网页管理端) 25672(集群)
3.3 集群搭建
- 修改 /etc/hosts 映射文件
127.0.0.1 name1 localhost localhost.localdomain localhost4
localhost4.localdomain4
::1 nam1 localhost localhost.localdomain localhost6
localhost6.localdomain6
192.168.204.141 nam1
192.168.204.142 name2
- name1、name2对应各服务器的主机名
- 拷贝.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie 192.168.204.142:/var/lib/rabbitmq
# 重启服务器
reboot
- 加入集群
# 启动rabbitmq服务
systemctl start rabbitmq-server
# 加入集群节点
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@A
rabbitmqctl start_app
# 查看节点状态
rabbitmqctl cluster_status
- 搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是 不能用的。
- 在新集群中重新添加用户。
3.4 镜像模式
rabbitmqctl set_policy xall "^" '{"ha-mode":"all"}'
- xall: 策略名,自定义
- pattern:队列的匹配模式。^:任意队列
ost.localdomain localhost6
localhost6.localdomain6
192.168.204.141 nam1
192.168.204.142 name2
+ name1、name2对应各服务器的主机名
2. 拷贝.erlang.cookie
```cmd
scp /var/lib/rabbitmq/.erlang.cookie 192.168.204.142:/var/lib/rabbitmq
# 重启服务器
reboot
- 加入集群
# 启动rabbitmq服务
systemctl start rabbitmq-server
# 加入集群节点
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@A
rabbitmqctl start_app
# 查看节点状态
rabbitmqctl cluster_status
- 搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是 不能用的。
- 在新集群中重新添加用户。
3.4 镜像模式
rabbitmqctl set_policy xall "^" '{"ha-mode":"all"}'
- xall: 策略名,自定义
- pattern:队列的匹配模式。^:任意队列
- ‘{“ha-mode”:“all”}’:高可用模式,all:任意节点