你用支付宝给商家支付,如果是个仔细的人,会考虑我转账的话,会不会把我的钱扣了,商家没有收到我的钱?
一般我们使用支付宝或微信转账支付的时候,都是扫码,支付,然后立刻得到结果,说你支付了多少钱,如果你绑定的是银行卡,可能这个时候你并没有收到支付的确认消息。往往是在一段时间之后,你会收到银行卡发来的短信,告诉你支付的信息。
支付平台如何保证这笔帐不出问题?
支付平台必须保证数据正确性,保证数据并发安全性,保证数据最终一致性。
支付平台通过如下几种方式保证数据一致性:
这个比较容易理解,就是在操作某条数据时先锁定,可以用redis或zookeeper等常用框架来实现。 比如我们在修改账单时,先锁定该账单,如果该账单有并发操作,后面的操作只能等待上一个操作的锁释放后再依次执行。
优点:能够保证数据强一致性。 缺点:高并发场景下可能有性能问题。
消息队列是为了保证最终一致性,我们需要确保消息队列有ack机制 客户端收到消息并消费处理完成后,客户端发送ack消息给消息中间件 如果消息中间件超过指定时间还没收到ack消息,则定时去重发消息。比如我们在用户充值完成后,会发送充值消息给账户系统,账户系统再去更改账户余额。
优点:异步、高并发 缺点:有一定延时、数据弱一致性,并且必须能够确保该业务操作肯定能够成功完成,不可能失败。
我们可以从以下几方面来保证消息的可靠性:
先执行行业务操作,业务操作成功后执行行消息发送,消息发送过程通过try catch 方式捕获异常,在异常处理理的代码块中执行行回滚业务操作或者执行行重发操作等。这是一种最大努力确保的方式,并无法保证100%绝对可靠,因为这里没有异常并不代表消息就一定投递成功。
另外,可以通过spring.rabbitmq.template.retry.enabled=true 配置开启发送端的重试
RabbitMQ后来引入了一种轻量量级的方式,叫发送方确认(publisher confirm)机制。生产者将信道设置成confirm(确认)模式,一旦信道进入confirm 模式,所有在该信道上⾯面发布的消息都会被指派一个唯一的ID(从1 开始),一旦消息被投递到所有匹配的队列之后(如果消息和队列是持久化的,那么确认消息会在消息持久化后发出),RabbitMQ 就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这样生产者就知道消息已经正确送达了。
RabbitMQ 回传给生产者的确认消息中的deliveryTag 字段包含了确认消息的序号,另外,通过设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息是否都已经得到了处理了。生产者投递消息后并不需要一直阻塞着,可以继续投递下一条消息并通过回调方式处理理ACK响应。如果 RabbitMQ 因为自身内部错误导致消息丢失等异常情况发生,就会响应一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理理该 nack 命令。
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeoutException;
public class PublisherConfirmsProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:[email protected]:5672/%2f");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 向RabbitMQ服务器发送AMQP命令,将当前通道标记为发送方确认通道
AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
channel.queueDeclare("queue.pc", true, false, false, null);
channel.exchangeDeclare("ex.pc", "direct", true, false, null);
channel.queueBind("queue.pc", "ex.pc", "key.pc");
// 发送消息
channel.basicPublish("ex.pc", "key.pc", null, "hell world".getBytes());
// 同步的方式等待RabbitMQ的确认消息
try {
channel.waitForConfirmsOrDie(5_000); // 加上下划线是对数字进行分隔,提高代码的可读性
System.out.println("发送的消息已经得到被确认");
} catch (IOException e) {
e.printStackTrace();
System.err.println("消息被拒绝!");
} catch (IllegalStateException e) {
e.printStackTrace();
System.err.println("在不是Publisher Confirms的通道上使用该方法");
} catch (TimeoutException e) {
e.printStackTrace();
System.err.println("等待消息确认超时!");
}
channel.close();
connection.close();
}
}
waitForConfirm方法有个重载的,可以自定义timeout超时时间,超时后会抛TimeoutException。类似的有几个waitForConfirmsOrDie方法,Broker端在返回nack(Basic.Nack)之后该方法会抛出java.io.IOException。需要根据异常类型来做区别处理理, TimeoutException超时是属于第三状态(无法确定成功还是失败),而返回Basic.Nack抛出IOException这种是明确的失败。上面的代码主要只是演示confirm机制,实际上还是同步阻塞模式的,性能并不不是太好。
实际上,我们也可以通过“批处理”的方式来改善整体的性能(即批量量发送消息后仅调用一次waitForConfirms方法)。正常情况下这种批量处理的方式效率会高很多,但是如果发生了超时或者nack(失败)后那就需要批量量重发消息或者通知上游业务批量回滚(因为我们只知道这个批次中有消息没投递成功,而并不知道具体是那条消息投递失败了,所以很难针对性处理),如此看来,批量重发消息肯定会造成部分消息重复。另外,我们可以通过异步回调的方式来处理Broker的响应。addConfirmListener 方法可以添加ConfirmListener 这个回调接口,这个 ConfirmListener 接口包含两个方法:handleAck 和handleNack,分别用来处理 RabbitMQ 回传的 Basic.Ack 和 Basic.Nack。
原生API案例
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherConfirmsProducer2 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:[email protected]:5672/%2f");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 向RabbitMQ服务器发送AMQP命令,将当前通道标记为发送方确认通道
AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
channel.queueDeclare("queue.pc", true, false, false, null);
channel.exchangeDeclare("ex.pc", "direct", true, false, null);
channel.queueBind("queue.pc", "ex.pc", "key.pc");
String message = "hello-";
// 批处理的大小
int batchSize = 10;
// 用于对需要等待确认消息的计数
int outstandingConfirms = 0;
for (int i = 0; i < 103; i++) {
// 发送消息
channel.basicPublish("ex.pc", "key.pc", null, (message + i).getBytes());
outstandingConfirms++;
if (outstandingConfirms == batchSize) {
// 此时已经有一个批次的消息需要同步等待broker的确认
// 同步等待
channel.waitForConfirmsOrDie(5_000); // 加上下划线是对数字进行分隔,提高代码的可读性
outstandingConfirms = 0;
System.out.println("消息已经被确认了");
}
}
if (outstandingConfirms > 0) {
channel.waitForConfirmsOrDie(5_000); // 加上下划线是对数字进行分隔,提高代码的可读性
System.out.println("剩余的消息已经被确认了");
}
channel.close();
connection.close();
}
}
还可以使用回调方法:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
public class PublisherConfirmsProducer3 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:[email protected]:5672/%2f");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 向RabbitMQ服务器发送AMQP命令,将当前通道标记为发送方确认通道
AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
channel.queueDeclare("queue.pc", true, false, false, null);
channel.exchangeDeclare("ex.pc", "direct", true, false, null);
channel.queueBind("queue.pc", "ex.pc", "key.pc");
ConcurrentNavigableMap outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback clearOutStandingConfirms = (deliveryTag, multiple) -> {
if (multiple) {
System.out.println("编号小于等于" + deliveryTag + "的消息都已经被确认了");
// headMap(T toKey)方法会返回一个key值小于给定的值的一个map集合。如果改变初始集合,这个改动也会映射到headMap中。
ConcurrentNavigableMap headMap = outstandingConfirms.headMap(deliveryTag, true);
// 清空outstandingConfirms中已经被确认的消息信息
headMap.clear();
} else {
// 移除已经被确认的消息
outstandingConfirms.remove(deliveryTag);
System.out.println("编号为:" + deliveryTag + "的消息被确认");
}
};
/**
* 参数一:数被确认的回调方法
* 参数二:是不被确认时的回调方法
*/
channel.addConfirmListener(clearOutStandingConfirms, (deliveryTag, multiple) -> {
if (multiple) {
ConcurrentNavigableMap headMap = outstandingConfirms.headMap(deliveryTag, true);
System.out.println("消息编号小于等于 " + deliveryTag + " 的消息都不确认");
} else {
System.out.println(deliveryTag + " 对应的消息 不确认");
outstandingConfirms.remove(deliveryTag);
}
});
String message = "hello-";
for (int i = 0; i < 1000; i++) {
// 获取下一条即将发送消息的消息ID
long nextPublishSeqNo = channel.getNextPublishSeqNo();
// 发送消息
channel.basicPublish("ex.pc", "key.pc", null, (message + i).getBytes());
System.out.println("编号为:"+ nextPublishSeqNo+"的消息已经发送成功。尚未确认");
outstandingConfirms.put(nextPublishSeqNo,(message + i));
}
// 线程休眠,等待消息被确认
Thread.sleep(10000);
channel.close();
connection.close();
}
}
springboot案例
(1)pom.xml添加依赖
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-web
(2)application.properties添加RabbitMQ配置信息
spring.application.name=publisherconfirm
spring.rabbitmq.host=node1
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
spring.rabbitmq.port=5672
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
(3)主入口类
package com.lagou.rabbitmq.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitmqDemo {
public static void main(String[] args) {
SpringApplication.run(RabbitmqDemo13.class, args);
}
}
(4)RabbitConfig类
package com.lagou.rabbitmq.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public Queue queue() {
Queue queue = new Queue("q.biz", false, false, false, null);
return queue;
}
@Bean
public Exchange exchange() {
Exchange exchange = new DirectExchange("ex.biz", false, false, null);
return exchange;
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with("biz").noargs();
}
}
(5)BizController类
package com.lagou.rabbitmq.demo.controller;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
@RestController
public class BizController {
private RabbitTemplate rabbitTemplate;
@Autowired
public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
this.rabbitTemplate.setConfirmCallback((correlationData, flag, cause) -> {
if (flag) {
try {
System.out.println("消息确认:" +
correlationData.getId() + " "
+ new
String(correlationData.getReturnedMessage().getBody(), "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} else {
System.out.println(cause);
}
});
}
@RequestMapping("/biz")
public String doBiz() throws UnsupportedEncodingException {
MessageProperties props = new MessageProperties();
props.setCorrelationId("1234");
props.setConsumerTag("msg1");
props.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
props.setContentEncoding("utf-8");
// props.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); // 1
// props.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 2
CorrelationData cd = new CorrelationData();
cd.setId("msg1");
cd.setReturnedMessage(new Message("这是msg1的响应".getBytes("utf-8"), null));
Message message = new Message("这是等待确认的消息".getBytes("utf-8"), props);
rabbitTemplate.convertAndSend("ex.biz", "biz", message, cd);
return "ok";
}
@RequestMapping("/bizfalse")
public String doBizFalse() throws UnsupportedEncodingException {
MessageProperties props = new MessageProperties();
props.setCorrelationId("1234");
props.setConsumerTag("msg1");
props.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
props.setContentEncoding("utf-8");
Message message = new Message("这是等待确认的消息".getBytes("utf-8"), props);
rabbitTemplate.convertAndSend("ex.bizFalse", "biz", message);
return "ok";
}
}
(6)结果
持久化是提高RabbitMQ可靠性的基础,否则当RabbitMQ遇到异常时(如:重启、断电、停机等)数据将会丢失。主要从以下几个方面来保障消息的持久性:
RabbitMQ中的持久化消息都需要写入磁盘(当系统内存不不足时,非持久化的消息也会被刷盘处理理),这些处理理动作都是在“持久层”中完成的。持久层是一个逻辑上的概念,实际包含两个部分:
下图中,$RABBITMQ_HOME/var/lib/mnesia/rabbit@$HOSTNAME/msg_stores/vhosts/$VHostId 这个路路径下包含 queues、msg_store_persistent、msg_store_transient 这 3 个目录,这是实际存储消息的位置。其中queues目录中保存着rabbit_queue_index相关的数据,而msg_store_persistent保存着持久化消息数据,msg_store_transient保存着⾮非持久化相关的数据。
另外,RabbitMQ通过配置queue_index_embed_msgs_below可以根据消息大小决定存储位置,默认queue_index_embed_msgs_below是4096字节(包含消息体、属性及headers),小于该值的消息存在rabbit_queue_index中。
如何保证消息被消费者成功消费?
这也是我们之前一直在讲的“最终一致性”、“可恢复性” 的基础。
一般而言,我们有如下处理手段:
原生api方式
package com.lagou.rabbitmq.demo;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeoutException;
public class MyConsumer{
public static void main(String[] args) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException, IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:[email protected]:5672/%2f");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue.ca",false,false,false,null);
// 拉消息模式
/* GetResponse getResponse = channel.basicGet("queue.ca", false);
channel.basicReject(getResponse.getEnvelope().getDeliveryTag(), true); */
// 推消息模式
// autoAck:false表示手动确认消息
channel.basicConsume("queue.ca",false,"myConsumer",new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println(new String(body));
// 确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
// 参数一:消息标签;参数二:表示不确认多个消息还是一个消息;参数三:表示不确认的消息是否要重新放入队列,然后重发
channel.basicNack(envelope.getDeliveryTag(),false,true); // 可以用于拒收多条消息
// 用于拒收一条消息
// 参数二:对于不确认的消息,是否要重新入列,然后重发
channel.basicReject(envelope.getDeliveryTag(), true);
}
});
channel.close();
connection.close();
}
}
spring监听器方式
/**
* NONE模式,则只要收到消息后就立即确认(消息出列,标记已消费),有丢失数据的风险
* AUTO模式,看情况确认,如果此时消费者抛出异常则消息会返回到队列中
* MANUAL模式,需要显式的调用当前channel的basicAck方法
*
* @param channel
* @param deliveryTag
* @param message
*/
@RabbitListener(queues = "lagou.topic.queue", ackMode = "AUTO")
public void handleMessageTopic(Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, @Payload byte[] message) {
System.out.println("RabbitListener消费消息,消息内容:" + new String((message)));
try {
// 手动ack,deliveryTag表示消息的唯一标志,multiple表示是否是批量确认
channel.basicAck(deliveryTag, false);
// 手动nack,告诉broker消费者处理失败,最后一个参数表示是否需要将消息重新入列
channel.basicNack(deliveryTag, false, true);
// 手动拒绝消息。第二个参数表示是否重新入列
channel.basicReject(deliveryTag, true);
} catch (IOException e) {
e.printStackTrace();
}
}
上面是通过在消费端直接配置指定ackMode,在一些比较老的spring项目中一般是通过xml方式去定义、声明和配置的,不管是XML还是注解,相关配置、属性这些其实都是大同小异,触类旁通。然后需要注意的是channel.basicAck这几个手工Ack确认的方法。
SpringBoot项目中支持如下的一些配置:
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=5
#是否开启消费者重试(为false时关闭消费者重试,意思不是“不重试”,而是一直收到消息直到jack确认或者一直到超时)
spring.rabbitmq.listener.simple.retry.enabled=true
#重试间隔时间(单位毫秒)
spring.rabbitmq.listener.simple.retry.initial-interval=5000
# 重试超过最大次数后是否拒绝
spring.rabbitmq.listener.simple.default-requeue-rejected=false
#ack模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
本小节的内容总结起来就如图所示,本质上就是“请求/应答”确认模式
springboot完整案例
(1)pom.xml
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-web
(2)application.properties
spring.application.name=consumer_ack
spring.rabbitmq.host=node1
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
spring.rabbitmq.port=5672
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=5
#是否开启消费者重试(为false时关闭消费者重试,
# 意思不是“不重试”,而是一直收到消息直到jack确认或者一直到超时)
spring.rabbitmq.listener.simple.retry.enabled=true
#重试间隔时间(单位毫秒)
spring.rabbitmq.listener.simple.retry.initial-interval=5000
# 重试超过最大次数后是否拒绝
spring.rabbitmq.listener.simple.default-requeue-rejected=false
#ack模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
(3)主入口类
package com.lagou.rabbitmq.demo;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class RabbitmqDemo {
@Autowired
private RabbitTemplate rabbitTemplate;
public static void main(String[] args) {
SpringApplication.run(RabbitmqDemo.class, args);
}
@Bean
public ApplicationRunner runner() {
return args -> {
Thread.sleep(5000);
for (int i = 0; i < 10; i++) {
MessageProperties props = new MessageProperties();
props.setDeliveryTag(i);
Message message = new Message(("消息:" + i).getBytes("utf-8"), props);
// this.rabbitTemplate.convertAndSend("ex.biz", "biz", "消息:" + i);
this.rabbitTemplate.convertAndSend("ex.biz", "biz", message);
}
};
}
}
(4)RabbitConfig
package com.lagou.rabbitmq.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public Queue queue() {
return new Queue("q.biz", false, false, false, null);
}
@Bean
public Exchange exchange() {
return new DirectExchange("ex.biz", false, false, null);
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with("biz").noargs();
}
}
(5)MessageListener
package com.lagou.rabbitmq.demo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Random;
//@Component
public class MessageListener {
private Random random = new Random();
/**
* NONE模式,则只要收到消息后就立即确认(消息出列,标记已消费),有丢失数据的风险
* AUTO模式,看情况确认,如果此时消费者抛出异常则消息会返回到队列中
* MANUAL模式,需要显式的调用当前channel的basicAck方法
*
* @param channel
* @param deliveryTag
* @param message
*/
// @RabbitListener(queues = "q.biz", ackMode = "AUTO")
@RabbitListener(queues = "q.biz", ackMode = "MANUAL")
// @RabbitListener(queues = "q.biz", ackMode = "NONE")
public void handleMessageTopic(Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@Payload String message) {
System.out.println("RabbitListener消费消息,消息内容:" + message);
try {
if (random.nextInt(10) % 3 != 0) {
// 手动nack,告诉broker消费者处理失败,最后一个参数表示是否需要将消息重新入列
// channel.basicNack(deliveryTag, false, true);
// 手动拒绝消息。第二个参数表示是否重新入列
channel.basicReject(deliveryTag, true);
} else {
// 手动ack,deliveryTag表示消息的唯一标志,multiple表示是否是批量确认
channel.basicAck(deliveryTag, false);
System.err.println("已确认消息:" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
(6)BizController
package com.lagou.rabbitmq.demo.controller;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.GetResponse;
import org.springframework.amqp.rabbit.core.ChannelCallback;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@RestController
public class BizController {
@Autowired
private RabbitTemplate rabbitTemplate;
private Random random = new Random();
@RequestMapping("/biz")
public String getBizMessage() {
String message = rabbitTemplate.execute(new ChannelCallback() {
@Override
public String doInRabbit(Channel channel) throws Exception {
final GetResponse getResponse =
channel.basicGet("q.biz", false);
if (getResponse == null) return "你已消费完所有的消息";
String message = new String(getResponse.getBody(), "utf-8");
if (random.nextInt(10) % 3 == 0) {
channel.basicAck(getResponse.getEnvelope().getDeliveryTag(), false);
return "已确认的消息:" + message;
} else {
// 拒收一条消息
channel.basicReject(getResponse.getEnvelope().getDeliveryTag(), true);
// 可以拒收多条消息
channel.basicNack(getResponse.getEnvelope().getDeliveryTag(), false, true);
return "拒绝的消息:" + message;
}
}
});
return message;
}
}
在电商的秒杀活动中,活动一开始会有大量并发写请求到达服务端,需要对消息进行削峰处理,如何削峰?
当消息投递速度远快于消费速度时,随着时间积累就会出现“消息积压”。消息中间件本身是具备一定的缓冲能力的,但这个能力是有容量限制的,如果长期运行并没有任何处理,最终会导致Broker崩溃,而分布式系统的故障往往会发生上下游传递,连锁反应那就会很悲剧...
下面我将从多个角度介绍QoS与限流,防止上面的悲剧发生。
在/etc/rabbitmq/rabbitmq.conf中配置磁盘可用空间大小:
(1)执行channel.basicConsume方法之前通过 channel.basicQoS方法可以设置该数量。消息的发送是异步的,消息的确认也是异步的。在消费者消费慢的时候,可以设置Qos的prefetchCount,它表示broker在向消费者发送消息的时候,一旦发送了prefetchCount个消息而没有一个消息确认的时候,就停止发送。消费者确认一个,broker就发送一个,确认两个就发送两个。换句话说,消费者确认多少,broker就发送多少,消费者等待处理的个数永远限制在prefetchCount个。
(2)如果对于每个消息都发送确认,增加了网络流量,此时可以批量确认消息。如果设置了multiple为true,消费者在确认的时候,比如说id是8的消息确认了,则在8之前的所有消息都确认了。package com.lagou.rabbitmq.demo; import com.rabbitmq.client.*; import java.io.IOException; import java.net.URISyntaxException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.concurrent.TimeoutException; public class MyConsumer { public static void main(String[] args) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException, IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setUri("amqp://root:[email protected]:5672/%2f"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare("queue.qos", false, false, false, null); // 表示Qos是10个消息,最多有10个消息等待确认 channel.basicQos(10); // 表示最多10个消息等待确认。如果参数二设置为true,则表示只要使用当前的channel的Consumer,该设置都生效,false则表示仅对当前的Consumer生效 channel.basicQos(10, false); // 参数一表示未确认消息的大小,Rabbit没有实现,不用管 channel.basicQos(1000, 10, true); channel.basicConsume("queue.qos", false, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // some code going on // 可以批量处理消息,减少每个消息都发送确认消息带来的网络流量负载 channel.basicAck(envelope.getDeliveryTag(), true); } }); channel.close(); connection.close(); } }
生产者往往是希望自己产生的消息能快速投递出去,而当消息投递太快且超过了下游的消费速度时就容易出现消息积压/堆积,所以,从上游来讲我们应该在生产端应用程序中也可以加入限流、应急开关等控制手段,避免超过Broker端的极限承载能力或者压垮下游消费者。
再看看下游,我们期望下游消费端能尽快消费完消息,而且还要防止瞬时大量消息压垮消费端(推模式),我们期望消费端处理速度是最快、最稳定而且还相对均匀(比较理想化)。
提升下游应用的吞吐量和缩短消费过程的耗时,优化主要以下几种方式:
@Bean
public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
// SimpleRabbitListenerContainerFactory发现消息中有content_type有text就会默认将其
// 转换为String类型的,没有content_type都按byte[]类型
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 设置并发线程数
factory.setConcurrentConsumers(10);
// 设置最大并发线程数
factory.setMaxConcurrentConsumers(20);
return factory;
}
消息可靠传输一般是业务系统接入消息中间件时首要考虑的问题,一般消息中间件的消息传输保障分为三个层级:
RabbitMQ 支持其中的“最多一次”和“最少一次”。
其中“最少一次”投递实现需要考虑以下这个几个方面的内容:
“最多一次”的方式就无须考虑以上那些方面,生产者随意发送,消费者随意消费,不过这样很难确
保消息不会丢失。
“恰好一次”是RabbitMQ 目前无法保障的。
刚刚我们讲到,追求高性能就无法保证消息的顺序,而追求可靠性那么就可能产生重复消息,从而导致重复消费...真是应证了那句老话:做架构就是权衡取舍。
RabbitMQ层面有实现“去重机制”来保证“恰好一次”吗?答案是并没有。而且这个在目前主流的消息中间件都没有实现。
借用淘宝沈洵的一句话:最好的解决办法就是不去解决。当为了在基础的分布式中间件中实现某种相对不太通用的功能,需要牺牲到性能、可靠性、扩展性时,并且会额外增加很多复杂度,最简单的办法就是交给业务自己去处理。事实证明,很多业务场景下是可以容忍重复消息的。例如:操作日志收集,而对一些金融类的业务则要求比较严苛。
一般解决重复消息的办法是,在消费端让我们消费消息的操作具备幂等性。
业界对于幂等性的一些常见做法:
对于接口请求类的幂等性保证要相对更复杂,我们通常要求上游请求时传递一个类GUID的请求号(或TOKEN),如果我们发现已经存在了并且上一次请求处理结果是成功状态的(有时候上游的重试请求是正常诉求,我们不能将上一次异常/失败的处理结果返回或者直接提示“请求异常”,如果这样重试就变得没意义了)则不继续往下执行,直接返回“重复请求”的提示和上次的处理结果(上游通常是由于请求超时等未知情况才发起重试的,所以直接返回上次请求的处理结果就好了)。如果请求ID都不存在或者上次处理结果是失败/异常的,那就继续处理流程,并最终记录最终的处理结果。这个请求序号由上游自己生成,上游通用需要根据请求参数、时间间隔等因子来生成请求ID。同样也需要利用这个请求ID做分布式锁的KEY实现排他。