MQ - RabbitMQ - 消息的可靠性 --学习笔记

news2024/9/22 15:27:55

消息的可靠性

RabbitMQ 提供了一系列的特性和机制来确保消息的可靠性,即确保消息不丢失、按需到达目的地。要实现在 RabbitMQ 中消息的可靠性,可通过以下几个方面进行操作:

一、发送者的可靠性

1、生产者重试机制

什么是生产者重试机制?

消息队列(MQ)的生产者重试机制是指当生产者尝试将消息发送到消息队列中时,如果因为某些原因(如网络问题、队列不可用或其他任何导致消息发送失败的问题)导致消息未能成功发送,那么生产者会根据预设的策略尝试重新发送消息的过程。

如何实现生产者重试机制?

修改配置文件即可:

spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数

2、生产者确认机制

什么是生产者确认机制?

生产者确认机制是一种确保消息从生产者安全达到消息队列的方法,尤其是在使用像RabbitMQ这样的消息中间件时,这种机制尤为重要。生产者确认(Publisher Confirms)也被视为是一种可靠性保障,使得生产者在发送消息后可以知晓该消息是否已被消息队列成功接收。

在不使用生产者确认机制的情况下,生产者将消息发送出去之后,就没有办法知道消息是否正确到达队列。这在某些场景下可能会导致数据丢失。因此,通过实现生产者确认机制,可以显著增加消息系统的可靠性。

如何实现生产者确认机制?

RabbitMQ提供的生产者消息确认机制包括Publisher ConfirmPublisher Return两种。在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执

  • 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功

  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功

  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功

  • 其它情况都会返回NACK,告知投递失败 

其中acknack属于Publisher Confirm机制,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。

默认两种机制都是关闭状态,需要通过配置文件来开启:

spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

这里publisher-confirm-type有三种模式可选:

  • none:关闭confirm机制

  • simple:同步阻塞等待MQ的回执

  • correlated:MQ异步回调返回回执

一般推荐使用correlated模式,兼顾了性能可靠性

ReturnCallback

ReturnCallback是RabbitMQ中Java客户端库提供的一个接口,用于处理不能被路由到合适队列的消息。每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置:

package com.itheima.publisher.config;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
    private final RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                log.error("触发return callback,");
                log.debug("exchange: {}", returned.getExchange());
                log.debug("routingKey: {}", returned.getRoutingKey());
                log.debug("message: {}", returned.getMessage());
                log.debug("replyCode: {}", returned.getReplyCode());
                log.debug("replyText: {}", returned.getReplyText());
            }
        });
    }
}

ConfirmCallback 

ConfirmCallback是RabbitMQ在Java客户端库中提供的一个接口,用于处理消息确认情况。当生产者将消息发送到RabbitMQ服务器时,可以通过实现ConfirmCallback接口来监听这些消息是否成功被服务器接收(acknowledged)或被拒绝(nacked)。这是一种异步通信模式,允许生产者在不阻塞当前线程的情况下获得消息发送后的状态反馈。由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发消息时定义。具体来说,是在调用RabbitTemplate中的convertAndSend方法时,多传递一个参数:

这里的CorrelationData中包含两个核心的东西:

  • id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆

  • SettableListenableFuture:回执结果的Future对象

将来MQ的回执就会通过这个Future来返回,我们可以提前给CorrelationData中的Future添加回调函数来处理消息回执:

示例,新建一个测试,向系统自带的交换机发送消息,并且添加ConfirmCallback: 

@Test
void testPublisherConfirm() {
    // 1.创建CorrelationData
    CorrelationData cd = new CorrelationData();
    // 2.给Future添加ConfirmCallback
    cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
        @Override
        public void onFailure(Throwable ex) {
            // 2.1.Future发生异常时的处理逻辑,基本不会触发
            log.error("send message fail", ex);
        }
        @Override
        public void onSuccess(CorrelationData.Confirm result) {
            // 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
            if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
                log.debug("发送消息成功,收到 ack!");
            }else{ // result.getReason(),String类型,返回nack时的异常描述
                log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
            }
        }
    });
    // 3.发送消息
    rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}

二、MQ的可靠性

1、数据持久化

为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置数据持久化,包括:

  • 交换机持久化

  • 队列持久化

  • 消息持久化

在控制台配置数据持久化:

交换机&队列:添加时配置Durability参数为Durable就是持久化模式,Transient就是临时模式。

消息:发送消息时配置Dilivery mode参数为persistent即可

在java客户端配置数据持久化:

交换机&队列:创建时配置属性durable为true即可

消息:发送时设置MessageProperties.PERSISTENT_TEXT_PLAIN属性:

String message = "Hello World!";
channel.basicPublish("my_exchange", "routing_key", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

注意:尽管消息标记为持久化,RabbitMQ也不会为每条消息都做fsync操作,因为这样会严重影响性能。RabbitMQ会按照自己的节奏批量地将消息写入磁盘。 

2、LazyQueue

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下,这会导致消息积压,比如:

  • 消费者宕机或出现网络故障

  • 消息发送量激增,超过了消费者处理速度

  • 消费者处理业务发生阻塞

一旦出现消息堆积问题,RabbitMQ的内存占用就会越来越高,直到触发内存预警上限。此时RabbitMQ会将内存消息刷到磁盘上,这个行为成为PageOut. PageOut会耗费一段时间,并且会阻塞队列进程。因此在这个过程中RabbitMQ不会再处理新的消息,生产者的所有请求都会被阻塞。

为了解决这个问题,从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的模式,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存

  • 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)

  • 支持数百万条的消息存储

而在3.12版本之后,LazyQueue已经成为所有队列的默认格式。因此官方推荐升级MQ为3.12版本或者所有队列都设置为LazyQueue模式。

控制台配置Lazy模式

在添加队列的时候,添加x-queue-mod=lazy参数即可设置队列为Lazy模式:

代码配置Lazy模式

在利用SpringAMQP声明队列的时候,添加x-queue-mod=lazy参数也可设置队列为Lazy模式:

@Bean
public Queue lazyQueue(){
    return QueueBuilder
            .durable("lazy.queue")
            .lazy() // 开启Lazy模式
            .build();
}

这里是通过QueueBuilderlazy()函数配置Lazy模式,底层源码如下:

也可以基于注解来声明队列并设置为Lazy模式:

@RabbitListener(queuesToDeclare = @Queue(
        name = "lazy.queue",
        durable = "true",
        arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){
    log.info("接收到 lazy.queue的消息:{}", msg);
}

三、消费者的可靠性

1、消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息

  • nack:消息处理失败,RabbitMQ需要再次投递消息

  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

一般reject方式用的较少,除非是消息格式有问题,那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch机制捕获,消息处理成功时返回ack,处理失败时返回nack.

由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:

  • none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用

  • manual:手动模式。需要自己在业务代码中调用api,发送ackreject,存在业务入侵,但更灵活

  • auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:

    • 如果是业务异常,会自动返回nack

    • 如果是消息处理或校验异常,自动返回reject;

通过下面的配置可以修改SpringAMQP的ACK处理方式:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 不做处理

2、失败重试机制

 当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。

极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力:

当然,上述极端情况发生的概率还是非常低的,不过不怕一万就怕万一。为了应对上述情况Spring又提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

修改consumer服务的application.yml文件,添加内容:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000ms # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

3、失败处理策略

在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。

因此Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

定义处理失败消息的交换机和队列,定义一个RepublishMessageRecoverer,关联队列和交换机:

package com.itheima.consumer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;

@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
    @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }
    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue", true);
    }
    @Bean
    public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
        return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
    }

    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}

4、业务幂等性

何为幂等性?

幂等是一个数学概念,用函数表达式来描述是这样的:f(x) = f(f(x)),例如求绝对值函数。

在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:

  • 根据id删除数据

  • 查询数据

  • 新增数据

但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:

  • 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况

  • 退款业务。重复退款对商家而言会有经济损失。

所以,我们要尽可能避免业务被重复执行。

然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况,例如:

  • 页面卡顿时频繁刷新导致表单重复提交

  • 服务间调用的重试

  • MQ消息的重复投递

因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:

  • 唯一消息ID

这个思路非常简单:

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。

  2. 消费者接收到消息后处理业务,业务处理成功后将消息ID保存到数据库

  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。

我们该如何给消息添加唯一ID呢?

其实很简单,SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。

以Jackson的消息转换器为例:

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jjmc.setCreateMessageIds(true);
    return jjmc;
}
  • 业务状态判断

业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息,不同的业务场景判断的思路也不一样。

例如我们当前案例中,处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理。

相比较而言,消息ID的方案需要改造原有的数据库,所以更推荐使用业务判断的方案。

5、兜底方案

虽然我们利用各种机制尽可能增加了消息的可靠性,但也不好说能保证消息100%的可靠。万一真的MQ通知失败该怎么办呢?

有没有其它兜底方案,能够确保订单的支付状态一致呢?

其实思想很简单:既然MQ通知不一定发送到消费者,那就消费者定时去查询相关生产者状态,这样即便MQ通知失败,还可以利用定时任务作为兜底方案,确保状态的最终一致性。

而这就需要使用到延迟消息的技术:

延迟消息

延迟消息是指在消息队列(MQ)系统中允许消息在一定时间后才被消费者消费的消息。这意味着,消息被发送到消息队列后,并不会立即被消费者处理,而是在指定的延迟时间之后才可用于消费。这种机制允许开发者在设计系统时,能够处理那些需要延迟执行的任务,例如定时任务、延迟通知、过期订单的处理等。

在RabbitMQ中实现延迟消息也有两种方案:

  • 死信交换机+TTL

  • 延迟消息插件

死信交换机+TTL

首先,什么是死信?

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.rejectbasic.nack声明消费失败,并且消息的requeue参数设置为false

  • 消息是一个过期消息,超时无人消费

  • 要投递的队列消息满了,无法投递

那么,什么是死信交换机呢?

如果一个队列中的消息已经成为死信,并且这个队列通过dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机(Dead Letter Exchange)。

TTL(Time-To-Live,生存时间):是指消息或队列存在的时长。

那么如何通过死信交换机+TTL实现延迟消息呢?

设想一下这样的场景:

如图,有一组绑定的交换机(ttl.fanout)和队列(ttl.queue)。但是ttl.queue没有消费者监听,而是设定了死信交换机hmall.direct,而队列direct.queue1则与死信交换机绑定,RoutingKey是blue:

假如我们现在发送一条消息到ttl.fanout,RoutingKey为blue,并设置消息的有效期为5000毫秒,消息肯定会被投递到ttl.queue之后,由于没有消费者,因此消息无人消费。5秒之后,消息的有效期到期,成为死信,死信被再次投递到死信交换机hmall.direct,并沿用之前的RoutingKey,也就是blue,由于direct.queue1hmall.direct绑定的key是blue,因此最终消息被成功路由到direct.queue1,如果此时有消费者与direct.queue1绑定, 也就能成功消费消息了。但此时已经是5秒钟以后了:

 也就是说,publisher发送了一条消息,但最终consumer在5秒后才收到消息。我们成功实现了延迟消息

DelayExchange插件

基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。

插件下载地址:rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ (github.com)

下载好后将插件上传到RabbitMQ的插件目录对应的数据卷,然后执行命令安装插件(docker):

docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

在java客户端声明延迟交换机

基于注解方式:

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(name = "delay.direct", delayed = "true"),
        key = "delay"
))
public void listenDelayMessage(String msg){
    log.info("接收到delay.queue的延迟消息:{}", msg);
}

基于@Bean的方式:

package com.itheima.consumer.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class DelayExchangeConfig {

    @Bean
    public DirectExchange delayExchange(){
        return ExchangeBuilder
                .directExchange("delay.direct") // 指定交换机类型和名称
                .delayed() // 设置delay的属性为true
                .durable(true) // 持久化
                .build();
    }

    @Bean
    public Queue delayedQueue(){
        return new Queue("delay.queue");
    }
    
    @Bean
    public Binding delayQueueBinding(){
        return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
    }
}

发送延迟消息

发送消息时,必须通过x-delay属性设定延迟时间:

@Test
void testPublisherDelayMessage() {
    // 1.创建消息
    String message = "hello, delayed message";
    // 2.发送消息,利用消息后置处理器添加消息头
    rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            // 添加延迟消息属性
            message.getMessageProperties().setDelay(5000);
            return message;
        }
    });
}

注意:

延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。

因此,不建议设置延迟时间过长的延迟消息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1934421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch:如何选择向量数据库?

作者&#xff1a;来自 Elastic Elastic Platform Team 向量数据库领域是一个快速发展的领域&#xff0c;它正在改变我们管理和搜索数据的方式。与传统数据库不同&#xff0c;向量数据库以向量的形式存储和管理数据。这种独特的方法可以实现更精确、更相关的搜索&#xff0c;并允…

Spire.PDF for .NET【文档操作】演示:如何在 C# 中切换 PDF 层的可见性

我们已经演示了如何使用 Spire.PDF在 C# 中向 PDF 文件添加多个图层以及在 PDF 中删除图层。我们还可以在 Spire.PDF 的帮助下在创建新页面图层时切换 PDF 图层的可见性。在本节中&#xff0c;我们将演示如何在 C# 中切换新 PDF 文档中图层的可见性。 Spire.PDF for .NET 是一…

Web开发:ASP.NET CORE前后端交互之AJAX(含基础Demo)

目录 一、后端 二、前端 三、代码位置 四、实现效果 五、关键的点 1.后端传输给前端&#xff1a; 2.前端传输给后端 一、后端 using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using WebAppl…

24暑假算法刷题 | Day16 | LeetCode 513. 找树左下角的值,112. 路径总合,106. 从中序和后序遍历序列构造二叉树

目录 513. 找树左下角的值题目描述题解 112. 路径总合题目描述题解 106. 从中序和后序遍历序列构造二叉树题目描述题解 513. 找树左下角的值 点此跳转题目链接 题目描述 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至…

React@16.x(62)Redux@4.x(11)- 中间件2 - redux-thunk

目录 1&#xff0c;介绍举例 2&#xff0c;原理和实现实现 3&#xff0c;注意点 1&#xff0c;介绍 一般情况下&#xff0c;action 是一个平面对象&#xff0c;并会通过纯函数来创建。 export const createAddUserAction (user) > ({type: ADD_USER,payload: user, });这…

网络安全----防御----防火墙双机热备

实验要求&#xff1a; 1&#xff0c;对现有网络进行改造升级&#xff0c;将当个防火墙组网改成双机热备的组网形式&#xff0c;做负载分担模式&#xff0c;游客区和DMZ区走FW4&#xff0c;生产区和办公区的流量走FW1 2&#xff0c;办公区上网用户限制流量不超过100M&#xff0…

记录一下在Hyper-v中动态磁盘在Ubuntu中不完全用到的问题(扩展根目录)

在之前给hyper虚拟机的Ubuntu分配磁盘有20G&#xff1b; 后来在Ubuntu中查看磁盘发现有一个分区没用到&#xff1a; 贴的图片是完成扩展后的 之前这里是10G&#xff0c;然后有个dev/sda4的分区&#xff0c;也是10G&#xff0c;Type是Microsoft Basic Data&#xff1b; …

健康问题查询找搜索引擎还是大模型

随着自然语言处理&#xff08;NLP&#xff09;的最新进展&#xff0c;大型语言模型&#xff08;LLMs&#xff09;已经成为众多信息获取任务中的主要参与者。然而&#xff0c;传统网络搜索引擎&#xff08;SEs&#xff09;在回答用户提交的查询中的作用远未被取代。例如&#xf…

云计算实训室的核心功能有哪些?

在当今数字化转型浪潮中&#xff0c;云计算技术作为推动行业变革的关键力量&#xff0c;其重要性不言而喻。唯众&#xff0c;作为教育实训解决方案的领先者&#xff0c;深刻洞察到市场对云计算技能人才的迫切需求&#xff0c;精心打造了云计算实训室。这一实训平台不仅集成了先…

基于电鸿(电力鸿蒙)的边缘计算网关,支持定制

1 产品信息 边缘计算网关基于平头哥 TH1520 芯片&#xff0c;支持 OpenHarmony 小型系统&#xff0c;是 连接物联网设备和云平台的重要枢纽&#xff0c;可应用于城市基础设施&#xff0c;智能工厂&#xff0c;智能建筑&#xff0c;营业网点&#xff0c;运营 服务中心相关场…

PostgreSQL 中如何解决因大量并发读取导致的缓存命中率下降?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 PostgreSQL 中如何解决因大量并发读取导致的缓存命中率下降一、了解 PostgreSQL 缓存机制二、分析缓存…

人工智能导论-神经网络

神经网络 概述 本章主要介绍人工神经网络的基本概念&#xff0c;以及几种重要模型&#xff0c;包括“单层感知机、两层感知机、多层感知机”等。 在此基础上&#xff0c;介绍两种重要的基础神经网络“Hopfield神经网络、BP神经网络”。 最后&#xff0c;着重介绍了深度学习…

Java跨平台的原理是什么?JDK,JRE,JVM三者的作用和区别?xxx.java和xxx.class有什么区别?看这一篇就够了

目录 1. Java跨平台相关问题 1.1 什么是跨平台(平台无关性)&#xff1f; 1.2 跨平台(平台无关性)的好处&#xff1f; 1.3 编译原理基础&#xff08;Java程序编译过程&#xff09; 1.4Java跨平台的是实现原理&#xff1f; 1.4.1 JVM(Java虚拟机) 1.4.2 Class文件 1.4.3 …

是德keysight N9020B(原Agilent) N9020A信号频谱分析仪

Agilent N9020B N9020B信号分析仪手持信号分析仪 N9020B MXA 信号分析仪&#xff0c;10 Hz 至 26.5 GHz 主要特性和功能快速适应无线器件不断演进的测试要求通过硬件加速功率测量缩短测试时间&#xff0c;显示更新速率快&#xff0c;并且具有游标峰值搜索和快速扫描功能X 系列…

el-select选择器修改背景颜色

<!--* FilePath: topSearch.vue* Author: 是十九呐* Date: 2024-07-18 09:46:03* LastEditTime: 2024-07-18 10:42:03 --> <template><div class"topSearch-container"><div class"search-item"><div class"item-name&quo…

ROS2从入门到精通2-3:机器人3D物理仿真Gazebo与案例分析

目录 0 专栏介绍1 什么是Gazebo&#xff1f;2 Gazebo架构2.1 Gazebo前后端2.2 Gazebo文件格式2.3 Gazebo环境变量 3 Gazebo安装与基本界面4 搭建自己的地图4.1 编辑地图4.2 保存地图4.3 加载地图 5 常见问题 0 专栏介绍 本专栏旨在通过对ROS2的系统学习&#xff0c;掌握ROS2底…

Java案例斗地主游戏

目录 一案例要求&#xff1a; 二具体代码&#xff1a; 一案例要求&#xff1a; &#xff08;由于暂时没有学到通信知识&#xff0c;所以只会发牌&#xff0c;不会设计打牌游戏&#xff09; 二具体代码&#xff1a; Ⅰ&#xff1a;主函数 package three;public class test {…

ExoPlayer架构详解与源码分析(15)——Renderer

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…

SpringData JPA Mongodb 查询部分字段

JPA 网上用的好像不多&#xff0c;找了好多材料以后最终找了这个可行的方案&#xff1a; Query(fields "{tender_id:1,_id:0}")List<MGPltTender> findByTenderIdIsNotNull(PageRequest pageRequest); 调用&#xff1a; Sort sort Sort.by(popularType.getC…

android串口通讯(JAVA)

一、app目录下添加 implementation io.github.xmaihh:serialport:2.1.1 1) 点击Sync Now更新依赖 2) AndroidManifest.xml文件添加读取设备信息权限 <uses-permission android:name"android.permission.READ_PHONE_STATE" /> 二、 使用 1) 创建MySerialPo…