当前位置: 首页 > news >正文

RabbitMQ(九)死信队列

目录

    • 一、简介
      • 1.1 定义
      • 1.2 何时进入死信队列?
      • 1.3 死信消息的变化
      • 1.4 死信队列的应用场景
      • 1.5 死信消息的生命周期
    • 二、代码实现
      • 2.1 死信队列的配置步骤
      • 2.2 配置类
      • 2.3 配置文件
      • 2.4 生产者
      • 2.5 业务消费者
      • 2.6 死信消费者
      • 2.7 测试结果
    • 三、总结
    • 四、补充
      • 4.1 启动报错 inequivalent arg 'x-dead-letter-exchange'

在这里插入图片描述

RabbitMQ 是流行的开源消息队列中间件,使用 erlang 语言开发,由于其社区活跃度高,维护更新较快,深得很多企业的喜爱。

一、简介

1.1 定义

死信队列(Dead Letter Queue,简称 DLX)是 RabbitMQ 中一种特殊的队列,用于处理无法正常被消费者消费的消息。当消息在原始队列中因为 达到最大重试次数过期、或者 满足特定条件 时,可以 将这些消息重新路由到一个预定义的死信队列中 进行进一步处理或记录。

1.2 何时进入死信队列?

当发生以下情况,业务队列中的消息会进入死信队列:

  1. 消息被否定确认:使用 channel.basicNackchannel.basicReject,并且此时 requeue 属性被设置为 false
  2. 消息过期:消息在队列的存活时间超过设置的 TTL 时间。
  3. 消息溢出:队列中的消息数量已经超过最大队列长度。

当发生以上三种情况后,该消息将成为 死信。死信消息会被 RabbitMQ 进行特殊处理:

  • 如果配置了死信队列,那么该消息将会被丢进死信队列中;
  • 如果没有配置,则该消息将会被丢弃。

1.3 死信消息的变化

那么 死信 被丢到死信队列后,会发生什么变化呢?

  • 如果队列配置了 x-dead-letter-routing-key 的话,“死信” 的路由键会被替换成该参数对应的值。
  • 如果没有配置,则保留该消息原有的路由键。

举个例子:

原有队列的路由键是 RoutingKey1,有以下两种情况:

  • 如果配置队列的 x-dead-letter-routing-key 参数值为 RoutingKey2,则该消息成为 “死信” 后,会将路由键更改为 RoutingKey2,从而进入死信交换机中的死信队列。
  • 如果没有配置 x-dead-letter-routing-key 参数,则该消息成为 “死信” 后,路由键不会更改,也不会进入死信队列。

在这里插入图片描述

当配置了 x-dead-letter-routing-key 参数后,消息成为 “死信” 后,会在消息的 Header 中添加很多奇奇怪怪的字段,我们可以在死信队列的消费端通过以下方式进行打印:

log.info("死信消息properties: {}", message.getMessageProperties());

日志内容如下:

2024-01-07 21:16:19.745  INFO 11776 --- [ntContainer#3-1] c.d.receiver.DeadLetterMessageReceiver   :消息properties: MessageProperties [headers={x-first-death-exchange=demo.simple.business.exchange, x-death=[{reason=rejected, count=1, exchange=demo.simple.business.exchange, time=Sun Jan 07 21:16:19 CST 2024, routing-keys=[], queue=demo.simple.business.queuea}], x-first-death-reason=rejected, x-first-death-queue=demo.simple.business.queuea}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=demo.simple.deadletter.exchange, receivedRoutingKey=demo.simple.deadletter.queuea.routingkey, deliveryTag=1, consumerTag=amq.ctag-RPfmKjM8Lau9X7Fl0CtbEA, consumerQueue=demo.simple.deadletter.queuea]

格式化后:

在这里插入图片描述

Header 中看起来有很多信息,实际上并不多,只是值比较长而已。下面就简单说明一下 Header 中的值:

字段名含义
x-first-death-exchange第一次成为死信时的交换机名称。
x-first-death-reason第一次成为死信的原因:
rejected:消息在进入队列时被队列拒绝。
expired:消息过期。
maxlen:队列内消息数量超过队列最大容量。
x-first-death-queue第一次成为死信时的队列名称。
x-death历史被投入死信交换机的信息列表,同一个消息每进入一次死信交换机,这个数组的信息就会被更新。

1.4 死信队列的应用场景

通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?

死信队列 一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要是消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等。当发生异常时,当然 不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息 (没错,以前很多人这么干的 = =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好得多。

1.5 死信消息的生命周期

死信消息的生命周期如下:

  1. 业务消息被 投入业务队列
  2. 消费者 消费业务队列的消息,由于处理过程中 发生异常,于是 进行了 NackReject 操作
  3. NackReject 的消息由 RabbitMQ 投递到死信交换机中
  4. 死信交换机将消息 投入相应的死信队列
  5. 死信队列的消费者 消费死信消息

二、代码实现

2.1 死信队列的配置步骤

死信队列的配置可以分为以下三步:

  1. 配置业务队列,绑定到业务交换机上;
  2. 为业务队列 配置死信交换机、路由键;
  3. 为死信交换机 配置死信队列

注意:

并不是直接声明一个公共的死信队列,然后所有死信消息就会自己进入死信队列中了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后每个业务队列分配一个单独的路由键。

有了死信交换机和路由键后,接下来就像配置业务队列一样,配置死信队列,并绑定在死信交换机上。看到这里,大家应该可以明白:

  • 死信队列 并不是什么特殊的队列,只不过是绑定在死信交换机上的队列
  • 死信交换机 也不是什么特殊的交换机,只不过是用来接收死信队列的交换机

所以死信交换机可以为任何类型【Direct、Fanout、Topic】。一般来说,因为开发过程中会为每个业务队列分配一个独有的路由 key,并对应的配置一个死信队列进行监听。

有了前面的这些描述后,我们接下来实战操作一下。

2.2 配置类

配置类中声明了两个交换机:

  • 业务交换机(广播),绑定了两个业务队列:
    • 业务队列A;
    • 业务队列B。
  • 死信交换机(直连),绑定了两个死信队列,并配置了相应的路由键:
    • 死信队列A;
    • 死信队列B。

RabbitMQConfig.java

package com.demo.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;/*** <p> @Title RabbitMQOrderConfig* <p> @Description RabbitMQ配置** @author ACGkaka* @date 2023/12/22 14:05*/
@Configuration
public class RabbitMQConfig {/** 业务队列 */public static final String BUSINESS_EXCHANGE_NAME = "demo.simple.business.exchange";public static final String BUSINESS_QUEUEA_NAME = "demo.simple.business.queuea";public static final String BUSINESS_QUEUEB_NAME = "demo.simple.business.queueb";/** 死信队列 */public static final String DEAD_LETTER_EXCHANGE = "demo.simple.deadletter.exchange";public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "demo.simple.deadletter.queuea.routingkey";public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "demo.simple.deadletter.queueb.routingkey";public static final String DEAD_LETTER_QUEUEA_NAME = "demo.simple.deadletter.queuea";public static final String DEAD_LETTER_QUEUEB_NAME = "demo.simple.deadletter.queueb";// 声明业务交换机(广播)@Beanpublic FanoutExchange businessExchange() {return new FanoutExchange(BUSINESS_EXCHANGE_NAME);}// 声明死信交换机(直连)@Beanpublic DirectExchange deadLetterExchange() {return new DirectExchange(DEAD_LETTER_EXCHANGE);}// 声明业务队列A@Beanpublic Queue businessQueueA() {Map<String, Object> args = new HashMap<>(2);// 声明当前队列绑定的死信交换机args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);// 声明当前队列绑定的死信路由键args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();}// 声明业务队列B@Beanpublic Queue businessQueueB() {Map<String, Object> args = new HashMap<>(2);// 声明当前队列绑定的死信交换机args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);// 声明当前队列绑定的死信路由键args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);return QueueBuilder.durable(BUSINESS_QUEUEB_NAME).withArguments(args).build();}// 声明死信队列A@Beanpublic Queue deadLetterQueueA() {return new Queue(DEAD_LETTER_QUEUEA_NAME);}// 声明死信队列B@Beanpublic Queue deadLetterQueueB() {return new Queue(DEAD_LETTER_QUEUEB_NAME);}// 声明业务队列A绑定关系@Beanpublic Binding businessBindingA(Queue businessQueueA, FanoutExchange businessExchange) {return BindingBuilder.bind(businessQueueA).to(businessExchange);}// 声明业务队列B绑定关系@Beanpublic Binding businessBindingB(Queue businessQueueB, FanoutExchange businessExchange) {return BindingBuilder.bind(businessQueueB).to(businessExchange);}// 声明死信队列A绑定关系@Beanpublic Binding deadLetterBindingA(Queue deadLetterQueueA, DirectExchange deadLetterExchange) {return BindingBuilder.bind(deadLetterQueueA).to(deadLetterExchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);}// 声明死信队列B绑定关系@Beanpublic Binding deadLetterBindingB(Queue deadLetterQueueB, DirectExchange deadLetterExchange) {return BindingBuilder.bind(deadLetterQueueB).to(deadLetterExchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);}
}

2.3 配置文件

application.yml

server:port: 8081spring:application:name: springboot-rabbitmq-dead-letterrabbitmq:# 此处不建议单独配置host和port,单独配置不支持连接RabbitMQ集群addresses: 127.0.0.1:5672username: guestpassword: guest# 虚拟host 可以不设置,使用server默认hostvirtual-host: /# 是否开启发送端消息抵达队列的确认publisher-returns: true# 发送方确认机制,默认为NONE,即不进行确认;SIMPLE:同步等待消息确认;CORRELATED:异步确认publisher-confirm-type: correlated# 消费者监听相关配置listener:simple:acknowledge-mode: manual # 确认模式,默认auto,自动确认;manual:手动确认default-requeue-rejected: false # 消费端抛出异常后消息是否返回队列,默认值为trueprefetch: 1 # 限制每次发送一条数据concurrency: 1 # 同一个队列启动几个消费者max-concurrency: 1 # 启动消费者最大数量# 重试机制retry:# 开启消费者(程序出现异常)重试机制,默认开启并一直重试enabled: true# 最大重试次数max-attempts: 3# 重试间隔时间(毫秒)initial-interval: 3000

2.4 生产者

为了方便测试,写一个简单的消息生产者,通过controller层来生产消息。

SendMessageController.java

import com.demo.config.RabbitMQConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** <p> @Title SendMessageController* <p> @Description 推送消息接口** @author ACGkaka* @date 2023/1/12 15:23*/
@Slf4j
@RestController
public class SendMessageController {/*** 使用 RabbitTemplate,这提供了接收/发送等方法。*/@Autowiredprivate RabbitTemplate rabbitTemplate;@GetMapping("/sendMessage")public String sendMessage(String message) {rabbitTemplate.convertAndSend(RabbitMQConfig.BUSINESS_EXCHANGE_NAME, "", message);return "OK";}
}

2.5 业务消费者

接下来是业务队列的消费端代码

BusinessMessageReceiver.java

import com.demo.config.RabbitMQConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.io.IOException;/*** <p> @Title BusinessMessageReceiver* <p> @Description RabbitMQ业务队列消费端** @author ACGkaka* @date 2024/1/7 17:43*/
@Slf4j
@Component
public class BusinessMessageReceiver {@RabbitListener(queues = RabbitMQConfig.BUSINESS_QUEUEA_NAME)public void receiveA(String body, Message message, Channel channel) throws IOException {log.info("业务队列A收到消息: {}", body);try {if (body.contains("deadletter")) {throw new RuntimeException("dead letter exception");}channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);} catch (Exception e) {log.error("业务队列A消息消费发生异常,error msg: {}", e.getMessage(), e);channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);}}@RabbitListener(queues = RabbitMQConfig.BUSINESS_QUEUEB_NAME)public void receiveB(String body, Message message, Channel channel) throws IOException {log.info("业务队列B收到消息: {}", body);channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}
}

2.6 死信消费者

接下来是死信队列的消费端代码

DeadLetterMessageReceiver.java

import com.demo.config.RabbitMQConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.io.IOException;/*** <p> @Title DeadLetterMessageReceiver* <p> @Description RabbitMQ死信队列消费端** @author ACGkaka* @date 2024/1/7 18:14*/
@Slf4j
@Component
public class DeadLetterMessageReceiver {@RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUEA_NAME)public void receiveA(String body, Message message, Channel channel) throws IOException {log.info("死信队列A收到消息: {}", body);channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}@RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUEB_NAME)public void receiveB(String body, Message message, Channel channel) throws IOException {log.info("死信队列B收到消息: {}", body);channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);}
}

2.7 测试结果

消费正常消息,请求结果:

请求地址:http://localhost:8081/sendMessage?message=Hello

在这里插入图片描述

从日志可以看到:两个业务队列成功消费

在这里插入图片描述

消费错误消息,请求结果:

请求地址:http://localhost:8081/sendMessage?message=deadletter

在这里插入图片描述

从日志可以看到:业务队列A和B都收到了消息,但是 业务队列A消费发生异常,然后消息就被 转到了死信队列死信队列消费端成功消费

在这里插入图片描述


三、总结

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。

死信消息 时 RabbitMQ 为我们做的一层保障,其实我们 也可以不使用死信队列,而是 在消息消费异常的时候,将消息主动投递到另一个交换机中,当你明白了这些之后,这些 Exchange 和 Queue 想怎样配合就可以怎样配合。比如:

  • 从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。
  • 或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费等等。

四、补充

4.1 启动报错 inequivalent arg ‘x-dead-letter-exchange’

如果之前已经存在队列,添加死信队列后可能会报错:

reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg ‘x-dead-letter-exchange’ for queue ‘MY_QUEUE’ in vhost ‘/’: received the value ‘MY_DEAD_LETTER_EXCHANGE’ of type ‘longstr’ but current is none

完整报错信息:

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'MY_QUEUE' in vhost '/': received the value 'MY_DEAD_LETTER_EXCHANGE' of type 'longstr' but current is none, class-id=50, method-id=10)at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:517)at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:341)at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:185)at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:117)at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:742)at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:47)at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:669)

修复方案:

将之前已经存在的队列删除后重启应用即可。

整理完毕,完结撒花~ 🌻





参考地址:

1.【RabbitMQ】一文带你搞定RabbitMQ死信队列,https://cloud.tencent.com/developer/article/1463065

相关文章:

RabbitMQ(九)死信队列

目录 一、简介1.1 定义1.2 何时进入死信队列&#xff1f;1.3 死信消息的变化1.4 死信队列的应用场景1.5 死信消息的生命周期 二、代码实现2.1 死信队列的配置步骤2.2 配置类2.3 配置文件2.4 生产者2.5 业务消费者2.6 死信消费者2.7 测试结果 三、总结四、补充4.1 启动报错 ineq…...

KEI5许可证没到期,编译却出现Error: C9555E: Failed to check out a license.问题解决

一、编译出现如下报错 二、检查一下许可证 三、许可证在许可日期内&#xff0c;故应该不是许可证的问题 四、检查一下编译器&#xff0c;我用的是这个&#xff0c;这几个编译器的区别其实我不太明白&#xff0c;但我把问题解决是选的这个 五、找到编译器的路径&#xff0c;去复…...

南京观海微电子----时序图绘制工具

Wavedrom 是一款功能强大且简单易用的文本转图表工具&#xff0c;被广泛应用于生成时序图、波形图等交互式波形。其特点在于使用简单的文本语法&#xff0c;使得开发人员能够以可视化的方式表示数字信号和时间序列数据。Wavedrom 的优势在于其高度灵活性和可扩展性&#xff0c;…...

Gin CORS 跨域请求资源共享与中间件

Gin CORS 跨域请求资源共享与中间件 文章目录 Gin CORS 跨域请求资源共享与中间件一、同源策略1.1 什么是浏览器的同源策略&#xff1f;1.2 同源策略判依据1.3 跨域问题三种解决方案 二、CORS:跨域资源共享简介(后端技术)三 CORS基本流程1.CORS请求分类2.基本流程 四、CORS两种…...

TS:.d.ts 文件 和 declare 的作用

1 declare 做外部声明1.1 声明外部类型1.2 声明外部模块1.2.1 解决引入资源模块报错1.2.2 跳过对第三方库的类型检查 1.3 声明外部变量1.4 声明外部命名空间&#xff08;作用域&#xff09; 2 .d.ts 文件做外部声明3 declare global {} 在模块中做外部声明 先说一下我对 .d.ts文…...

JavaScript-jQuery2-笔记

1.获取元素文本、属性、内部结构、表单中的值 获取标签中所夹的文本内容&#xff1a;text() 获取标签的属性值&#xff1a;prop(属性名) 获取表单元素的内容&#xff1a;如 文本框中的内容 val() 获取元素的内部html结构&#xff1a;html() 2.筛选选择器 筛选选择器&#xff1…...

设计模式之多线程版本的if------Balking模式

系列文章目录 设计模式之避免共享的设计模式Immutability&#xff08;不变性&#xff09;模式 设计模式之并发特定场景下的设计模式 Two-phase Termination&#xff08;两阶段终止&#xff09;模式 设计模式之避免共享的设计模式Copy-on-Write模式 设计模式之避免共享的设计模…...

mybatis核心配置文件介绍

mybatis核心配置文件 1. properties配置介绍 properties标签&#xff1a;加载外部的资源配置文件 ​ 属性&#xff1a;resource 指定要引入的配置文件路径 ​ 在核心配置文件中&#xff0c;通过&#xff1a;${key}方式引入外部配置文件的数据 jdbc.peroperties 的文件内容…...

Linux完全卸载Anaconda3和MiniConda3

如何安装Anaconda3和MiniConda3请看这篇文章&#xff1a; 安装Anaconda3和MiniConda3_minianaconda3-CSDN博客文章浏览阅读474次。MiniConda3官方版是一款优秀的Python环境管理软件。MiniConda3最新版只包含conda及其依赖项如果您更愿意拥有conda以及超过720个开源软件包&…...

Apache Answer,最好的开源问答系统

Apache Answer是一款适合任何团队的问答平台软件。无论是社区论坛、帮助中心还是知识管理平台&#xff0c;你可以永远信赖 Answer。 目前该项目在github超过10K星&#xff0c;系统采用go语言开发&#xff0c;安装配置简单&#xff0c;界面清洁易用&#xff0c;且开源免费。项目…...

【C】内存分配

首先&#xff0c;回顾一下内存分配。所有程序都必须预留足够的内存来存储程序使用的数据。这些内存中有些是自动分配的&#xff1a; float x; int place[100]; 这些声明预留了足够的空间&#xff0c;还为内存提供了一个标识符&#xff0c;可以使用x或place识别数据。 1、mal…...

MySQL 从零开始:03 基本入门语句

文章目录 1、连接数据库1.1 命令提示符登陆1.2 MySQL 8.0 Command Line Client 登陆1.3 MySQL Workbench 登陆 2、基本语句2.1 查看所有库2.2 创建库2.3 删除库2.4 选择数据库2.5 查看表2.6 创建表2.7 删除表2.8 改表名2.9 清空表 在上一小节中介绍了 MySQL 数据库的安装&#…...

井盖异动传感器,守护脚下安全

随着城市化进程的加速&#xff0c;城市基础设施的安全问题日益受到关注。其中&#xff0c;井盖作为城市地下管道的重要入口&#xff0c;其安全问题不容忽视。然而&#xff0c;传统的井盖监控方式往往存在盲区&#xff0c;无法及时发现井盖的异常移动。为此&#xff0c;我们推出…...

复合机器人作为一种新型的智能制造装备高效、精准和灵活的生产方式

随着汽车制造业的快速发展&#xff0c;对于高效、精准和灵活的生产方式需求日益增强。复合机器人作为一种新型的智能制造装备&#xff0c;以其独特的优势在汽车制造中发挥着越来越重要的作用。因此&#xff0c;富唯智能顺应时代的发展趋势&#xff0c;研发出了ICR系列的复合机器…...

重置 Docker 中 Gitlab 的账号密码

1、首先进入Docker容器 docker exec -it gitlab bash 2、连接到 gitlab 的数据库 需要谨慎操作 gitlab-rails console -e production 等待加载完后会进入控制台 ------------------------------------------------------------------------------------------------------…...

任务类型划分

以下内容来自于ChatGPT内存密集型应用和IO密集型应用是两种不同类型的计算应用&#xff0c;它们在资源需求和性能特点上有所不同。 内存密集型应用&#xff08;Memory-Intensive Applications&#xff09;&#xff1a; 特点&#xff1a; 这类应用主要依赖大量的内存资源来执行任…...

docker搭建部署mysql并挂载指定目录

Docker是一种轻量级、可移植的容器化平台&#xff0c;可以简化应用程序的部署和管理。在本文中&#xff0c;我们将探讨如何使用Docker来搭建和部署MySQL数据库&#xff0c;并将数据和配置文件挂载到外部目录&#xff0c;以实现数据持久化和方便的配置管理。 1: 安装Docker 首…...

即将推出的 OpenWrt One/AP-24.XY:OpenWrt 和 Banana Pi 合作路由器板

OpenWrt开发人员正在与Banana Pi合作开发OpenWrt One/AP-24.XY路由器板。OpenWrt 是一个轻量级嵌入式 Linux 操作系统&#xff0c;支持近 1,800 个路由器和其他设备。然而&#xff0c;这将是第一块由 OpenWrt 直接开发的路由器板。 该主板将基于 MediaTek MT7981B (Filogic 82…...

【uniapp-小程序-分享图5/4】

utils.js //裁剪分享的图片为5:4 const makeCanvas (imgUrl) > {console.log("imgUrl",imgUrl);return new Promise((resolve, reject) > {// 获取图片信息,小程序下获取网络图片信息需先配置download域名白名单才能生效uni.getImageInfo({src: imgUrl,succe…...

【响应式编程】前置知识和相关技术的总结

前置知识 这些概念都与响应式编程密切相关。&#x1f98c; 1. 并发和多线程编程&#xff1a;响应式编程需要处理并发性&#xff0c;它允许多个操作独立地并行执行。这使得应用程序可以在不同的线程、进程或设备上处理多个事件。 2. 事件驱动编程&#xff1a;响应式编程是一种…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

23-Oracle 23 ai 区块链表(Blockchain Table)

小伙伴有没有在金融强合规的领域中遇见&#xff0c;必须要保持数据不可变&#xff0c;管理员都无法修改和留痕的要求。比如医疗的电子病历中&#xff0c;影像检查检验结果不可篡改行的&#xff0c;药品追溯过程中数据只可插入无法删除的特性需求&#xff1b;登录日志、修改日志…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)

CSI-2 协议详细解析 (一&#xff09; 1. CSI-2层定义&#xff08;CSI-2 Layer Definitions&#xff09; 分层结构 &#xff1a;CSI-2协议分为6层&#xff1a; 物理层&#xff08;PHY Layer&#xff09; &#xff1a; 定义电气特性、时钟机制和传输介质&#xff08;导线&#…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止

<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet&#xff1a; https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

跨链模式:多链互操作架构与性能扩展方案

跨链模式&#xff1a;多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈&#xff1a;模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展&#xff08;H2Cross架构&#xff09;&#xff1a; 适配层&#xf…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...