目录
【*】Rabbit安装和启动教程
https://www.cnblogs.com/saryli/p/9729591.html
【*】RabbitMQ和kafka比较常用,并发能力比较强
【*】为什么要使用MQ
在网络通讯中,Http请求默认采用同步请求方式,在客户端与服务器进行通讯时,客户端调用服务端接口后,必须等待服务端完成处理后返回结果给客户端才能继续执行,这种情况属于同步调用方式,如果服务器端发生网络延迟、可能客户端也会受到影响,MQ采用的是异步方式,无需等待
【*】MQ解决的问题
- 应用解耦
- 异步消息
- 流量削峰
【*】RabbitMQ相关内容
- 基于Erlang语言开发的,支持AMQP协议
【*】监控界面地址
http://127.0.0.1:15672
【*】添加用户
【*】Virtual Host
- VirtualHost相当于一个相对独立的RabbitMQ服务器
- 每个VirtualHost之间是相互隔离的。
- exchange(交换器)、queue(队列)、message(消息)不能互通。
- VirtualHost相当于mysql中的不同数据库,不同的VirtualHost,不同的业务
【*】自动应答和手动应答
- 自动应答
- 消息队列将消息发送给消费者
- 消息就会从队列中删除
- 消费者不能完全处理消息,或者宕机,消息丢失
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
- 手动应答(ACK)
- 消息发送给消费者
- 消费者接受处理完消息
- 消费者应答RabbitMQ,MQ将消息从消息队列删除
- 消费者没有应答RabbitMQ,MQ将消息发送给其他消费者
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
【*】交换机的四种类型
- Fanout exchange(扇型交换机)将消息路由给绑定到它身上的所有队列
- Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的
- Topic exchange(主题交换机)队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列
- Headers exchange(头交换机)类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。(不经常使用)
【*】五种工作模式
1.点对点(简单)的队列:
- 连接RabbitMQ的工具类
public class MQConnectionUtils {
public static Connection newConnection() throws IOException, TimeoutException {
/** 1.定义连接工厂 */
ConnectionFactory factory = new ConnectionFactory();
/** 2.设置服务器地址 */
factory.setHost("127.0.0.1");
/** 3.设置协议端口号 */
factory.setPort(5672);
/** 4.设置vhost */
factory.setVirtualHost("test001_host");
/** 5.设置用户名称 */
factory.setUsername("guest");
/** 6.设置用户密码 */
factory.setPassword("guest");
/** 7.创建新的连接 */
Connection newConnection = factory.newConnection();
return newConnection;
}
}
- 创建生产者
public class Customer {
/** 队列名称 */
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("002");
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.获取通道 */
Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msgString);
}
};
/** 3.监听队列 */
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
- 创建消费者
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME = "lby_644064779";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("消费者启动....01");
// 1.创建一个新的连接
Connection connection = MQConnectionUtils.newConnection();
// 2.创建通道
final Channel channel = connection.createChannel();
// 3.消费者关联队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// 监听获取消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
} finally {
// 手动应答 模式 告诉给队列服务器 已经处理成功
// channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 4.设置应答模式 如果为true情况下 表示为自动应答模式 false 表示为手动应答
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
- 图示
- 功能描述:一个生产者 P 发送消息到队列 Q,一个消费者 C 接收
- P 表示为生产者
- C 表示为消费者
- 红色表示队列
总结
- 角色:生产者、队列、消费者
由于队列是由生产者创建的,我们就不需要单独创建了
- 创建连接工具类过程
- 创建连接工厂
- 设置服务器地址
- 设置协议端口号
- 设置vhost
- 设置用户名
- 设置密码
- 使用工厂创建连接
- 创建生产者过程
- 使用连接工具类创建连接
- 使用连接创建通道
- 使用通道调用队列声明
- 使用通道创建基础发布消息
- 关闭通道
- 关闭连接
- 创建消费者过程
- 使用连接工具获取连接
- 使用连接创建通道
- 监听获取消息
- 设置应答模式
2.工作(公平性)队列模式
- 循环队列(轮流分发)
- 消息队列向对各消费者发送消息,每个消费者按照顺序依次执行消息
- 公平队列(寻找空闲工作者进行分发)
- 消息队列向多个消费者发送消息
- 消费者对RabbitMQ产生ACK应答,MQ才会再次给这个队列发送消息
-
生产者
public class Producer2 {
/** 队列名称 */
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = newConnection.createChannel();
/**3.创建队列声明 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/**保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息 */
channel.basicQos(1);
for (int i = 1; i <= 50; i++) {
String msg = "生产者消息_" + i;
System.out.println("生产者发送消息:" + msg);
/**4.发送消息 */
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
channel.close();
newConnection.close();
}
}
- 消费者
public class Customer2_1 {
/**
* 队列名称
*/
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("001");
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.获取通道 */
final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息 */
channel.basicQos(1);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msgString);
try {
Thread.sleep(1000);
} catch (Exception e) {
} finally {
/** 手动回执消息 */
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
/** 3.监听队列 */
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
总结
- 在生产者和消费者上加如下代码,代表一次只分发一次消息, 限制发送给同一个消费者 不得超过一条消息
channel.basicQos(1);
3.发布订阅模式
- 基本原理
- 生产者将消息发送给交换机
- 交换机根据路由策略将消息发送给队列
- 队列将消息推送给消费者
- 注意事项
- 这里的交换机类型使用的是广播(fanout)
- 生产者只需要生产一条消息
- 交换机会将消息发送到所有的队列
- 生产者
public class ProducerFanout {
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = connection.createChannel();
/** 3.绑定的交换机 参数1交互机名称 参数2 exchange类型,第三个参数置为空时,可以接收到生产者所有的消息(生产者 routingKey 参数为空时) */
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/** 4.发送消息 */
for (int i = 0; i < 10; i++)
{
String message = "用户注册消息:" + i;
System.out.println("[send]:" + message);
//发送消息,其中第二个参数为空类似于表示全局广播,只要绑定到该队列上的消费者理论上是都可以收到的。
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("utf-8"));
try {
Thread.sleep(5 * i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** 5.关闭通道、连接 */
channel.close();
connection.close();
/** 注意:如果消费没有绑定交换机和队列,则消息会丢失 */
}
}
- 邮件消费者
public class ConsumerEmailFanout {
private static final String QUEUE_NAME = "consumerFanout_email";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("邮件消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
- 短信消费者
public class ConsumerSMSFanout {
private static final String QUEUE_NAME = "ConsumerFanout_sms";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
总结
- 生产者绑定交换机
- 消费者绑定交换机和消息队列
- 生产者绑定了交换机,但是消费者没有绑定消息或者消息队列,消息就会丢失
- 队列是会缓存的,就算消费者服务停掉了,但是生产者将消息发送之后,消息还是会缓存在队列中
- 但是队列和交换机解绑之后,就不存在缓存的功能了
4.路由模式Routing(选择性消费)
这种模式使用的交换机类型是:Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的
- 在发布订阅的基础上加了RoutingKey,从而实现选择性消费
- 生产者在发送消息给交换机的时候加上RoutingKey标识
- 不同功能消费者,根据对应的RoutingKey标识,将交换机和消息队列绑定在一起,从而实现选择性消费
- 图示
- 生产者
public class ProdecerRouting {
private static final String EXCHANGE_NAME = "my_fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = connection.createChannel();
/** 3.绑定的交换机 参数1交互机名称 参数2 exchange类型 */
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
/** 4.发送消息 */
String message = "",sendType="";
for (int i = 0; i < 10; i++)
{
if(i%2==0){
sendType = "info";
message = "我是 info 级别的消息类型:" + i;
}else{
sendType = "error";
message = "我是 error 级别的消息类型:" + i;
}
System.out.println("[send]:" + message + " " +sendType);
channel.basicPublish(EXCHANGE_NAME, sendType, null, message.getBytes("utf-8"));
try {
Thread.sleep(5 * i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** 5.关闭通道、连接 */
channel.close();
connection.close();
/** 注意:如果消费没有绑定交换机和队列,则消息会丢失 */
}
}
- Info消费者
public class ConsumerInfo {
private static final String QUEUE_NAME = "consumer_info";
private static final String EXCHANGE_NAME = "my_fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("info消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
- Error消费者
public class ConsumerError {
private static final String QUEUE_NAME = "consumer_error";
private static final String EXCHANGE_NAME = "my_fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("error消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
/*5.如果既想收到Info也想收到Error,那就再下面再加一个就OK了*/
// channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
总结
- 不同的路由可以帮定到相同的队列上
- 路由模式,所有角色之间的关系全是通过RoutingKey进行绑定的
5.通配符模式Topics
- *:只能匹配一个词
- #:可以匹配多个词
和路由的模式相似,其实就是路由模式的模糊匹配
- 生产者
public class ProducerTopic {
private static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = connection.createChannel();
/** 3.绑定的交换机 参数1交互机名称 参数2 exchange类型 */
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
/** 4.发送消息 */
String routingKey = "log.info.error";
String msg = "topic_exchange_msg:" + routingKey;
System.out.println("[send] = " + msg);
channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());
/** 5.关闭通道、连接 */
channel.close();
connection.close();
/** 注意:如果消费没有绑定交换机和队列,则消息会丢失 */
}
}
- *消费者
public class ConsumerLogXTopic {
private static final String QUEUE_NAME = "topic_consumer_info";
private static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("log * 消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "log.*");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
- #消费者
public class ConsumerLogJTopic {
private static final String QUEUE_NAME = "topic_consumer_info";
private static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("log # 消费者启动");
/* 1.创建新的连接 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.创建通道 */
Channel channel = connection.createChannel();
/* 3.消费者关联队列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消费者绑定交换机 参数1 队列 参数2交换机 参数3 routingKey */
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "log.#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
/* 5.消费者监听队列消息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
【*】RabbitMQ消息持久化
默认是不持久化的
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
设置为持久化(此处修改了第二个参数)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
【*】RabbitMQ消息确认机制(2种)
1.AMQP事务机制(生产段配置)
- txSelect():事务开启
- txCommit():事务提交
- 以及txRollback():事务回滚
生产者将消息发送出去之后,消息到底有没有到达 rabbitmq 服务器,默认情况下是不知道的,但是因为RabbitMQ的持久化机制,只要确认消息到达了Rabbit服务器就可以保证消息不会丢失
在try-catch中通过txSelect()开启事务,当channel.basicPublish向MQ服务器发送消息,使用txCommit()进行对事物提交,如果提交过程中发生任何问题导致事务提交失败,catch中就会执行txRollbacj事务回滚,默认情况下,是没办法判断生产者的消息是否到达MQ服务器,使用AMQP事务就可以保证消息提交到服务器上。
事务机制是一个同步的过程,效率相对较低,但是数据的一致性特别好。
public class Producer {
/** 队列名称 */
private static final String QUEUE_NAME = "lby_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = newConnection.createChannel();
/** 3.创建队列声明 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 4.发送消息 */
try {
/** 4.1 开启事务 */
channel.txSelect();
String msg = "我是生产者生成的消息";
System.out.println("生产者发送消息:"+msg);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
/** 4.2 提交事务 - 模拟异常 */
int i = 1/0;
channel.txCommit();
}catch (Exception e){
e.printStackTrace();
System.out.println("发生异常,我要进行事务回滚了!");
/** 4.3 事务回滚 */
channel.txRollback();
}finally {
channel.close();
newConnection.close();
}
}
}
2.Confirm模式(生产端配置)
生产端
生产者端 confirm 模式的实现原理
生产者将channel设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该channel上面发布的消息都会被指定一个唯一的 ID(从1开始),一旦消息被投递到所有的匹配队列之后,RabbitMQ就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出
- confirm 模式最大的好处在于它是异步的。
-
Confirm 的三种实现方式:
- channel.waitForConfirms() 普通发送方确认模式
- channel.waitForConfirmsOrDie() 批量确认模式
- channel.addConfirmListener() 异步监听发送方确认模式
- 普通消息发送
public class Producer11 {
/** 队列名称 */
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = newConnection.createChannel();
/** 3.创建队列声明 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 4.开启发送方确认模式 */
channel.confirmSelect();
/** 5.发送消息 */
for (int i = 0; i < 5; i++) {
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, (" Confirm模式, 第" + (i + 1) + "条消息").getBytes());
try {
if (channel.waitForConfirms()) {
System.out.println("发送成功");
}else{
System.out.println("进行消息重发");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** 5.关闭通道、连接 */
channel.close();
newConnection.close();
}
}
- 在推送消息之前,channel.confirmSelect() 声明开启发送方确认模式,再使用channel.waitForConfirms() 等待消息被服务器确认即可
- 批量消息发送
public class Producer22 {
/** 队列名称 */
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = newConnection.createChannel();
/** 3.创建队列声明 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 4.开启发送方确认模式 */
channel.confirmSelect();
/** 5.发送消息 */
for (int i = 0; i < 5; i++) {
channel.basicPublish("", QUEUE_NAME, null, (" Confirm模式, 第" + (i + 1) + "条消息").getBytes());
}
/** 6.直到所有信息都发布,只要有一个未确认就会IOException */
channel.waitForConfirmsOrDie();
System.out.println("全部执行完成");
/** 5.关闭通道、连接 */
channel.close();
newConnection.close();
}
}
-
异步Confirm模式
public class Producer33 {
/** 队列名称 */
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.获取连接 */
Connection newConnection = MQConnectionUtils.newConnection();
/** 2.创建通道 */
Channel channel = newConnection.createChannel();
/** 3.创建队列声明 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 4.开启发送方确认模式 */
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
String message = "我是生产者生成的消息:" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
}
/** 5.发送消息 异步监听确认和未确认的消息 */
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("未确认消息,标识:" + deliveryTag);
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println(String.format("已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
}
});
/** 6.关闭通道、连接 */
/** channel.close();*/
/** newConnection.close();*/
}
}
- 总结
- 异步模式的优点,就是执行效率高,不需要等待消息执行完,只需要监听消息即可,以上异步返回的信息如下
- 维持异步调用要求我们不能断掉连接
消费者
-
手动ACK
// 手动确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
// 关闭自动确认
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
自动应答的模式有一个非常大的缺点,就是消息过载,就是消息不断的发送给消费者,但是消费者处理消息的能力并不够,此时就会有大量未完成的消息堆积在内存中,到这操作被系统终止,这样在传送中的消息,就会丢失。
为了保证消息从队列可靠地到达消费者,RabbitMQ 提供消息确认机制,消费者在声明队列时,可以指定 noAck 参数,当 noAck=false 时, RabbitMQ 会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。如果RabbitMQ一直没有收到消费者ack信号的消息,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
-
RabbitMQ不会为未ack的消息设置超时时间
-
它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。
-
原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久
-
【*】SpringBoot整合RabbitMQ
- 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- springboot-web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
- 配置文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /
- 交换机绑定队列
@Component
public class FanoutConfig {
// 邮件队列
private String FANOUT_EMAIL_QUEUE = "fanout_eamil_queue";
// 短信队列
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
// 交换机
private String EXCHANGE_NAME = "fanoutExchange";
//将邮件队列注入IOC
@Bean
public Queue fanOutEamilQueue() {
return new Queue(FANOUT_EMAIL_QUEUE);
}
//将短信队列注入IOC
@Bean
public Queue fanOutSmsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
//将定义交换机注入IOC
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
//邮件队列与交换机绑定
@Bean
Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
}
// 4.短信队列与交换机绑定
@Bean
Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
}
}
- 生产者投递消息
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
String msg = "my_fanout_msg:" + new Date();
System.out.println(msg + ":" + msg);
amqpTemplate.convertAndSend(queueName, msg);
}
}
- 控制层
@RestController
public class ProducerController {
@Autowired
private FanoutProducer fanoutProducer;
@RequestMapping("/sendFanout")
public String sendFanout(String queueName) {
fanoutProducer.send(queueName);
return "success";
}
}
- 消费者依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- springboot-web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
- 消费者配置
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /
- 邮件消费者
@Component
@RabbitListener(queues = "fanout_eamil_queue")
public class FanoutEamilConsumer {
@RabbitHandler
public void process(String msg) throws Exception {
System.out.println("邮件消费者获取生产者消息msg:" + msg);
}
}
- 短信消费者
@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("短信消费者获取生产者消息msg:" + msg);
}
}
【*】RabbitMQ自动补偿机制
如果在消费者在处理消息的过程中出现了异常,RabbitMQ就会进行消息补偿,不断的将失败的消息重新入队列,进行发送
是队列服务器发送的补偿请求
- @RabbitListener:
- 底层使用的是AOP进行拦截,没有出现异常,会自动提交事务,告诉RabbitMQ服务器,已经成功处理请求,此时RabbitMQ就会将消息删除
- 如果出现异常,就会自动实现补偿机制,该消息就会缓存在RabbitMQ的服务器上进行存放
- 重试机制的策略:间隔时间5秒重试一次
- 配置文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_host
listener:
simple:
retry:
####开启消费者重试
enabled: true
####最大重试次数,超出这个次数就放弃这个消息,将这个消息删除
max-attempts: 5
####重试间隔次数
initial-interval: 3000
server:
port: 8081
【*】RabbitMQ重试机制需要注意的问题
- 消费者对消息进行处理的时候,程序出现异常,如何处理
----答:采用重试机制
- 如何选择重试机制
- 情况一:消费者获得消息后,调用第三方接口,第三方接口暂时无法访问
- 情况二:消费者获得消息后,程序报类型转换异常(代码异常)
对于情况一,问题可能是短暂的,可能因为网络延迟的问题,导致接口暂时无法访问,此时采用重试机制可能在重试一次,接口就可以使用了,对于情况二,属于代码问题,不修改代码,重试多少次都没用,没必要重试
- 具体对消息补偿方式:
- 生产者没有向RabbitMQ发送成功,或者消费者没有成功处理消息失败,则通过日志将这些消息记录下来
- 通过定时任务自动的在补偿一次消息
- 通过人工接口,手动的将消息再次发送
- 和其他公司进行通讯的时候,一般不适用MQ,因为有些MQ是不能够跨语言的,一般采用HTTP协议的方式
- HttpClient工具类
package com.itmayiedu.rabbitmq.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* HttpClient4.3工具类
*
* @author hang.luo
*/
public class HttpClientUtils {
private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日志记录
private static RequestConfig requestConfig = null;
static {
// 设置请求和传输超时时间
requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build();
}
/**
* post请求传输json参数
*
* @param url
* url地址
* @param json
* 参数
* @return
*/
public static JSONObject httpPost(String url, JSONObject jsonParam) {
// post请求返回结果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
// 设置请求和传输超时时间
httpPost.setConfig(requestConfig);
try {
if (null != jsonParam) {
// 解决中文乱码问题
StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 请求发送成功,并得到响应
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String str = "";
try {
// 读取服务器返回过来的json字符串数据
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(str);
} catch (Exception e) {
logger.error("post请求提交失败:" + url, e);
}
}
} catch (IOException e) {
logger.error("post请求提交失败:" + url, e);
} finally {
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* post请求传输String参数 例如:name=Jack&sex=1&type=2
* Content-type:application/x-www-form-urlencoded
*
* @param url
* url地址
* @param strParam
* 参数
* @return
*/
public static JSONObject httpPost(String url, String strParam) {
// post请求返回结果
CloseableHttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
try {
if (null != strParam) {
// 解决中文乱码问题
StringEntity entity = new StringEntity(strParam, "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/x-www-form-urlencoded");
httpPost.setEntity(entity);
}
CloseableHttpResponse result = httpClient.execute(httpPost);
// 请求发送成功,并得到响应
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String str = "";
try {
// 读取服务器返回过来的json字符串数据
str = EntityUtils.toString(result.getEntity(), "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(str);
} catch (Exception e) {
logger.error("post请求提交失败:" + url, e);
}
}
} catch (IOException e) {
logger.error("post请求提交失败:" + url, e);
} finally {
httpPost.releaseConnection();
}
return jsonResult;
}
/**
* 发送get请求
*
* @param url
* 路径
* @return
*/
public static JSONObject httpGet(String url) {
// get请求返回结果
JSONObject jsonResult = null;
CloseableHttpClient client = HttpClients.createDefault();
// 发送get请求
HttpGet request = new HttpGet(url);
request.setConfig(requestConfig);
try {
CloseableHttpResponse response = client.execute(request);
// 请求发送成功,并得到响应
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 读取服务器返回过来的json字符串数据
HttpEntity entity = response.getEntity();
String strResult = EntityUtils.toString(entity, "utf-8");
// 把json字符串转换成json对象
jsonResult = JSONObject.parseObject(strResult);
} else {
logger.error("get请求提交失败:" + url);
}
} catch (IOException e) {
logger.error("get请求提交失败:" + url, e);
} finally {
request.releaseConnection();
}
return jsonResult;
}
}
- 测试调用第三方接口失败情况
- 测试方式:访问第三方的接口服务器没有启动,当然就调用失败
- 测试结果:重复的向第三方接口发送消息,第三方接口启动成功,发送消息成功
- 发送消息代码
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(String msg) throws Exception {
System.out.println("邮件消费者获取生产者消息msg:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
String email = jsonObject.getString("email");
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
System.out.println("邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl);
JSONObject result = HttpClientUtils.httpGet(emailUrl);
// 如果调用第三方邮件接口无法访问,如何实现自动重试.
if (result == null) {
throw new Exception("调用第三方邮件服务器接口失败!");
}
System.out.println("邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束");
}
}
【*】MQ幂等性问题(重复消费)
- 原因
在网络延迟的情况下,消费者可能会产生延迟或者消费失败的情况,这样就会触发MQ的重试补偿机制,造成重复消费
- 解决
-
- 使用全局MessageID判断消费方使用同一个,解决幂等性。
- 或者使用业务逻辑保证唯一(比如订单号码)
- 生产者代码
-
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("email", "644064779");
jsonObject.put("timestamp", System.currentTimeMillis());
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
// 生产者发送消息的时候需要设置消息id
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build();
amqpTemplate.convertAndSend(queueName, message);
}
}
- 消费者代码
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
if(messageId == "已经存在"){
return;
}
// 重试机制都是间隔性,不会有并发的问题
JSONObject jsonObject = JSONObject.parseObject(msg);
String email = jsonObject.getString("email");
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
System.out.println("邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl);
JSONObject result = HttpClientUtils.httpGet(emailUrl);
// 如果调用第三方邮件接口无法访问,如何实现自动重试.
if (result == null) {
throw new Exception("调用第三方邮件服务器接口失败!");
}
System.out.println("邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束");
}
}
总结
思路:写一个日志,此方法走完代表消息消费成功了,就将对应MessageID的消息,加一个成功的标记,存放在日志中,当再次执行的时候,判断MessageID在成功消息日志中是否存在,如果存在直接return,不再继续执行
【*】SpringBoot整合RabbitMQ开启手动签收
- 再消费者配置文件开启应答模式
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_host
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 2000
####开启手动ack
acknowledge-mode: manual
- 消费者开启手动签收
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
// 重试机制都是间隔性
JSONObject jsonObject = JSONObject.parseObject(msg);
String email = jsonObject.getString("email");
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
System.out.println("邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl);
JSONObject result = HttpClientUtils.httpGet(emailUrl);
// 如果调用第三方邮件接口无法访问,如何实现自动重试.
if (result == null) {
throw new Exception("调用第三方邮件服务器接口失败!");
}
System.out.println("邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束");
// 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收,向RabbitMQ发送通知,这边已经消费成功了
channel.basicAck(deliveryTag, false);
}
}
【*】RabbitMQ死信队列
- 什么是死信队列
- 可以理解为备胎队列,和正常的队列在区别上不大,就是概念不同,当正常队列中的消息,不能顺利的被消费者消费情况下,这部分消息就会发送给死信队列
- 死信队列:包括死信交换机,死性队列,死定消费者
- 使用私信队列的情况:
- 死信队列:队列的长度大小有一定的限度,此时队列已满,交换机继续向队列发送消息,就会造成消息丢失
- 队列中的消息有过期时间,在规定时间内消息没有被消费,消息就会被删除
- 消费者拒绝执行消息,或因为代码异常执行消息失败,此时重试补偿也没有用
- 已经创建好的队列和交换机不能做更改,没效果,必须在网络上清空,然后再重新绑定
- 基本信息类
@Component
public class FanoutConfig {
//死信队列
public final static String deadQueueName = "dead_queue";
//死信交换机
public final static String deadExchangeName = "dead_exchange";
//私信队列和死信交换机的RoutingKey
public final static String deadRoutingKey = "dead_routing_key";
//系统参数:绑定当前队列和死信交换机使用
public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
//系统参数:通过RoutingKe绑定当前队列和死信交换机使用
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";
//短信队列
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
//普通交换机
private String EXCHANGE_NAME = "fanoutExchange";
// 1.定义邮件队列
@Bean
public Queue fanOutEamilQueue() {
// 将普通队列绑定到死信队列交换机上
Map<String, Object> args = new HashMap<>(2);
//声明当前队列绑定的属性交换机
args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
//声明当前队列的死信路由KEY
args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
Queue queue = new Queue(FANOUT_EMAIL_QUEUE, true, false, false, args);
return queue;
}
// 2.定义短信队列
@Bean
public Queue fanOutSmsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
/*死信队列*/
@Bean
public Queue deadQueue() {
Queue queue = new Queue(deadQueueName, true);
return queue;
}
// 2.普通交换机
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
/*死信交换机,使用的是路由模式*/
@Bean
public DirectExchange deadExchange() {
return new DirectExchange(deadExchangeName);
}
// 3.绑定邮件队列和交换机
@Bean
Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
}
// 4.绑定短信队列和交换机
@Bean
Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
}
/*绑定死信队列和死信交换机*/
@Bean
public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
}
}
- 生产者
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("email", "644064779");
jsonObject.put("timestamp", 0);
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
// 生产者发送消息的时候需要设置消息id
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build();
amqpTemplate.convertAndSend(queueName, message);
}
}
- 控制Controller类
@RestController
public class ProducerController {
@Autowired
private FanoutProducer fanoutProducer;
@RequestMapping("/sendFanout")
public String sendFanout(String queueName) {
fanoutProducer.send(queueName);
return "success";
}
}
- 消费者
// 默认是自动应答模式
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
Integer timestamp = jsonObject.getInteger("timestamp");
try {
int result = 1 / timestamp;
System.out.println("result:" + result);
// 通知mq服务器删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
//丢弃该消息,会发送给死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
- 死信消费者
//死信对接
@Component
public class DeadConsumer {
@RabbitListener(queues = "dead_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("死信邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
【*】RabbitMQ解决分布式事务
- 什么是分布式事务问题:
- 就是当完成的一个请求需要多个服务的情况下,需要保证事务的最终一致性(可以暂时不一致)
例如:在饿了么平台上订餐的过程,需要下单服务,派单服务等,当下单后,下单服务需要创建订单号,然后将订单号发送给派单服务,进行派单,所以此时订单表需要有当前订单,派单表也应该有这个订单的内容,但是可能由于一些意外原因导致,两个服务中的订单出现不一致状况,例如我下单了,但是派单表没我的订单,或者派单表有我的订单,但是订单表没有。此时就产生了分布式事务不一致的问题。
- 服务之间的通讯问题
- 并发量比较大:使用MQ进行通讯,MQ具备流量削峰的功能,比较适用于项目内部服务
- 并发量不大:使用HTTP协议进行通讯,可以在不同的项目的通讯中进行使用,因为其Socket可以跨语言
HTTP协议的话,订单服务可能承受不住大量的高并发,HTTP服务不能解决高并发问题,如果使用服务降级和熔断的话,可能会造成数据丢失
- 怎样解决分布式事务问题
- 需要保证以下三要素
- 确认生产者一定要将数据投递到MQ服务器中(采用MQ Confirm消息确认机制)
- MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性(重复消费)问题)
- 生产者的事务先执行
- 三要素解析
- 需要保证以下三要素
订单服务产生订单,将消息发送给MQ了,但是发送消息代码的下面还有业务逻辑,此时订单服务的业务逻辑没有执行完毕,且因为下面代码异常造成事务回滚,但是派单服务已经对消息进行消费了,此时就产生了订单服务由于事务回滚,没有差生订单,派单服务因为已经消费了消息,派单服务产生了对应该消息的订单号
此时可以采用补偿机制,在创建一个补单消费者进行监听,则进行补单。
- 生产者将消息投递到队列,消费者可以暂时处理失败,生产者不需要回滚事务,使用补偿机制,进行重试,补偿的过程中,注意幂等性问题
- 配置
@Component
public class RabbitmqConfig {
// 下单并且派单存队列
public static final String ORDER_DIC_QUEUE = "order_dic_queue";
// 补单队列,判断订单是否已经被创建
public static final String ORDER_CREATE_QUEUE = "order_create_queue";
// 下单并且派单交换机
private static final String ORDER_EXCHANGE_NAME = "order_exchange_name";
// 1.定义订单队列
@Bean
public Queue directOrderDicQueue() {
return new Queue(ORDER_DIC_QUEUE);
}
// 2.定义补订单队列
@Bean
public Queue directCreateOrderQueue() {
return new Queue(ORDER_CREATE_QUEUE);
}
// 2.定义交换机
@Bean
DirectExchange directOrderExchange() {
return new DirectExchange(ORDER_EXCHANGE_NAME);
}
// 3.订单队列与交换机绑定
@Bean
Binding bindingExchangeOrderDicQueue() {
return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
// 3.补单队列与交换机绑定
@Bean
Binding bindingExchangeCreateOrder() {
return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
}
- 生产者
@Service
public class OrderService extends BaseApiService implements RabbitTemplate.ConfirmCallback {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
public ResponseBase addOrderAndDispatch() {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setName("蚂蚁课堂永久会员充值");
orderEntity.setOrderCreatetime(new Date());
// 价格是300元
orderEntity.setOrderMoney(300d);
// 状态为 未支付
orderEntity.setOrderState(0);
Long commodityId = 30l;
// 商品id
orderEntity.setCommodityId(commodityId);
String orderId = UUID.randomUUID().toString();
orderEntity.setOrderId(orderId);
// ##################################################
// 1.先下单,创建订单 (往订单数据库中插入一条数据)
int orderResult = orderMapper.addOrder(orderEntity);
System.out.println("orderResult:" + orderResult);
if (orderResult <= 0) {
return setResultError("下单失败!");
}
// 2.使用消息中间件将参数存在派单队列中
send(orderId);
return setResultSuccess();
}
private void send(String orderId) {
JSONObject jsonObect = new JSONObject();
jsonObect.put("orderId", orderId);
String msg = jsonObect.toJSONString();
System.out.println("msg:" + msg);
// 封装消息
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(orderId).build();
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(orderId);
// 发送消息
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.convertAndSend("order_exchange_name", "orderRoutingKey", message, correlationData);
}
// 生产消息确认机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String orderId = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
System.out.println("消息发送确认成功");
} else {
send(orderId);
System.out.println("消息发送确认失败:" + cause);
}
}
}
- 补单消费者
@Component
public class CreateOrderConsumer {
@Autowired
private OrderMapper orderMapper;
@RabbitListener(queues = "order_create_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("补单消费者" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderId = jsonObject.getString("orderId");
// 判断订单是否存在,如果不存在 实现自动补单机制
OrderEntity orderEntityResult = orderMapper.findOrderId(orderId);
if (orderEntityResult != null) {
System.out.println("订单已经存在 无需补单 orderId:" + orderId);
return;
}
// 订单不存在 ,则需要进行补单
OrderEntity orderEntity = new OrderEntity();
orderEntity.setName("蚂蚁课堂永久会员充值");
orderEntity.setOrderCreatetime(new Date());
// 价格是300元
orderEntity.setOrderMoney(300d);
// 状态为 未支付
orderEntity.setOrderState(0);
Long commodityId = 30l;
// 商品id
orderEntity.setCommodityId(commodityId);
orderEntity.setOrderId(orderId);
// ##################################################
// 1.先下单,创建订单 (往订单数据库中插入一条数据)
try {
int orderResult = orderMapper.addOrder(orderEntity);
System.out.println("orderResult:" + orderResult);
if (orderResult >= 0) {
// 手动签收消息,通知mq服务器端删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
} catch (Exception e) {
// 丢弃该消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
-
派单消费者
@Component
public class DispatchConsumer {
@Autowired
private DispatchMapper dispatchMapper;
@RabbitListener(queues = "order_dic_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("派单服务平台" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderId = jsonObject.getString("orderId");
if (StringUtils.isEmpty(orderId)) {
// 日志记录
return;
}
DispatchEntity dispatchEntity = new DispatchEntity();
// 订单id
dispatchEntity.setOrderId(orderId);
// 外卖员id
dispatchEntity.setTakeoutUserId(12l);
// 外卖路线
dispatchEntity.setDispatchRoute("40,40");
try {
int insertDistribute = dispatchMapper.insertDistribute(dispatchEntity);
if (insertDistribute > 0) {
// 手动签收消息,通知mq服务器端删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
} catch (Exception e) {
e.printStackTrace();
// // 丢弃该消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
【注】此文内容,参照蚂蚁课堂余老师视频,在此特别感谢 http://www.mayikt.com/