关于什么是消息中间件以及好处,可以参考博主之前的文章(SpringBoot整合ActiveMQ),这里就不做过多介绍 ~
一 . 简单整合
1. 添加RabbitMQ依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 配置application.yml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: test
password: 123456
# virtual-host: /
3. 新建RabbitMQConfig
package com.example.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
// 测试队列名称
private String testQueueName = "test_queue";
// 测试交换机名称
private String testExchangeName = "test_exchange";
// RoutingKey
private String testRoutingKey = "test_routing_key";
/** 创建队列 */
@Bean
public Queue testQueue() {
return new Queue(testQueueName);
}
/** 创建交换机 */
@Bean
public TopicExchange testExchange() {
return new TopicExchange(testExchangeName);
}
/** 通过routingKey把队列与交换机绑定起来 */
@Bean
public Binding testBinding() {
return BindingBuilder.bind(testQueue()).to(testExchange()).with(testRoutingKey);
}
}
4. 新建生产者(producer)
package com.example.producer;
import com.alibaba.fastjson.JSONObject;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class TestProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String queueName) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("email", "756840349@qq.com");
jsonObject.put("timestamp", System.currentTimeMillis());
String jsonString = jsonObject.toJSONString();
// 生产者发送消息的时候需要设置消息id
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)// 持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.build();
rabbitTemplate.convertAndSend(queueName, message);
}
}
5. 新建消费者(consumer)
package com.example.listener;
import com.alibaba.fastjson.JSONObject;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class FanoutSmsConsumer {
@RabbitListener(queues = "test_queue")
public void consumeMessage(Message message) throws Exception{
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(msg);
System.out.println("消费消息:" + jsonObject);
}
}
6. 编写controller测试
package com.example.controller;
import com.example.producer.FanoutProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProducerController {
@Autowired
private TestProducer TestProducer;
@RequestMapping("/sendMsg")
public String sendFanout() {
fanoutProducer.send("test_queue");
return "success";
}
}
浏览器访问localhost:8080/sendMsg,会返回success,并且控制台监听器会打印消息:
二 . 进阶场景
1. 消费者在消费消息的时候,如果消费者业务逻辑出现程序异常,这时候应该如何处理?
解决办法:使用消息重试机制(自带的特性,无需配置,只要出现异常就自动重试)
原理:@RabbitListener 底层使用AOP拦截,如果程序没有抛出异常,自动提交事务;如果抛出异常,则自动重试,重新消费消息
拓展①:如果一直报异常,不可能一直重试,这时候可以修改重试策略(在application.properties添加以下配置):
#开启消费者重试
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数(重试5次还不行则会把消息删掉,默认是不限次数的,次数建议控制在10次以内)
spring.rabbitmq.listener.simple.retry.max-attempts=5
#重试间隔时间
spring.rabbitmq.listener.simple.retry.initial-interval=3000
拓展②:如何合理选择重试机制?
消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试? (需要重试机制)
消费者获取到消息后,抛出数据转换异常,是否需要重试?(不需要重试机制,需要发布版本进行解决)
@Component
public class EamilConsumer {
@RabbitListener(queues = "femail_queue")
public void process(String msg) throws Exception {
JSONObject jsonObject = JSONObject.parseObject(msg);
String email = jsonObject.getString("email");
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因为网络原因,造成无法访问,继续重试
throw new Exception("调用接口失败!");
}
System.out.println("执行结束....");
}
}
2. RabbitMQ消息确认机制ack模式(默认为手动应答,这里讲解自动应答)
① application.properties配置:
# 手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
② 修改监听器代码(在请求参数加入"@Headers Map<String, Object> headers, Channel channel"):
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
System.out.println(Thread.currentThread().getName()
+ ",msg:" + new String(message.getBody(), "UTF-8")
+ ",messageId:" + message.getMessageProperties().getMessageId());
// 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收
channel.basicAck(deliveryTag, false);
}
}
或者:
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
System.out.println(Thread.currentThread().getName()
+ ",msg:" + new String(message.getBody(), "UTF-8")
+ ",messageId:" + message.getMessageProperties().getMessageId());
// 手动签收
basicNack(message, channel);
}
private void basicNack(Message message, Channel channel) throws IOException {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
3. 消费者如何保证消息幂等性,不被重复消费?
① 产生原因:网络延迟传输中,消费出现异常或者是消费延迟消费,会造成MQ进行重试补偿,重试过程中,可能造成重复消费
② 解决办法:1> 使用全局MessageId判断消费方使用同一个,解决幂等性 2> 或者使用业务逻辑保证唯一(比如订单号码)
生产者代码:
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
String uuid = UUID.randomUUID() + "";
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build();
//将messageId存入redis(在监听器判空,防止重复消费)
stringRedisTemplate.opsForValue().set("uuid ", uuid );
rabbitTemplate.convertAndSend(queueName, message);
}
消费者代码:
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(msg);
if(!stringRedisTemplate.hasKey("meaagaeId")){
return;//已经被消费
}
String email = jsonObject.getString("email");
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
//如果调用第三方邮件接口无法访问,如何实现自动重试?抛出异常即可
if (result == null) {
throw new Exception("调用第三方邮件服务器接口失败!");//如果走到这一行,则会自动重试
}
stringRedisTemplate.delete(messageId);//删除,实际开发中也可以设置为空,只需要在上面判空即可
//或者是走到这一行,写入数据库日志记录表,在上面if(StringUtils.equals。。中判断日志表中是否有记录即可
}
4. 如何确保生产者一定能将消息投递到MQ服务器?
答案:采用MQ生产者消息确认机制(参考博客:https://www.jianshu.com/p/86b8171e4be4)
① application.properties 配置:
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
② 生产者实现RabbitTemplate.ConfirmCallback接口
public class TestProducer implements RabbitTemplate.ConfirmCallback
③ 生产者代码如下:
package com.mayikt.pay.mq.producer;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class IntegralProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void send(JSONObject jsonObject) {
String jsonString = jsonObject.toJSONString();
String paymentId = jsonObject.getString("paymentId");//这里以支付交易号作为全局messageId
// 封装消息
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(paymentId).build();
// 构建回调返回的数据(消息id)
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(jsonString);
rabbitTemplate.convertAndSend("XXX_exchange_name", "XXXRoutingKey", message, correlationData);
}
// 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String jsonString = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
log.info(">>>使用MQ消息确认机制确保消息一定要投递到MQ中成功");
return;
}
JSONObject jsonObject = JSONObject.parseObject(jsonString);
// 生产者消息投递失败的话,采用递归重试机制
send(jsonObject);
log.info(">>>使用MQ消息确认机制投递到MQ中失败");
}
}
三. 实际案例(RabbitMQ解决分布式事务)
相信很多人都对接过支付宝/微信支付,那么在支付回调中,涉及到增加积分,你们是如何处理的?
这里我整理了一套近乎完美的解决方案,仅供大家参考,首先贴一段支付回调的核心代码:
前面的代码主要进行了验签和判断状态的操作,主要看红框中的代码:
如上所示,第2步为修改订单状态为已支付,第3步为增加积分(异步推送到服务器进行增加);
此时,有个场景,如果第2步改为已支付,第三步也增加了相应的积分,但走完第三步,程序出了个异常(这里模仿1/0),此时整个回调代码会进行回滚,即第2步会回滚为未支付状态,而此时已经增加了相应的积分(因为@Transactional注解底层是采用aop,整个方法的代码执行完后,才提交事务),数据造成了不一致,如何解决?
答案:采用补偿机制,再创建一个补偿消费者进行监听,如果未支付,修改为已支付。
原理:当需要增加积分的时候,会有两个队列:增加积分队列和补偿队列(专门负责把支付状态改为已支付)
这两个队列会同时与交换机绑定,相当于把消息会同时发给这两个队列,补偿队列主要负责:查一下订单状态是否为已支付,如果未支付,把状态改为已支付(安全起见可以主动调用第三方接口对账一下, 然后再改为已支付);两个监听器没有执行顺序。
废话不多说,直接上代码:
① 首先编写RabbitMQ配置文件:
package com.mayikt.pay.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class RabbitmqConfig {
// 添加积分队列
public static final String INTEGRAL_DIC_QUEUE = "integral_queue";
// 支付补偿队列
public static final String INTEGRAL_CREATE_QUEUE = "integral_create_queue";
// 积分交换机
private static final String INTEGRAL_EXCHANGE_NAME = "integral_exchange_name";
// 1.添加积分队列
@Bean
public Queue directIntegralDicQueue() {
return new Queue(INTEGRAL_DIC_QUEUE);
}
// 2.定义支付补偿队列
@Bean
public Queue directCreateintegralQueue() {
return new Queue(INTEGRAL_CREATE_QUEUE);
}
// 2.定义交换机
@Bean
DirectExchange directintegralExchange() {
return new DirectExchange(INTEGRAL_EXCHANGE_NAME);
}
// 3.积分队列与交换机绑定
@Bean
Binding bindingExchangeintegralDicQueue() {
return BindingBuilder.bind(directIntegralDicQueue()).to(directintegralExchange()).with("integralRoutingKey");
}
// 3.补偿队列与交换机绑定
@Bean
Binding bindingExchangeCreateintegral() {
return BindingBuilder.bind(directCreateintegralQueue()).to(directintegralExchange()).with("integralRoutingKey");
}
}
② 积分生产者
package com.mayikt.pay.mq.producer;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class IntegralProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void send(JSONObject jsonObject) {
String jsonString = jsonObject.toJSONString();
String paymentId = jsonObject.getString("paymentId");//这里以支付交易号作为全局messageId
// 封装消息
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(paymentId).build();
// 构建回调返回的数据(消息id)
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(jsonString);
rabbitTemplate.convertAndSend("integral_exchange_name", "integralRoutingKey", message, correlationData);
}
// 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String jsonString = correlationData.getId();
if (ack) {
log.info(">>>使用MQ消息确认机制确保消息一定要投递到MQ中成功");
return;
}
JSONObject jsonObject = JSONObject.parseObject(jsonString);
// 生产者消息投递失败的话,采用递归重试机制
send(jsonObject);
log.info(">>>使用MQ消息确认机制投递到MQ中失败");
}
}
③ 积分监听器
package com.mayikt.integral.consume;
import java.io.IOException;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.mayikt.integral.mapper.IntegralMapper;
import com.mayikt.integral.mapper.entity.IntegralEntity;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class IntegralConsumer {
@Autowired
private IntegralMapper integralMapper;
@RabbitListener(queues = "integral_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
try {// 程序出现异常才会重试,这里用try就为了不让mq重试,代码层层判断即可
String messageId = message.getMessageProperties().getMessageId();// //支付id,支付交易号
String msg = new String(message.getBody(), "UTF-8");
log.info(">>>messageId:{},msg:{}", messageId, msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
String paymentId = jsonObject.getString("paymentId");
if (StringUtils.isEmpty(paymentId)) {
log.error(">>>>支付id不能为空 paymentId:{}", paymentId);
basicNack(message, channel);// 既然支付id获取不到,一直重试也没有意义,所以直接手动ack,并return
return;
}
// 使用paymentId查询是否已经增加过积分 支付回调-网络重试间隔(所以不存在并发性问题)
IntegralEntity resultIntegralEntity = integralMapper.findIntegral(paymentId);
if (resultIntegralEntity != null) {
log.error(">>>>paymentId:{}已经增加过积分", paymentId);
// 已经增加过积分,通知MQ不要在继续重试,直接手动ack,通知mq删除该消息,并return,不必执行下面逻辑
basicNack(message, channel);
return;
}
Integer userId = jsonObject.getInteger("userId");
if (userId == null) {
log.error(">>>>paymentId:{},对应的用户userId参数为空", paymentId);
basicNack(message, channel);
return;
}
Long integral = jsonObject.getLong("integral");
if (integral == null) {
log.error(">>>>paymentId:{},对应的用户integral参数为空", integral);
return;
}
IntegralEntity integralEntity = new IntegralEntity();
integralEntity.setPaymentId(paymentId);
integralEntity.setIntegral(integral);
integralEntity.setUserId(userId);
integralEntity.setAvailability(1);
// 插入到数据库中(增加积分)
int insertIntegral = integralMapper.insertIntegral(integralEntity);
if (insertIntegral > 0) {
// 手动签收消息,通知mq服务器端删除该消息
basicNack(message, channel);
}
// 采用重试机制
} catch (Exception e) {
log.error(">>>>ERROR MSG:", e.getMessage());
basicNack(message, channel);
}
}
// 消费者获取到消息之后 手动签收 通知MQ删除该消息
private void basicNack(Message message, Channel channel) throws IOException {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
④ 补偿监听器
package com.mayikt.pay.consumer;
import java.io.IOException;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.mayikt.pay.constant.PayConstant;
import com.mayikt.pay.mapper.PaymentTransactionMapper;
import com.mayikt.pay.mapper.entity.PaymentTransactionEntity;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
/**
* 支付回调检查状态,是否为已经支付完成(补偿队列监听器)
*/
@Component
@Slf4j
public class PayCheckStateConsumer {
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
@RabbitListener(queues = "integral_create_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
try {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
log.info(">>>messageId:{},msg:{}", messageId, msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
String paymentId = jsonObject.getString("paymentId");
if (StringUtils.isEmpty(paymentId)) {
log.error(">>>>支付id不能为空 paymentId:{}", paymentId);
basicNack(message, channel);
return;
}
// 1.使用paymentId查询之前是否已经支付过
PaymentTransactionEntity paymentTransactionEntity = paymentTransactionMapper.selectByPaymentId(paymentId);
if (paymentTransactionEntity == null) {
log.error(">>>>支付id paymentId:{} 未查询到", paymentId);
basicNack(message, channel);
return;
}
Integer paymentStatus = paymentTransactionEntity.getPaymentStatus();
if (paymentStatus.equals(PayConstant.PAY_STATUS_SUCCESS)) {// 已支付状态
log.error(">>>>支付id paymentId:{} ", paymentId);
basicNack(message, channel);// 3.如果支付,则手动确认消息,通知mq删除该消息并return
return;
}
// 安全起见 可以主动调用第三方接口查询(这里省略了该步骤) --- 未支付
String paymentChannel = jsonObject.getString("paymentChannel");
int row = paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "",
paymentId, paymentChannel);// 2.如果不是已支付状态,则把状态改为已支付
if (row > 0) {
basicNack(message, channel);
return;
}
// 如果上面row>0,则ack,否则继续重试
} catch (Exception e) {
e.printStackTrace();
basicNack(message, channel);
}
}
private void basicNack(Message message, Channel channel) throws IOException {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
此时,即使增加了积分,回调回滚了,依然会走支付回调 - 补偿队列的监听器,修改订单状态为已支付,此时,由于第一次走回调会报1/0异常,之后支付宝/微信会走重试(第三方支付自带的特性),此时,走到下图红色箭头的时候会直接判断支付状态,由于补偿队列已经更改为已支付,所以不会继续往下执行,所有0/1最多只会执行一次。
小结:
RabbitMQ解决分布式事务原理:采用最终一致性原理。需要保证以下三要素:
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式,使用补偿机制(注意重试幂等性问题-全局id)
3、如果MQ异步代码执行之后,代码出现异常的情况下,如何保证分布式事务问题?采用补偿队列