RabbitMQ
RabbitMQ
定义
RabbitMQ是实现高级消息队列协议(AMQP)的开源消息代理软件,使用Erlang语言编写的,而集群和故障转移的构建在开放电信平台框架上的。
结构
关键字
AMQP协议中间的几个重要概念:
- Server:接收客户端的连接,实现AMQP实体服务。
- Connection:连接,应用程序与Server的网络连接,TCP连接。
- Channel:信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。
- Message:消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。有Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。
- Virtual Host:虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。
- Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。RabbitMQ常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。
- Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey。
- RoutingKey:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”。
- Queue:消息队列,用来保存消息,供消费者消费。
交换机
RabbitMQ常用的交换器类型有direct、topic、fanout、headers四种
- Direct路由交换机
该类型的交换机将所有发送到该交换机的消息转发到RoutingKey指定队列中,也就是说路由到BindingKey和RoutingKey完全匹配的队列中.
- topic
该类型的交换机将所有发送到Topic Exchange的消息被转发到所有RoutingKey中指定的Topic的队列上面。
Exchange将RoutingKey和某Topic进行模糊匹配,期中“ ” 用来匹配一个词, “#”用来匹配一个或者多个词。 例如: “com.#”能匹配到“com.rabbitmq.oa” 和 “com.rabbitmq”; 而 "login."只能匹配到”com.rabbitmq“。
区别:topic与direct区别:topic可以模糊匹配; direct只能精准匹配
- fanout订阅交换机
该类型不处理路由键,会把所有发送到交换机的信息路由到所有绑定的队列中。
优点: 转发消息最快,性能最好
- headers
该类型的减缓及不依赖路由规则来路由消息,而是根据消息内同中的headers属性进行匹配。headers类型交换机性能差,在实际中并不常用.
work工厂模式
工厂模式
Direct路由交换机
路由交换机:绑定key
fanount发布订阅模式
发布订阅交换机
消息可靠投递
消息的两种可靠投递: 消息发送到交换机的确认; 消息从交换机发送到队列的确认机制
1、confirm可靠投递: 消息发送到交换机的确认
2、return可靠投递: 消息从交换机发送到队列的确认
如果消息没有到exchange,则confirm回调,ack=false
如果消息到达exchange,则confirm回调,ack=true
但如果是找不到exchange,则会先触发returncallback
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
// 如果ack为true -----> 消息已经发送到交换机成功
// 否则ack为false ----> 消息发送交换机失败
}
}
消息的ACK确认机制
1、什么是消息确认ACK。
答:如果在处理消息的过程中,消费者的服务器在处理消息的时候出现异常,那么可能这条正在处理的消息就没有完成消息消费,数据就会丢失。为了确保数据不会丢失,RabbitMQ支持消息确定-ACK。
2、ACK的消息确认机制。
答:ACK机制是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。
如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
如果在集群的情况下,RabbitMQ会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务。
消息永远不会从RabbitMQ中删除,只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。
消息的ACK确认机制默认是打开的。
3、ACK机制的开发注意事项。
答:如果忘记了ACK,那么后果很严重。当Consumer退出时候,Message会一直重新分发。然后RabbitMQ会占用越来越多的内容,由于RabbitMQ会长时间运行,因此这个"内存泄漏"是致命的。
4、结合项目实例进行,理解一下ACK机制。之前写过RabbitMQ的交换器Exchange之direct(发布与订阅 完全匹配),这里借助这个进行消息持久化测试。生产者的代码不发生改变。控制层的触发生产者生产消息,这里只生产一条消息。方便观察现象
TTL
TTL全程Time TO Live (存活时间/过期时间)
当消息达到存货时间后,还没有被消费,会被自动清除
RabbitMQ可以对消息设置过期时间,也可以对整个队列设置过期时间.
延迟队列
延迟队列,消息进入队列后不会立即被消费,只有达到指定时间,才会被消费。
rabbitMQ没有延迟队列的,可以通过TTL + 死信队列 组合实现
对队列设置过期时间,当过期之后,将队列中的消息发送到死信队列中
延迟队列与私信队列的绑定区别....
死信队列
死信队列(DLX),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX.
死信队列的条件:
1、 队列消息长度达到限制;
2、 消费者拒绝接受消息,basicNack/basicReject,并且不把消息重新放入原目标队列, requeue=false;
3、 原队列存在消息过期设置,消息到达超市时间未被消费;
队列绑定死信交换机参数:
x-dead-letter-exchange 和 x-dead-letter-routing-key
消息补偿
RabbitMQ继承springboot项目
POM.XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
server.port=14001
spring.application.name=service-rabbitmq
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=mafeng_1234_
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=host1
spring.rabbitmq.username=king
spring.rabbitmq.password=king
# 开启消息确认机制
spring.rabbitmq.publisher-confirm-type=simple
# 开启ACK确认机制 手动
spring.rabbitmq.listener.direct.acknowledge-mode=manual
rabbitMQ配置类
/**
* 死信队列
* @return
*/
@Bean
public Queue dlQueue(){
return QueueBuilder.durable("dlQueue")
.build();
}
@Bean
public DirectExchange dlExchange(){
return (DirectExchange) ExchangeBuilder.directExchange("dlExchange").build();
}
@Bean
public Binding dlMessageBinding(){
return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("dlRoutingKey");
}
@Bean
public DirectExchange messageDirectExchange() {
return (DirectExchange) ExchangeBuilder.directExchange("orderExchange")
.durable(true)
.build();
}
@Bean
public Queue messageQueue() {
return QueueBuilder.durable("orderQueue")
//配置死信
.withArgument("x-dead-letter-exchange","dlExchange")
.withArgument("x-dead-letter-routing-key","dlRoutingKey")
.build();
}
@Bean
public Binding messageBinding() {
return BindingBuilder.bind(messageQueue())
.to(messageDirectExchange())
.with("orderRoutingKey");
}
MQ发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(OrderPO orderPO) {
rabbitTemplate.convertAndSend("orderExchange","orderRoutingKey", JSON.toJSONString(orderPO),
message -> { message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
log.debug(" this is message ------> { message } ", message);
return message;
},
new CorrelationData(orderPO.getOrderNo()));
}
消息的可靠投递
@Autowired
private ConfirmDTO confirmDTO;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info(" this is message if true and false ------> { ack } ", String.valueOf(ack));
if(ack) {
// 更新订单消息状态
ConfirmPO confirmPO = new ConfirmPO();
confirmPO.setOrderId(Integer.parseInt(correlationData.getId()));
confirmPO.setSendStatus(1);
confirmDTO.updateCon(confirmPO);
} else {
log.debug(" this is error ------> { ack } ", ack);
}
}
});
}
ACK确认机制
@Component
@Slf4j
public class OrderListener {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = "orderQueue")
public void HandlerMessage(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long tag,
@Header(AmqpHeaders.REDELIVERED) boolean reDelivered ) throws IOException {
log.info(message);
OrderPO orderBean = JSON.parseObject(message, OrderPO.class);
try {
log.info("收到的消息为{}",JSON.toJSONString(orderBean));
//保证幂等性
if(stringRedisTemplate.opsForValue().get(orderBean.getOrderNo())==null){
sendMessage(orderBean);
stringRedisTemplate.opsForValue().set(orderBean.getOrderNo(),"1");
}
channel.basicAck(tag,false);
} catch (Exception e) {
if(reDelivered){
log.info("消息已重复处理失败:{}",message);
channel.basicReject(tag,false);
}else{
log.error("消息处理失败",e);
//重新入队一次
channel.basicNack(tag,false,true);
}
}
}
private void sendMessage(OrderPO orderBean)throws Exception{
if(orderBean.getOrderNo().equals("0007")){
int a =3/0;
}
log.info("模拟发送短信");
}
}
RabbitMQ的最终一致性
消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分为本地事务进行处理。
①基本流程:两阶段处理方式
假设有两个事务,第一个事务先写入业务数据,然后在写入消息数据(此时会额外建立一个消息表来记录我要发送的消息内容),写完消息数据之后,在将数据发送给MQ(第一阶段到此结束)。
第二个事务要用到这个数据第二个事务就从MQ中拿到对应的数据,拿到这个消息,在写入业务数据。如果说事务成功了,就修改消息表(就是事务1新建的,用来存储要发送的消息内容的消息表)中的状态。如果说事务操作这个消息失败了,也要给事务1说一下,然后事务1在调用一些补偿的代码来执行一些数据库回滚的操作。若写入业务事务成功,(事务1)发送到MQ失败了,会怎么样?则消息表会存积大量的未处理的消息数据,此时,会有一个另外的线程去定时的去扫描这个消息表,若发现有大量的未处理的消息,则在进行一些对应的补偿逻辑
其他分布式事务处理方式
1、基于XA协议的两阶段提交
①XA规范中分布式事务由AP,RM,TM三部分组成。具体点就是,应用程序(AP),事务管理器(TM),资源管理器(RM),通信资源管理器(CRM)四个部分。一般,常见的事务管理器(TM)是交易中间件,常见的资源管理器(RM)是数据库,常见的通信资源管理器(CRM)是消息中间件
AP:定义事务边界(定义事务开始和结束),并访问事务边界内的资源
RM:资源管理器,管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库,文件系统,打印机服务器等
TM:负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等
②两阶段协议:
第一阶段:TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作的内容就行持久化,并给TM回执OK,否则给TM回执NO,RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
第二阶段:TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务,如果所有的RM都prepare成功,那么TM通知所有的RM进行提交,如果有RM prepare回执NO的话,则TM通知所有的RM回滚自己的事务分支
③XA协议两阶段提交的优缺点:
优点:进来保持了数据的强一致性,适合对数据强一致性要求跟高的领域(并非100%保证强一致性)
缺点:实现复杂,牺牲了可用性,对性能影响比较大,不适合高并发高性能的场景
2、TCC补偿机制
TCC其实就是采用的补偿机制,其核心思想是,针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作,他分为三个阶段:
·Try阶段,主要是对业务系统做检测已经资源御灵
·Confirm阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。即:主要Try成功,Confirm一定成功
·Cancel阶段主要是业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放
②TCC补偿机制的优缺点:
优点:相比于两阶段提交,可用性比较强(因为两阶段涉及到了锁的概念)
缺点:数据的一致性要差一些,TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码。在一些场景中,一些业务流程可能用TCC不太好定义处理

浙公网安备 33010602011771号