目录
- Spring Boot整合RabbitMQ
- RabbitMQ保证消息100%消费
- 死信队列
- 最后
Spring Boot整合RabbitMQ
目录结构

spring:
rabbitmq:
host: 192.168.81.132
port: 5671
username: admin
password: admin
##开启生产端->>>Exchange的Confirm
publisher-confirms: true
##开启Exchange->>Queue的Confirm
publisher-returns: true
listener:
simple:
##设置手动ACK Queue ->消息端
acknowledge-mode: manual
## unack message的数量。在Channel里面的数量不能超过100
prefetch: 100
初始化
@Configuration
public class CreateOrderRabbitConfig {
/**
* 发货交换器
* @return
*/
@Bean
public DirectExchange sendOrderExchange(){
return new DirectExchange(RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey());
}
/**
* 订单超时交换器
* @return
*/
@Bean
public DirectExchange orderOverTimeExchange(){
return (DirectExchange) ExchangeBuilder.directExchange(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey()).durable(true).build();
}
/**
* 发货队列
* @return
*/
@Bean
public Queue sendOrderQueue(){
return new Queue(RabbitKeyEnums.ORDER_SEND_QUEUE.getKey());
}
/**
* 归还库存死信队列
* @return
*/
@Bean
public Queue returnInventoryQueue(){
return new Queue(RabbitKeyEnums.ORDER_RETURN_INVENTORY_QUEUE.getKey());
}
/**
* 订单超时队列
* @return
*/
@Bean
public Queue orderOverTimeQueue(){
Map<String,Object> args = new HashMap<>(2);
//x-dead-letter-exchange 声明死信队列Exchange
args.put("x-dead-letter-exchange",
RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey());
//x-dead-letter-routing-key 声明超时订单未支付队列中过期的消息将转发到归还库存队列中
args.put("x-dead-letter-routing-key",
RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
return QueueBuilder.durable(RabbitKeyEnums.ORDER_PAY_OVERTIME_QUEUE.getKey()).
withArguments(args).
build();
}
/**
* 发货队列交换器绑定
* @return
*/
@Bean
public Binding createOrderBinding(){
return BindingBuilder.
bind(sendOrderQueue()).
to(sendOrderExchange()).
with(RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey());
}
/**
* 订单超时-绑定
* @return
*/
@Bean
public Binding orderOverTimeBinding(){
return BindingBuilder.
bind(orderOverTimeQueue()).
to(orderOverTimeExchange()).
with(RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey());
}
/**
* 归还库存-绑定
* @return
*/
@Bean
public Binding returnInventoryBinding(){
return BindingBuilder.
bind(returnInventoryQueue()).
to(orderOverTimeExchange()).
with(RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
}
}
配置RabbitTemplate
@Slf4j
@Configuration
public class RabbitConfig {
@Autowired
private CachingConnectionFactory connectionFactory;
@Autowired
private MsgLogMapper msgLogMapper;
@Bean
public RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(converter());
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
String msgId = correlationData.getId();
MsgLog msgLog = new MsgLog();
msgLog.setMsgId(msgId);
msgLog.setStatus(MsgLogEnum.DELIVERY_SUC);
msgLogMapper.updateById(msgLog);
log.info("消息发送成功到Exchange");
}else{
//需要进行重发消息
log.info("消息发送到Exchange失败,{},cause:{}",correlationData,cause);
}
}
});
// 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
rabbitTemplate.setMandatory(true);
rabbitTemplate.setUseDirectReplyToContainer(true);
// 消息是否从Exchange路由到Queue, 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
});
return rabbitTemplate;
}
@Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
}
RabbitMQ 交換器、队列,绑定Key
@Getter
public enum RabbitKeyEnums {
/**
* 订单发货交换器
*/
ORDER_SEND_EXCHANGE("order.send.exchange"),
/**
* 订单超时exchange
*/
ORDER_PAY_OVERTIME_EXCHANGE("order.pay.overtime.exchange"),
/**
* 订单发货队列
*/
ORDER_SEND_QUEUE("order.send.queue"),
/**
* 订单超时queue
*/
ORDER_PAY_OVERTIME_QUEUE("order.pay.overtime.queue"),
/**
* 归还库存_死信队列
*/
ORDER_RETURN_INVENTORY_QUEUE("order.return.inventory.queue"),
/**
* 订单发货队列交换器绑定Key
*/
ORDER_SEND_ROUTING_KEY("order.send.routing"),
/**
* 订单超时routing_key
*/
ORDER_PAY_OVERTIME_ROUTING_KEY("order.pay.overtime.routing"),
/**
* 归还库存routing_key
*/
ORDER_RETURN_INVENTORY_ROUTING_KEY("order.return.inventory.routing"),
;
private String key;
RabbitKeyEnums(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
简单模拟支付成功发货场景使用
- 具体流程:收到支付回调之后,发送一条通知到仓库发货的消息,仓库服务收到监听发货队列并消费消息发货
- 发送端代码:
@GetMapping("/payCallBack")
public String payCallBack(String orderNo){
CorrelationData correlationData = new CorrelationData(orderNo);
if(isPay==1){
log.info("{}订单支付成功,进行发货处理",orderNo);
msgLogFun.saveMsgLog(orderNo,
orderNo,
RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey(),
RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey());
log.info("{}订单发货消息落库成功",orderNo);
//已支付
rabbitTemplate.convertAndSend(RabbitKeyEnums.ORDER_SEND_EXCHANGE.getKey(),
RabbitKeyEnums.ORDER_SEND_ROUTING_KEY.getKey(),
MessageHelper.objToMsg(orderNo),
correlationData);
}else{
log.info("{}订单异常,支付回调失败",orderNo);
}
return "成功";
}
- 消费端:
@RabbitListener(queues = "order.send.queue")
public void consume(Message message, Channel channel) {
String orderNo = MessageHelper.msgToObj(message, String.class);
try {
log.info("收到发货的消息:orderId:{}", orderNo);
MsgLog msgLog = msgLogMapper.selectById(orderNo);
if (msgLog == null || MsgLogEnum.SPENT.equals(msgLog.getStatus())) {
log.info("重复发货,orderId:{}", orderNo);
return;
}
MessageProperties messageProperties = message.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
MsgLog newMsg = new MsgLog();
newMsg.setMsgId(msgLog.getMsgId());
newMsg.setStatus(MsgLogEnum.SPENT);
newMsg.setUpdateTime(LocalDateTime.now());
UpdateWrapper<MsgLog> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("status",MsgLogEnum.DELIVERY_SUC);
int count = msgLogMapper.update(newMsg,updateWrapper);
if (count > 0) {
log.info("发货成功,msgId:{}", orderNo);
channel.basicAck(deliveryTag, false);
} else {
log.info("发货,重新发送,msgId:{}", orderNo);
channel.basicNack(deliveryTag, false, true);
}
}catch (Exception e){
log.info("程序异常,发货", orderNo);
e.printStackTrace();
}
}
保证百分百消费
- 在一些比较重要的消息,需要保证消息百分百消费,上面这个例子也提现了这一场景
- 流程:发送消息前,我们提前对消息进行落库,收到Exchange消息到达之后,并且成功路由到队列,将消息状态改为投递成功,消费端接受到消息之后并成功消费后将消息状态改为消费成功。
- 最后:设置定时任务从数据库中查询出消费次数大于三次,且是失败的消息,进行重新投递。
数据库结构
@TableName(value="msg_log")
@Data
public class MsgLog{
/** 消息唯一标识 */
@TableId
private String msgId ;
/** 消息体, json格式化 */
private String msg ;
/** 交换机 */
private String exchange ;
/** 路由键 */
private String routingKey;
/** 状态: 0投递中 1投递成功 2投递失败 3已消费 */
private MsgLogEnum status;
/** 重试次数 */
private Integer tryCount ;
/** 下一次重试时间 */
private LocalDateTime nextTryTime ;
/** 创建时间 */
private LocalDateTime createTime ;
/** 更新时间 */
private LocalDateTime updateTime ;
}
消息状态
@Getter
public enum MsgLogEnum implements IEnum<Integer>{
/** 状态: 0投递中 1投递成功 2投递失败 3已消费 */
/**
* 投递中
*/
DELIVERING(0,"投递中"),
/**
* 投递成功
*/
DELIVERY_SUC(1,"投递成功"),
/**
* 投递失败
*/
DELIVERY_FAILED(2,"投递失败"),
/**
* 已消费
*/
SPENT(3,"已消费"),
;
MsgLogEnum(Integer state, String desc) {
this.state = state;
this.desc = desc;
}
private int state;
private String desc;
@Override
public String toString() {
return this.desc;
}
@Override
public Integer getValue() {
return this.state;
}}
发送端配置和代码
rabbitmq:
##开启生产端->>>Exchange的Confirm
publisher-confirms: true
##开启Exchange->>Queue的Confirm
publisher-returns: true
listener:
simple:
##设置手动ACK Queue ->消息端
acknowledge-mode: manual
说明:开启pubisher确认机制,开启Exchange->>Queue的Confirm,设置手动ACK Queue进行手动确认
@Bean
public RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(converter());
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
String msgId = correlationData.getId();
MsgLog msgLog = new MsgLog();
msgLog.setMsgId(msgId);
msgLog.setStatus(MsgLogEnum.DELIVERY_SUC);
msgLogMapper.updateById(msgLog);
log.info("消息发送成功到Exchange");
}else{
//需要进行重发消息
log.info("消息发送到Exchange失败,{},cause:{}",correlationData,cause);
}
}
});
// 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
rabbitTemplate.setMandatory(true);
rabbitTemplate.setUseDirectReplyToContainer(true);
// 消息是否从Exchange路由到Queue, 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String msgId = MessageHelper.msgToObj(message, String.class);
MsgLog msgLog = new MsgLog();
msgLog.setMsgId(msgId);
msgLog.setStatus(MsgLogEnum.DELIVERY_FAILED);
msgLogMapper.updateById(msgLog);
log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
});
return rabbitTemplate;
}
说明:必须设置ConfirmCallback,和ReturnCallback进行相对应的业务处理,这里处理是改变消息的状态。 消费端代码
@RabbitListener(queues = "order.send.queue")
public void consume(Message message, Channel channel) {
String orderNo = MessageHelper.msgToObj(message, String.class);
try {
log.info("收到发货的消息:orderId:{}", orderNo);
MsgLog msgLog = msgLogMapper.selectById(orderNo);
if (msgLog == null || MsgLogEnum.SPENT.equals(msgLog.getStatus())) {
log.info("重复发货,orderId:{}", orderNo);
return;
}
MessageProperties messageProperties = message.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
MsgLog newMsg = new MsgLog();
newMsg.setMsgId(msgLog.getMsgId());
newMsg.setStatus(MsgLogEnum.SPENT);
newMsg.setUpdateTime(LocalDateTime.now());
UpdateWrapper<MsgLog> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("status",MsgLogEnum.DELIVERY_SUC);
int count = msgLogMapper.update(newMsg,updateWrapper);
if (count > 0) {
log.info("发货成功,msgId:{}", orderNo);
channel.basicAck(deliveryTag, false);
} else {
log.info("发货,重新发送,msgId:{}", orderNo);
channel.basicNack(deliveryTag, false, true);
}
}catch (Exception e){
log.info("程序异常,发货", orderNo);
e.printStackTrace();
}
}
说明:消费成功,进行手动ack,消费失败进行重发。
定时任务
@Slf4j
@Configuration
@EnableScheduling
public class ResendMsg {
@Autowired
private MsgLogMapper msgLogMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 每30秒拉去投递失败的消息,重新投递
*/
@Scheduled(cron = "0/30 * * * * ?")
public void resend() {
log.info("开始执行定时任务:重新投递消费失败的消息");
QueryWrapper<MsgLog> wrapper = new QueryWrapper<>();
wrapper.eq("status", MsgLogEnum.DELIVERY_FAILED);
List<MsgLog> msgLogs = msgLogMapper.selectList(wrapper);
if (msgLogs != null && msgLogs.size() > 0) {
msgLogs.forEach((msgLog -> {
String msgId = msgLog.getMsgId();
if (msgLog.getTryCount() >= 3) {
log.info("超过最大投递次数,消息投递失败,msgId:{}", msgId);
} else {
int num = msgLog.getTryCount() + 1;
msgLog.setTryCount(num);
msgLog.setStatus(MsgLogEnum.SPENT);
msgLogMapper.updateById(msgLog);
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(msgLog.getExchange(),
msgLog.getRoutingKey(),
MessageHelper.objToMsg(msgLog.getMsg()),
correlationData);
log.info("msgId:{},第{}次重新投递消息", msgId, num);
}
}));
}
log.info("结束执行定时任务:重新投递失败消息");
}
}
说明:这里使用SpringBoot自带的定时任务,并每30秒拉去投递失败的消息,重新投递,可以根据业务进行调整。
死信队列
使用场景
- 发红包过期退款业务
- 订单到期未支付恢复库存业务
- 等等
变成死信队列的情况
- 消息被拒绝(basic.reject/basic.nack)并且requeue = false
- 消息TTL过期
- 队列达到最大长度
配置
/**
* 订单超时交换器
* @return
*/
@Bean
public DirectExchange orderOverTimeExchange(){
return (DirectExchange) ExchangeBuilder.directExchange(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey()).durable(true).build();
}
/**
* 归还库存死信队列
* @return
*/
@Bean
public Queue returnInventoryQueue(){
return new Queue(RabbitKeyEnums.ORDER_RETURN_INVENTORY_QUEUE.getKey());
}
/**
* 订单超时队列
* @return
*/
@Bean
public Queue orderOverTimeQueue(){
Map<String,Object> args = new HashMap<>(2);
//x-dead-letter-exchange 声明死信队列Exchange
args.put("x-dead-letter-exchange",
RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey());
//x-dead-letter-routing-key 声明超时订单未支付队列中过期的消息将转发到归还库存队列中
args.put("x-dead-letter-routing-key",
RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
return QueueBuilder.durable(RabbitKeyEnums.ORDER_PAY_OVERTIME_QUEUE.getKey()).
withArguments(args).
build();
}
/**
* 订单超时-绑定
* @return
*/
@Bean
public Binding orderOverTimeBinding(){
return BindingBuilder.
bind(orderOverTimeQueue()).
to(orderOverTimeExchange()).
with(RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey());
}
/**
* 归还库存-绑定
* @return
*/
@Bean
public Binding returnInventoryBinding(){
return BindingBuilder.
bind(returnInventoryQueue()).
to(orderOverTimeExchange()).
with(RabbitKeyEnums.ORDER_RETURN_INVENTORY_ROUTING_KEY.getKey());
}
发送死信队列消息
//发送订单未支付恢复库存的死信队列
CorrelationData correlationData = new CorrelationData(orderNo);
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
//订单未支付超时时间
messageProperties.setExpiration(String.valueOf(timeOut));
messageProperties.setContentEncoding("utf-8");
return message;
}
};
rabbitTemplate.convertAndSend(RabbitKeyEnums.ORDER_PAY_OVERTIME_EXCHANGE.getKey(),
RabbitKeyEnums.ORDER_PAY_OVERTIME_ROUTING_KEY.getKey(),
MessageHelper.objToMsg(orderNo),messagePostProcessor,correlationData);
log.info("{}订单超时未支付消息已发送",orderNo);
说明:这里发送指定的队列不进行消费,消息过期后,会转发到死信队列中(returnInventoryQueue)
最后
- 如果对你有帮助,就点个赞吧。
- 不懂可以在评论区留言,一起进步。
- 如果哪里有不对的地方,或者有更好的方式,欢迎指出。