一、RabbitMq发送消息与接收消息的大致步骤
生产者
- 创建连接工厂ConnectionFactory并设置连接属性
- 通过工厂创建连接Connection connection = connectionFactory.newConnection();
- 通过连接创建channel Channel channel = connection.createChannel();
- 然后发送消息到交换机中 channel.basicPublish(exchangeName, routingKey, basicProperties, msg.getBytes());
消费者
- 监听队列 channel.basicConsume(queueName, false, new MyConsumer(channel)); false表示关闭自动签收,需要我们ask确认。
- 消息确认 channel.basicAck(envelope.getDeliveryTag(),false);
二、交换机类型
- direct直连交换机
发送到direct类型中的消息会被投递到routing_key完全相同的绑定队列中 - Topic
利用通配符进行绑定交换机和队列*表示一个字段,#表示多个字段 - 扇形交换机
交换机直接绑定到队列,不需要路由key进行绑定分发、
三、自定义消费者
自定义消费者就很简单了,需要我们实现DefaultConsumer接口重写handleDelivery()方法。
/**
* Convenience class providing a default implementation of {@link Consumer}.
* We anticipate that most Consumer implementations will subclass this class.
*/
public class DefaultConsumer implements Consumer {
/** Channel that this consumer is associated with. */
private final Channel _channel;
/** Consumer tag for this consumer. */
private volatile String _consumerTag;
/**
* Constructs a new instance and records its association to the passed-in channel.
* @param channel the channel to which this consumer is attached
*/
public DefaultConsumer(Channel channel) {
_channel = channel;
}
/**
* No-op implementation of {@link Consumer#handleDelivery}.
*/
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
// no work to do
}
}
##############################
使用的话就是
api: String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
生产者接收消息方式
channel.basicConsume(queueName, false, new AckConsumer(channel));
四、死信队列 x-dead-letter-exchange
定义:队列中的消息没有被消费者ack确认,这个消息就是一个死消息,那这个消息就会重现发送另一个死信队列中。
具体的实现如下:
ap<String,Object> queueArgs = new HashMap<>();
//正常队列上绑定死信队列
queueArgs.put("x-dead-letter-exchange",dlxExhcangeName);
queueArgs.put("x-max-length",4);
channel.queueDeclare(nomalqueueName,true,false,false,queueArgs);
channel.queueBind(nomalqueueName,nomalExchangeName,routingKey);
不消息确认不签收
channel.basicNack(envelope.getDeliveryTag(),false,false);
- 可以看到过期的消息自动分发到我们的死信队列中了。
五、延迟队列
延迟队列的使用场景
- 支付项目中的自动取消支付呀
- 半小时之后闹钟啊
- 肯定比每秒定时去数据库中查询的效率高吧,并且数据量大了你的效率就更低了。
- 实现逻辑:给消息设置一个失效时间,然后监听死信队列。
生产者
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
//30秒过期后就回发送到死信队列中
.expiration("30000")
.build();
//生产者发送消息
channel.basicPublish(exchangeName,routingKey,basicProperties,message.getBytes());
消费者
channel.basicConsume(queueName, false, new MyDleConsumer(channel));
六、消息的限流
作用:当我们的一个服务发送的消息特别多时,如果大量的消息发送到我们的消费者,那么我们的消费者就会很大的压力,甚至导致服务器的宕机,所以我们做消息的限流。
其实注释也给我们解释的很清楚了。
/**
* Request specific "quality of service" settings.
*
* These settings impose limits on the amount of data the server
* will deliver to consumers before requiring acknowledgements.
* Thus they provide a means of consumer-initiated flow control.
* @see com.rabbitmq.client.AMQP.Basic.Qos
* @param prefetchSize maximum amount of content (measured in
* octets) that the server will deliver, 0 if unlimited
* @param prefetchCount maximum number of messages that the server
* will deliver, 0 if unlimited
* @param global true if the settings should be applied to the
* entire channel rather than each consumer
* @throws java.io.IOException if an error is encountered
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
使用方式
-参数1 prefetchSize如果没有限制,则为0
-参数2 prefetchCount服务器上的最大消息数
channel.basicQos(0,1,false);
七、消息确认(ConfirmListener )
Confirm确认机制 |
这段注释也写的很明白了
/**
* Implement this interface in order to be notified of Confirm events.
* Acks represent messages handled successfully; Nacks represent
* messages lost by the broker. Note, the lost messages could still
* have been delivered to consumers, but the broker cannot guarantee
* this.
*/
public interface ConfirmListener {
//消息的正常确认
void handleAck(long deliveryTag, boolean multiple)
throws IOException;
//消息的否定确认
void handleNack(long deliveryTag, boolean multiple)
throws IOException;
}
步骤1. 开启消息确认模式
channel.confirmSelect();
步骤2. 开启消息确认监听
- 使用方式
channel.addConfirmListener(new TestConfirmListener());
八、消息的幂等性
为什么重发: 项目实践中为了让消息百分百发送成功我们一般都使用重发消息,例如由于网络问题,我们都会将这个消息重新发送一次。
重返导致问题: 会出现被消费者多次消费。
解决办法: 需要我们保证消息的幂等性。
- 对每一个消息加一个全局唯一id,每次重发的时候判断是否已存在,不存在就保存到数据库中。
- 消费者端可以利用redis的setnx来保证幂等性。
九、消息发送接收的伪代码
Map<String, Object> map = new HashMap<>();
map.put("name", "luoli");
//定义消息的属性 ·是否持久化 ·编码格式 ·唯一id ·消息头信息 ·失效时间
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2) //2为持久化,1 不持久化
.contentEncoding("UTF-8") //编码格式
.correlationId(UUID.randomUUID().toString())
.headers(map) //头信息,消费端可以获取到
.expiration("10000") //失效时间
.build();
channel.basicPublish(exchangeName, routingKey, basicProperties, msg.getBytes());
//自定义消费者
public class MyConsumer extends DefaultConsumer {
@Override
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException {
// name = luoli
Sring name= (Sring) properties.getHeaders().get("name");
channel.basicAck(envelope.getDeliveryTag(),false);
}
//消息的ack确认 ,当开启消息确认后,队列中的每个消息都需要被ack消费确认后才能继续消费
channel.basicAck(envelope.getDeliveryTag(),false);
//channel限流 0:不限制消息大小, 1:每次推送多少条消息
channel.basicQos(0,1,false);
十、ReturnListener 不可达消息
- channel.basicPublish(exchangeName,okRoutingKey,true,basicProperties,msg.getBytes());
- 当交换机错误或不存在,或在根据路由键不能找到队列,对应的消息就会成为死消息
- 当mandatory设置true,mq会调用ReturnListener中的handleReturn(),当设置为false mq会将这种消息自动删除。
十一、spring整合rabbitmq
//最基本的一个配置
@Configuration
public class RabbitmqConfig {
/**
* 创建连接工厂
* @return
*/
@Bean
public ConnectionFactory connectionFactory () {
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
cachingConnectionFactory.setAddresses("ip:port");
cachingConnectionFactory.setVirtualHost("luoLi-mq");
cachingConnectionFactory.setUsername("guest");
cachingConnectionFactory.setPassword("guest");
cachingConnectionFactory.setConnectionTimeout(100000);
cachingConnectionFactory.setCloseTimeout(100000);
return cachingConnectionFactory;
}
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setReceiveTimeout(50000);
return rabbitTemplate;
}
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
//spring容器启动加载该类
rabbitAdmin.setAutoStartup(true);
return rabbitAdmin;
}
//配置消息监听容器
@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer() {
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(connectionFactory());
//设置监听的队列
simpleMessageListenerContainer.setQueues(testTopicQueue(),testDirectQueue(),testTopicQueue2(),orderQueue(),addressQueue(),fileQueue());
//设置当前消费者者数量
simpleMessageListenerContainer.setConcurrentConsumers(5);
//最大消费者个数5
simpleMessageListenerContainer.setMaxConcurrentConsumers(10);
//设置签收模式 -自动签收
simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
//设置拒绝重回队列- 不允许重回队列
simpleMessageListenerContainer.setDefaultRequeueRejected(false);
}
}
//创建消息监听适配器对象
//自己创建一个消息委托对象
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new LuoLiMsgDelegate());
simpleMessageListenerContainer.setMessageListener(messageListenerAdapter);
因为默认的消费方法是 handleMessage,所以消息委托对象LuoLiMsgDelegate中需要配置handleMessage方法
public void handleMessage(String msgBody) {
System.out.println("MsgDelegate。。。。。。handleMessage" + msgBody);
}
发送消息
rabbitTemplate.convertAndSend("luoli.exchange","routing.key","luoli hello");
springboot整合rabbitmq
@RabbitListener(queues = {ORDER_TO_PRODUCT_QUEUE_NAME})
@RabbitHandler
public void consumerMsg(Message message, Channel channel) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
MessageDto messageDto= objectMapper.readValue(message.getBody(), MessageDto.class);
Long deliveryTag = (Long) message.getMessageProperties().getDeliveryTag();
//消息确认
channel.basicAck(deliveryTag,false);
//处理业务逻辑
System.out.println(msgTxtBo.getMsgId());
}
十二、RabbitMq实现分布式事务
- 用户下单后会生成一个订单和一个支付消息保存到数据库中,状态为0 位支付
- 开启定时任务,每2s去消息表中查询未支付的消息
- 支付成功了的将跟新消息状态生成发货的数据
- 完成消息的ACK确认
为什么要开启一个定时任务呢? |
业务中支付模块和消费者不是在同一个服务中的,如果网络原因导致支付服务的支付失败,但是这个消息还是会被ACK,那么这条消息就会被直接消费掉,导致用户无法继续支付。我们只用当用户支付成功后该消息才应该被正常ACK。
如果用户支付时间较长那么就会生成多个消息,会导致用户重新支付。那又怎么办呢? |
可以利用redis实现一个分布式锁redisTemplate.opsForValue().setIfAbsent(),利用每个支付消息的唯一id。
注意点,消费者监听到第一个消息的时候这个消息就已经保存到redis中了,如果支付失败也会导致用户无法再次支付,因为已经redis中已经存在唯一消息的key了,所以当支付失败的时候,我们需要将这个唯一消息key从redis中删除掉。这样用户才能继续的支付。