目录
一、异步调用
以订单支付功能为例:

- 性能更高。对于更新订单状态、短信通知等服务,不是支付功能所必要的环节,如果全部由请求线程执行,那么耗时是所有服务的和。
- 故障隔离,提高支付成功概率。支付服务应以尽量卖出商品为主,同步方案中如果更新订单状态失败那么会回滚将余额退还用户,这对于支付服务(尽可能卖出商品的理念)来说是不利的,将更新订单状态置为异步后由消息队列控制订单状态一定能被更新成功。
- 解耦合,实现对扩展开放、对修改关闭。对于扣减余额(必须同步调用)环节后要增加的功能,可以同时监听消息队列,不用修改原代码。
- 缓存信息,实现流量削峰填谷。消息队列一般结合线程池使用,线程池的线程以固定的流量读取消息并写库,写库流量稳定无凸峰。
二、整体架构

- 生产者向交换机发送消息 ,需要指定目标交换机1:1。
- 交换机向队列发送消息 ,主要是起到将消息发送给多个不同队列的功能,需要指定交换机和队列的关系m:n。
- 消费者从队列取消息 ,需要指定目标队列1:1。
- 虚拟主机起到数据隔离的作用,类似于mysql不同的database
- 消费者从消息队列获取消息,如果队列绑定多个消费者,那么消费者采用轮询 的方式获取消息,不管消费者有没有处理完上一条已获取的消息。在配置文件中设置
prefetch: 1规定消费者处理完当前消息后才能继续获取消息。
三、消息收发
1.生产者-交换机
生产者与交换机绑定,一对一关系,生产者对于队列不可见。
1.1 Fanout交换机
交换机会将接收到的消息广播到每一个跟其绑定的queue。
- 生产者基于Spring AMQP 发送消息:
java
import org.springframework.amqp.rabbit.core.RabbitTemplate;
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 交换机名称
String exchangeName = "simple.queue";
// 消息内容
String message = "hello, spring amqp!";
// 发送消息到队列
rabbitTemplate.convertAndSend(exchangeName, message);
}
}
1.2 Direct交换机
交换机与每个queue约定一个或多 个bindingKey,不同queue的bindingKey可重复 ,生产者向交换机发送消息时指定bindingKey,交换机将消息发送给bindingKey对应的队列集合。
- 生产者基于Spring AMQP 发送消息:
java
import org.springframework.amqp.rabbit.core.RabbitTemplate;
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 交换机名称
String exchangeName = "simple.queue";
// 交换机与队列的约定
String bindingKey = "red";
// 消息内容
String message = "hello, spring amqp!";
// 发送消息到队列
rabbitTemplate.convertAndSend(exchangeName, bindingKey, message);
}
}
1.3 Topic交换机
与Direct交换机相同,但是Topic交换机的bindingKey支持通配符,#指代0~n个单词、*指代1个单词。
2.队列-消费者
一个消费者与一个queue绑定,多个消费者可以绑定同一个queue,消费者对于交换机不可见。
- 基于Spring AMQP 接收消息:
java
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.print(msg);
}
}
3.交换机-队列
3.1 创建交换机和队列
基于配置类
java
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
// 声明FanoutExchange交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("hmall.fanout");
}
// 声明队列
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
// 绑定交换机和队列
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
基于注解
直接在消费者端配置,因为消费者直接绑定队列value ,所以可以直接创建队列;又因为队列和交换机绑定,所以可以创建交换机exchange 并配置交换机和队列的关系key。
java
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到Direct消息:【" + msg + "】");
}
四、消息转换器
AMQP默认使用SimpleMessageConverter消息转换器将对象进行JDK序列化发送到RabbitMQ,JDK序列化后的对象非常占用存储空间。
AMQP还提供了Jackson2JsonMessageConverter消息转换器,会将对象进行Json序列化 ,可读性和空间占用更小,但需要手动导入对象到IOC容器,AMQP识别到该对象会自动替换默认消息转换器。(AMQP是Spring对RabbitMQ操作的封装,原始操作RabbitMQ非常繁琐)
java
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
五、生产者可靠性
1.连接超时重连
在SpringBoot配置文件中配置:
yml
rabbitmq:
host:
port:
virtual-host:
username:
password:
connection-timeout: 1s # 连接等待时长
template:
retry:
enabled: true # 是否重连
multiplier: 2s # 每次重连修改等待时长为connection-timeout*multiplier
max-attempts: 3 # 重试次数
2.消息确认
- 消息投递到MQ服务器,且入队持久化成功才会返回ACK。
- 其他情况会返回NACK,此时需要重发消息。
yml
rabbitmq:
publisher-confirm-type: correlated # MQ以异步方式返回消息,simple是同步阻塞等待MQ返回消息
获取消息发送结果:
java
@Slf4j
public class PublisherConfirmTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testPublisherConfirm() throws InterruptedException {
// 消息的唯一标识
CorrelationData cd = new CorrelationData();
// 2. 给FutureTask添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onSuccess(CorrelationData.Confirm result) {
if (result.isAck()) { // true代表ack回执,false代表nack回执
log.debug("发送消息成功,收到 ack!");
} else { // getReason()返回nack时的异常描述
log.error("发送消息失败,收到 nack,reason : {}", result.getReason());
}
}
});
// 发送消息
rabbitTemplate.convertAndSend("hmall.direct", "red1", "hello", cd);
}
}
六、MQ可靠性
MQ采用惰性队列的方式,接收到消息直接存入磁盘,消费者消费时将消息读到内存缓冲区,内存缓冲区只缓存2048条消息。
七、消费者可靠性
- 消费者返回ACK,MQ从队列中删除该消息
- 消费者返回NACK,MQ保留消息
- 消费者返回Reject,MQ从队列中删除该消息
SpringAMQP提供了三种消息返回的方式,可以配置acknowledge-mode实现:
none:消费者获取消息后立即自动返回ACK。manual:需要手动发送ack或nack。auto:Spring基于AOP做了环绕通知,业务执行成功返回ACK,业务出现异常返回NACK,Message类型异常返回Reject。
默认情况下如果返回 NACK那么会重复执行 消费者的@RabbitListener业务逻辑,直到返回ACK,可以通过如下配置设置重试上限:
yml
rabbitmq:
listener:
simple:
retry:
enabled: true # 消费者返回NACK是否重新处理消息
multiplier: 2s # 每次重试修改等待时长为connection-timeout*multiplier
max-attempts: 3 # 重试次数
stateless: true
当超过重试上限的处理方式:
RejectAndDontRequeueRecoverer:重试耗尽后,返回reject,丢弃消息。ImmediateRequeueMessageRecoverer:重试耗尽后,返回 nack,消息重新入队,重新开始重试循环RepublishMessageRecoverer:重试次数耗尽后,将失败消息投递到指定的交换机,此时可以人工查看该交换机的内容进行人工处理。
八、幂等性
幂等f(x) = f(f(x)),即同一段逻辑执行一次与执行多次的结果是一致的。
可靠性:消息都能被消费
幂等性:消息仅能被消费一次
1.唯一消息ID/主键
每个消息建立唯一id,消费者接收到消息后处理业务,处理完成后将id保存到数据库,每次取到消息先查数据库判断是否存在,存在则为重复消息放弃处理。
2.乐观锁/版本号法
基于业务本身如订单状态判断是否重复修改订单状态,如果订单状态为已支付那么放弃更新数据库直接return(订单表有支付时间所以是不幂等的)。