先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer 从 queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
1.1、死信的来源
- 消息TTL过期;
- 队列达到最大长度(队列满了,无法再添加数据到mq.中);
- 消息被拒绝(basic.reject 或 basic.nack)并且requeue=false;
1.2、死信实战
架构图
1.3、消费者01
//死信队列
public class DeadMessageConsumer01 {
//普通交换机
public static final String NORMAL\_EXCHANGE = "normal\_exchange";
//死信交换机
public static final String DEAD\_EXCHANGE = "dead\_exchange";
//普通队列
public static final String NORMAL\_QUEUE = "normal\_queue";
//死信队列
public static final String DEAD\_QUEUE = "dead\_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
//声明普通和死信交换机
channel.exchangeDeclare(NORMAL\_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD\_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明普通队列,这一次需要使用参数了
Map<String, Object> arguments = new HashMap<>();
//这里就要设置死信到死信交换机上了
arguments.put("x-dead-letter-exchange",DEAD\_EXCHANGE);
//设置死信routingKey
arguments.put("x-dead-letter-routing-key","deadKey");
channel.queueDeclare(NORMAL\_QUEUE,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD\_QUEUE,false,false,false,null);
//开始分别将死信和普通交换机与队列绑定
channel.queueBind(NORMAL\_QUEUE,NORMAL\_EXCHANGE,"normalKey");
channel.queueBind(DEAD\_QUEUE,DEAD\_EXCHANGE,"deadKey");
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("死信测试普通队列消费者01收到消息:"+new String(message.getBody()));
};
channel.basicConsume(NORMAL\_QUEUE,true,deliverCallback,consumerTag -> {});
}
}
1.4、生产者
//死信队列的生产者
public class DeadMessageProducers {
//普通交换机
public static final String NORMAL\_EXCHANGE = "normal\_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
//死信消息设置TTL过期时间,设置10秒
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties()
.builder().expiration("10000").build();
//发10条消息
for (int i = 1; i < 11; i++) {
String message = "dead"+i;
channel.basicPublish(NORMAL\_EXCHANGE,"normalKey",basicProperties,message.getBytes());
System.out.println("发送消息成功:"+ message);
}
}
}
先把消费者启动,将交换机和队列先声明出来,然后关掉消费者再启动生产者,生产者一直发这10个消息,但是消费者已经停止了,没人消费,所以TTL一到期就会被放进死信队列里,我们在web管理界面就能看到,普通队列的10条消息很快就跑到死信队列中了。
然后现在死信队列里有那10条消息,再来一个专门消费死信消息的消费者2,非常简单,只需要消费死信队列即可。
//死信队列
public class DeadMessageConsumer02 {
//死信队列
public static final String DEAD\_QUEUE = "dead\_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("死信测试普通队列消费者02收到消息:"+new String(message.getBody()));
};
channel.basicConsume(DEAD\_QUEUE,true,deliverCallback,consumerTag -> {});
}
}
启动它消费死信队列
消费完成,看看管理界面,死信消息归0
1.5、队列达到最大长度的死信
在原来的消费者基础上增加一条
//设置队列最大长度
arguments.put("x-max-length",6);
原来的生产者也不需要给消息设置过期时间了直接发送消息即可。
测试之前先将原来的普通队列删除再启动消费者
仍然把消费者停掉,生产者启动,发送10条消息6条正常4条超过了限制会被放到死信队列
1.6、消息被拒死信队列
我们先把原来的消息全部消费掉或者直接删除原来的队列,再演示新的。
消费者做一些修改
DeliverCallback deliverCallback = (consumerTag,message) ->{
String msg = new String(message.getBody());
if (msg.equals("dead5")){
System.out.println("拒绝第5个消息:"+ msg);
//basicReject第二个参数是是否还放回队列
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
} else {
System.out.println("死信测试普通队列消费者01收到消息:" + msg);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
channel.basicConsume(NORMAL\_QUEUE,false,deliverCallback,consumerTag -> {});
生产者发送10条消息,看到消费者01拒绝了第5个消息,其他的正常收到
通过管理界面看到第5条消息放到了死信队列中
启动专门消费死信队列的消费者02将第5个消息消费。
2、延迟队列
2.1、延迟队列概念
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
其实,TTL过期导致的死信队列就是延迟队列
应用场景:
- 1.订单在十分钟之内未支付则自动取消
- ⒉新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?
如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。
但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
2.1.1、RabbitMQ 中的 TTL
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间, 单位是毫秒。
换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
- 消息设置 TTL
例如:
rabbitTemplate.convertAndSend("X","XC",message,msg -> {
//发送消息设置消息的TTL
msg.getMessageProperties().setExpiration(ttl+"000");
return msg;
});
- **队列设置 TTL **
例如:在创建队列的时候设置队列的“x-message-ttl”属性
//声明普通队列 TTL 为40秒
@Bean("queueB")
public Queue queueB(){
HashMap<String, Object> map = new HashMap<>(3);
//设置死信交换机
map.put("x-dead-letter-exchange",Y\_DEAD\_LETTER\_EXCHANGE);
//设置死信RoutingKey
map.put("x-dead-letter-routing-key","YD");
//设置过期时间,10秒
map.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE\_B).withArguments(map).build();
}
2.2、整合SpringBoot
添加依赖项
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<!-- 添加 Spring Boot 版本 -->
<spring.boot.version>2.3.4.RELEASE</spring.boot.version>
<!-- 添加 Lombok 版本 -->
<lombok.version>1.18.20</lombok.version>
<!-- 添加 Spring AMQP 版本 -->
<spring.amqp.version>2.1.4.RELEASE</spring.amqp.version>
<!-- 添加 Springfox Swagger 版本 -->
<springfox.version>2.9.2</springfox.version>
</properties>
<dependencies>
<!-- RabbitMQ 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80-fix</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
<!-- RabbitMQ 测试依赖 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<version>${spring.amqp.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
创建application.properties文件
spring.rabbitmq.host=192.168.111.28
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.redis.password=123
swagger2的配置类SwaggerConfig
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER\_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 微服务接口定义")
.version("1.0")
.contact(new Contact("zm", "http://zmblog.vip",
"3339332352@qq.com"))
.build();
}
}
2.3、队列TTL
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
2.4、配置文件类代码
TtlQueueConfig
//TTL队列,配置类代码
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X\_EXCHANGE = "X";
//死信交换机名称
public static final String Y\_DEAD\_LETTER\_EXCHANGE = "Y";
//普通队列名称,两个
public static final String QUEUE\_A = "QA";
public static final String QUEUE\_B = "QB";
//死信队列名称
public static final String DEAD\_LETTER\_QUEUE = "QD";
//声明普通交换机X\_EXCHANGE,起一个别明xExchange注入bean
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X\_EXCHANGE);
}
//声明死信交换机Y\_EXCHANGE,起一个别明yExchange注入bean
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y\_DEAD\_LETTER\_EXCHANGE);
}
//声明普通队列 TTL 为10秒
@Bean("queueA")
public Queue queueA(){
HashMap<String, Object> map = new HashMap<>(3);
//设置死信交换机
map.put("x-dead-letter-exchange",Y\_DEAD\_LETTER\_EXCHANGE);
//设置死信RoutingKey
map.put("x-dead-letter-routing-key","YD");
//设置过期时间,10秒
map.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUE\_A).withArguments(map).build();
}
//声明普通队列 TTL 为40秒
@Bean("queueB")
public Queue queueB(){
HashMap<String, Object> map = new HashMap<>(3);
//设置死信交换机
map.put("x-dead-letter-exchange",Y\_DEAD\_LETTER\_EXCHANGE);
//设置死信RoutingKey
map.put("x-dead-letter-routing-key","YD");
//设置过期时间,10秒
map.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE\_B).withArguments(map).build();
}
//死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD\_LETTER\_QUEUE).build();
}
//绑定
@Bean
public Binding QABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
@Bean
public Binding QBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
@Bean
public Binding QDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
生产者controller,DeadLetterProducers
//生产者发送延迟消息
@Slf4j
@RestController
public class DeadLetterProducers {
//自动注入
@Autowired
private RabbitTemplate rabbitTemplate;
//发消息
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条消息给两个TTL队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自于TTL为10秒的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自于TTL为40秒的队列:"+message);
}
}
消费者:
//消费者
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//接收消息,定义一个监听器来监听死信队列中的消息
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel){
String s = new String(message.getBody());
log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(),s);
}
}
成功启动
在浏览器上输入localhost:8080/sendMsg/哈哈哈哈哈
发现后台开始输出信息,先是发送的提示信息,然后过了10秒消费者收到了一个信息,然后再过30秒收到了第二条信息。
结果是没有问题的,第一条消息在10S后变成了死信消息,然后被消费者消费掉,第二条消息在40S之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有10S和40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
2.5、延迟队列优化
在这里新增了一个队列 QC,它是通用的延迟队列,生产者掌握时间(这是设定消息的过期时间,之前是设定队列的过期时间),绑定关系如下,该队列不设置 TTL 时间
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-0AN8AuZe-1712859262644)]
[外链图片转存中…(img-yaSTZ7Jo-1712859262645)]
[外链图片转存中…(img-AKehHLNC-1712859262645)]
[外链图片转存中…(img-Qk5wntmh-1712859262645)]
[外链图片转存中…(img-wqLG8vWH-1712859262646)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
[外链图片转存中…(img-PO9Zuzle-1712859262646)]