SpringBoot整合RabbitMQ
一、目前存在的问题
1.1搜索与商品服务的问题
我们思考一下,是否存在问题?
- 商品的原始数据保存在数据库中,增删改查都在数据库中完成。
- 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。
如果我们在后台修改了商品的价格,搜索页面依然是旧的价格,这样显然不对。该如何解决?
这里有两种解决方案:
- 方案1:每当后台对商品做增删改操作,同时要修改索引库数据
- 方案2:搜索服务对外提供操作接口,后台在商品增删改后,调用接口
以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。
所以,我们会通过另外一种方式来解决这个问题:消息队列
1.2订单服务取消订单问题
用户下单后,如果2个小时未支付,我们该如何取消订单
- 方案1:定时任务,定时扫描未支付订单,超过2小时自动关闭
- 方案2:使用延迟队列关闭订单
1.3分布式事务问题
如:用户支付订单,我们如何保证更新订单状态与扣减库存 ,三个服务数据最终一致!
二、消息队列解决什么问题
消息队列都解决了什么问题?
2.1异步
2.2并行
2.3解耦
2.4排队
五种消息模型
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,那么也就剩下5种。
但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
基本消息模型:生产者–>队列–>消费者
work消息模型:生产者–>队列–>多个消费者共同消费
订阅模型-Fanout:广播模式,将消息交给所有绑定到交换机的队列,每个消费者都会收到同一条消息
订阅模型-Direct:定向,把消息交给符合指定 rotingKey 的队列
订阅模型-Topic 主题模式:通配符,把消息交给符合routing pattern(路由模式) 的队列
3. 消息不丢失
消息的不丢失,在MQ角度考虑,一般有三种途径:
1,生产者不丢数据
2,MQ服务器不丢数据
3,消费者不丢数据
保证消息不丢失有两种实现方式:
1,开启事务模式
2,消息确认模式
说明:开启事务会大幅降低消息发送及接收效率,使用的相对较少,因此我们生产环境一般都采取消息确认模式,以下我们只是讲解消息确认模式
3.1消息确认
3.1.1 消息持久化
如果希望RabbitMQ重启之后消息不丢失,那么需要对以下3种实体均配置持久化
Exchange
声明exchange时设置持久化(durable = true)并且不自动删除(autoDelete = false)
Queue
声明queue时设置持久化(durable = true)并且不自动删除(autoDelete = false)
message
发送消息时通过设置deliveryMode=2持久化消息
说明:后面会有代码!先做个了解
@Queue: 当所有消费客户端连接断开后,是否自动删除队列
true:删除 false:不删除
@Exchange:当所有绑定队列都不在使用时,是否自动删除交换器
true:删除 false:不删除
3.1.2 发送确认
有时,业务处理成功,消息也发了,但是我们并不知道消息是否成功到达了rabbitmq,如果由于网络等原因导致业务成功而消息发送失败,那么发送方将出现不一致的问题,此时可以使用rabbitmq的发送确认功能,即要求rabbitmq显式告知我们消息是否已成功发送。
3.1.3 手动消费确认
有时,消息被正确投递到消费方,但是消费方处理失败,那么便会出现消费方的不一致问题。比如:订单已创建的消息发送到用户积分子系统中用于增加用户积分,但是积分消费方处理却都失败了,用户就会问:我购买了东西为什么积分并没有增加呢?
要解决这个问题,需要引入消费方确认,即只有消息被成功处理之后才告知rabbitmq以ack,否则告知rabbitmq以nack
4. 搭建示例工程
4.1. 创建工程
4.2. 添加依赖
<dependencies>
<!--rabbitmq消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--rabbitmq 协议-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
</dependencies>
4.3 添加配置
rabbitmq:
host: 192.168.200.128
port: 5672
username: guest
password: guest
publisher-confirms: true // 交换机的确认
publisher-returns: true // 队列的确认
listener:
simple:
acknowledge-mode: manual #默认情况下消息消费者是自动确认消息的,如果要手动确认消息则需要修改确认模式为manual
prefetch: 1 # 消费者每次从队列获取的消息数量。此属性当不设置时为:轮询分发,设置为1为:公平分发
4.4 封装发送端消息确认
/**
* @Description 消息发送确认
* <p>
* ConfirmCallback 只确认消息是否正确到达 Exchange 中
* ReturnCallback 消息没有正确到达队列时触发回调,如果正确到达队列不执行
* <p>
* 1. 如果消息没有到exchange,则confirm回调,ack=false
* 2. 如果消息到达exchange,则confirm回调,ack=true
* 3. exchange到queue成功,则不回调return
* 4. exchange到queue失败,则回调return
*
*/
@Component
@Slf4j
public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
// 修饰一个非静态的void()方法,在服务器加载Servlet的时候运行,并且只会被服务器执行一次在构造函数之后执行,init()方法之前执行。
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this); //指定 ConfirmCallback
rabbitTemplate.setReturnCallback(this); //指定 ReturnCallback
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息发送成功:" + JSON.toJSONString(correlationData));
} else {
log.info("消息发送失败:" + cause + " 数据:" + JSON.toJSONString(correlationData));
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 反序列化对象输出
System.out.println("消息主体: " + new String(message.getBody()));
System.out.println("应答码: " + replyCode);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交换器 exchange : " + exchange);
System.out.println("消息使用的路由键 routing : " + routingKey);
}
}
4.5 封装消息发送
@Service
public class RabbitService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
* @param exchange 交换机
* @param routingKey 路由键
* @param message 消息
*/
public boolean sendMessage(String exchange, String routingKey, Object message) {
rabbitTemplate.convertAndSend(exchange, routingKey, message);
return true;
}
}
4.6 发送确认消息测试
@RestController
@RequestMapping("/mq")
@Slf4j
public class MqController {
@Autowired
private RabbitService rabbitService;
/**
* 消息发送
*/
//http://cart.gmall.com/8282/mq/sendConfirm
@GetMapping("sendConfirm")
public Result sendConfirm() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
rabbitService.sendMessage("exchange.confirm", "routing.confirm", sdf.format(new Date()));
return Result.ok();
}
}
4.7消息接收端
@Component
@Configuration
public class ConfirmReceiver {
@SneakyThrows
@RabbitListener(bindings=@QueueBinding(
value = @Queue(value = "queue.confirm",autoDelete = "false"),
exchange = @Exchange(value = "exchange.confirm",autoDelete = "true"),
key = {"routing.confirm"}))
public void process(Message message, Channel channel){
System.out.println("RabbitListener:"+new String(message.getBody()));
// 采用手动应答模式, 手动确认应答更为安全稳定
//如果手动确定了,再出异常,mq不会通知;如果没有手动确认,抛异常mq会一直通知
//channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// false 确认一个消息,true 批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
启动测试即可