RabbitMQ技术方案分析

方案分析

在上一篇文档中,详细讲述了如何通过Canal+MQ实现对分库分表的数据库和数据表进行数据同步,而在这个方案中,还有一个关键点是需要注意的:首先,数据增删改的信息是保证写入binlog的,Canal解析出增删改的信息后写入MQ,同步程序从MQ中读取消息,如果MQ中的消息丢失了数据将无法进行同步。

因此就需要对MQ传递消息的可靠性进行研究

保证MQ消息的可靠性分为两个方面:保证生产消息的可靠性、保证消费消息的可靠性

保证生产消息可靠性

RabbitMQ提供生产者确认机制保证生产消息的可靠性,技术方案如下:

1. 首先发送消息的方法如果执行失败会进行重试,重试次数耗尽记录消息失败

zhilian-framework包中的zhilian-rabbitmq工程下面的client包的消息处理类,用于发送消息

package com.zhilian.rabbitmq.client;import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.zhilian.common.expcetions.MqException;
import com.zhilian.common.utils.DateUtils;
import com.zhilian.common.utils.JsonUtils;
import com.zhilian.common.utils.NumberUtils;
import com.zhilian.rabbitmq.dao.FailMsgDao;
import com.zhilian.rabbitmq.plugins.DelayMessagePostProcessor;
import com.zhilian.rabbitmq.plugins.RabbitMqListenableFutureCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;import javax.annotation.Resource;/*** 消息处理类**/
@Slf4j
@Service
public class RabbitClient {@Resourceprivate RabbitTemplate rabbitTemplate;@Autowired(required = false)private FailMsgDao failMsgDao;@Resourceprivate RabbitClient rabbitClient;public void sendMsg(String exchange, String routingKey, Object msg) {rabbitClient.sendMsg(exchange, routingKey, msg, null, null, false);}/*** 发送消息 重试3次** @param exchange   交换机* @param routingKey 路由key* @param msg        消息对象,会将对象序列化成json字符串发出* @param delay      延迟时间 秒* @param msgId      消息id* @param isFailMsg  是否是失败消息* @return 是否发送成功*/@Retryable(value = MqException.class, maxAttempts = 3, backoff = @Backoff(value = 3000, multiplier = 1.5), recover = "saveFailMag")public void sendMsg(String exchange, String routingKey, Object msg, Integer delay, Long msgId, boolean isFailMsg) {// 1.发送消息前准备// 1.1获取消息内容,如果非字符串将其序列化String jsonMsg = JsonUtils.toJsonStr(msg);// 1.2.全局唯一消息id,如果调用者设置了消息id,使用调用者消息id,如果为配置,默认雪花算法生成消息idmsgId = NumberUtils.null2Default(msgId, IdUtil.getSnowflakeNextId());// 1.3.设置默认延迟时间,默认立即发送delay = NumberUtils.null2Default(delay, -1);log.debug("消息发送!exchange = {}, routingKey = {}, msg = {}, msgId = {}", exchange, routingKey, jsonMsg, msgId);// 1.4.构建回调RabbitMqListenableFutureCallback futureCallback = RabbitMqListenableFutureCallback.builder().exchange(exchange).routingKey(routingKey).msg(jsonMsg).msgId(msgId).delay(delay).isFailMsg(isFailMsg).failMsgDao(failMsgDao).build();// 1.5.CorrelationData设置CorrelationData correlationData = new CorrelationData(msgId.toString());correlationData.getFuture().addCallback(futureCallback);// 1.6.构造消息对象Message message = MessageBuilder.withBody(StrUtil.bytes(jsonMsg, CharsetUtil.CHARSET_UTF_8))//持久化.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//消息id.setMessageId(msgId.toString()).build();try {// 2.发送消息this.rabbitTemplate.convertAndSend(exchange, routingKey, message, new DelayMessagePostProcessor(delay), correlationData);} catch (Exception e) {log.error("send error:" + e);// 3.构建异常回调,并抛出异常MqException mqException = new MqException();mqException.setMsg(ExceptionUtil.getMessage(e));mqException.setMqId(msgId);throw mqException;}}/*** @param mqException mq异常消息* @param exchange    交换机* @param routingKey  路由key* @param msg         mq消息* @param delay       延迟消息* @param msgId       消息id*/@Recoverpublic void saveFailMag(MqException mqException, String exchange, String routingKey, Object msg, Integer delay, String msgId) {//发送消息失败,需要将消息持久化到数据库,通过任务调度的方式处理失败的消息failMsgDao.save(mqException.getMqId(), exchange, routingKey, JsonUtils.toJsonStr(msg), delay, DateUtils.getCurrentTime() + 10,  ExceptionUtil.getMessage(mqException));}}

@Retryable注解可实现方法执行失败进行重试,如下:

@Retryable(value = MqException.class, maxAttempts = 3, backoff = @Backoff(value = 3000, multiplier = 1.5), recover = "saveFailMag")

属性说明如下:

value:抛出制定异常才会重试

include:和value一样,默认为空,当exclude也为空时,默认所有异常

exclude:指定不处理的异常

maxAttempts:最大重试次数,默认3次

backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为3000L;表示第一次失败后等待3秒后重试,multiplier(指定延迟倍数)默认为0,如果把multiplier设置为1.5表示每次等待重试时间是上一次的1.5倍,则第一次重试为3秒,第二次为4.5秒,第三次为6.75秒。

Recover: 设置回调方法名,当重试耗尽时,通过recover属性设置回调的方法名。通过@Recover注解定义重试失败后的处理方法,在Recover方法中记录失败消息到数据库。

2. 通过MQ提供的生产者确认机制保证生产消息的可靠性

使用生产者确认机制需要给每个信息指定一个唯一ID,生产者确认机制通过异步回调的方式进行,包括ConfirmCallback和Return回调。

ConfirmCallback:消息发送到Broker会有一个结果返回给发送者表示消息是否处理成功:

消息成功投递到交换机,返回ack

消息未投递到交换机,返回nack

发送消息时指定回调对象

// 1.4.构建回调RabbitMqListenableFutureCallback futureCallback = RabbitMqListenableFutureCallback.builder().exchange(exchange).routingKey(routingKey).msg(jsonMsg).msgId(msgId).delay(delay).isFailMsg(isFailMsg).failMsgDao(failMsgDao).build();// 1.5.CorrelationData设置CorrelationData correlationData = new CorrelationData(msgId.toString());correlationData.getFuture().addCallback(futureCallback);// 1.6.构造消息对象Message message = MessageBuilder.withBody(StrUtil.bytes(jsonMsg, CharsetUtil.CHARSET_UTF_8))//持久化.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//消息id.setMessageId(msgId.toString()).build();

回调类:RabbitMqListenableFutureCallback

如果没有返回ack则将消息记录到失败消息表,如果经过重试后返回了ack说明消息发送成功,此时将消息从失败消息表删除。

package com.zhilian.rabbitmq.plugins;import cn.hutool.core.exceptions.ExceptionUtil;
import com.zhilian.common.utils.DateUtils;
import com.zhilian.rabbitmq.dao.FailMsgDao;
import lombok.Builder;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.util.concurrent.ListenableFutureCallback;@Builder
public class RabbitMqListenableFutureCallback implements ListenableFutureCallback<CorrelationData.Confirm> {//记录失败消息serviceprivate FailMsgDao failMsgDao;private String exchange;private String routingKey;private String msg;private Long msgId;private Integer delay;//是否是失败消息private boolean isFailMsg=false;@Overridepublic void onFailure(Throwable ex) {if(failMsgDao == null) {return;}failMsgDao.save(msgId, exchange, routingKey, msg, delay, DateUtils.getCurrentTime() + 10, ExceptionUtil.getMessage(ex));}@Overridepublic void onSuccess(CorrelationData.Confirm result) {if(failMsgDao == null){return;}if(!result.isAck()){// 执行失败保存失败信息,如果已经存在保存信息,如果不在信息信息failMsgDao.save(msgId, exchange, routingKey, msg, delay,DateUtils.getCurrentTime() + 10, "MQ回复nack");}else if(isFailMsg && msgId != null){// 如果发送的是失败消息,当收到ack需要从fail_msg删除该消息failMsgDao.removeById(msgId);}}
}

Return回调:如果消息发送到交换机成功了但是并没有到达队列,此时会调用ReturnCallback回调方法,在回调方法中我们可以收到失败的消息存入失败消息表以便进行补偿

要使用Return回调需要开启设置:(在shared-rabbitmq.yaml中配置rabbitMQ参数:

spring:rabbitmq:publisher-confirm-type: correlatedpublisher-returns: truetemplate:mandatory: true

说明:

publish-confirm-type:开启publisher-confirm,这里支持两种类型:

simple:同步等待confirm结果,直到超时

correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback

publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback

template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

对于发送消息失败之后将消息写入失败消息表的逻辑参考如下:

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {// 获取RabbitTemplateRabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//定义returnCallback回调方法rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {byte[] body = returnedMessage.getMessage().getBody();//消息idString messageId = returnedMessage.getMessage().getMessageProperties().getMessageId();String content = new String(body, Charset.defaultCharset());log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息id{},消息内容{}",returnedMessage.getReplyCode(),returnedMessage.getReplyText(),returnedMessage.getExchange(),returnedMessage.getRoutingKey(),messageId,content);if (failMsgDao != null) {failMsgDao.save(NumberUtils.parseLong(messageId), returnedMessage.getExchange(), returnedMessage.getRoutingKey(), content, null, DateUtils.getCurrentTime(), "returnCallback");}}});
}

保证消费信息可靠性

保证消费消息可靠性方案首先保证发送消息设置为持久化,其次通过MQ的消费确认机制保证消费者消费成功消息后再将消息删除。

首先设置消息持久化,保证消息发送到MQ消息不丢失。具体需要设置交换机和队列支持持久化,发送消息设置deliveryMode=2。

RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,向RabbitMQ发送ACK回执,表明自己已经处理完成消息,RabbitMQ收到ACK后删除消息。

消费消息失败重试3次,仍失败则将消费失败的消息放入失败消息数据库

通过任务调度扫描失败消息队列(错误消息队列)重新发送,达到一定的次数还未成功则由人工处理

核心代码实现:(从预定义的错误队列中取出之前处理失败的消息,重新发送到消息原本的目标地址

package com.zhilian.rabbitmq.plugins;import com.zhilian.common.utils.IoUtils;
import com.zhilian.common.utils.JsonUtils;
import com.zhilian.rabbitmq.domain.ErrorRabbitMqMessage;
import com.zhilian.rabbitmq.properties.RabbitmqProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.GetResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.core.RabbitTemplate;import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.charset.Charset;@Slf4j
public class RabbitMqResender {private RabbitTemplate rabbitTemplate;private RabbitmqProperties rabbitmqProperties;private Channel channel;public RabbitMqResender(RabbitTemplate rabbitTemplate, RabbitmqProperties rabbitmqProperties) {this.rabbitTemplate = rabbitTemplate;this.rabbitmqProperties = rabbitmqProperties;channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);}/*** 从队列中获取一条数据并处理,如果没有消息,返回false,有消息返回true*/public boolean getOneMessageAndProcess() {try {GetResponse response = channel.basicGet(rabbitmqProperties.getError().getQueue(), false);if(response == null) {return false;}ErrorRabbitMqMessage errorRabbitMqMessage = JsonUtils.toBean(new String(response.getBody()), ErrorRabbitMqMessage.class);Message message = MessageBuilder.withBody(errorRabbitMqMessage.getMessage().getBytes(Charset.defaultCharset())).build();rabbitTemplate.send(errorRabbitMqMessage.getOriginExchange(), errorRabbitMqMessage.getOriginRoutingKey(), message);channel.basicAck(response.getEnvelope().getDeliveryTag(), false);return true;}catch (IOException e) {log.error("消息重发失败,e:",e);return false;}}@PreDestroypublic void destory() {log.info("rabbitmq销毁...");IoUtils.close(channel);}
}
1. 核心功能//错误消息的捞取//从指定的错误队列(error.queue,由RabbitmqProperties配置)中拉取未被确认(unacknowledged)的消息。2. 消息解析与重发//将错误队列中的消息体解析为ErrorRabbitMqMessage对象(包含原始交换机、路由键、消息内容等元数据)。//使用RabbitTemplate将消息内容重新发送到原始的交换机(originExchange)和路由键(originRoutingKey)。
3. 异常处理与可靠性//解析失败:若消息无法解析为ErrorRabbitMqMessage,直接拒绝消息并丢弃(不重新入队),避免死循环。//发送失败:若重发过程中抛出异常(如网络问题),拒绝消息并允许重新入队,等待下次重试。//资源安全:通过@PreDestroy确保程序关闭时正确释放RabbitMQ的Channel和Connection,防止连接泄漏。

RabbitMQ提供三个确认模式:

•manual:手动ack,需要在业务代码结束后,调用api发送ack。

•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

•none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

这三种确认模式各有各的特点:

  • none模式下,消息投递是不可靠的,可能丢失
  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
  • manual:自己根据业务情况,判断什么时候该ack

本项目的配置:

spring:rabbitmq:....listener:simple:acknowledge-mode: auto #,出现异常时返回nack,消息回滚到mq;没有异常,返回ackretry:enabled: true # 开启消费者失败重试initial-interval: 1000 # 初识的失败等待时长为1秒multiplier: 10 # 失败的等待时长倍数,下次等待时长 = multiplier * last-intervalmax-attempts: 3 # 最大重试次数stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

本项目使用自动ack模式,当消费消息失败会重试,重试3次如果还失败会将消息投递到失败消息队列,由定时任务程序定时读取队列的消息,达到一定的次数还未成功则由人工处理。

保证消息幂等性

消费者在消费消息时难免出现重复消费的情况,比如:消费者没有向MQ返回ack导致重复消费,所以消费者需要保证消费信息幂等性

幂等性是指不论执行多少次其结果是一致的

比如:收到消息需要向数据新增一条记录,如果重复消费则会出现重复添加记录的问题。

根据场景分析解决方案:

  1. 查询操作:本身具有幂等性
  2. 添加操作:如果主键是自增则可能重复添加记录。

解决:保证幂等性可以设置数据库的唯一约束,比如添加学生信息,将学号字段设置为唯一索引,即使重复添加相同学生,同一个学号只会添加一条记录

  1. 更新操作:如果更新一个固定,比如update users set status = 1 where id = ?,本身就具有幂等性;

但是如果只允许更新成功一次?

解决:可以使用token机制,发送消息前生成一个token写入redis,收到消息后解析出token,从redis查询token,如果成功则说明没有消费,此时更新成功,将token从redis删除,当重复消费相同的消息时,由于token已经从redis删除不会再执行更新操作

  1. 删除操作:与更新操作类似,如果是删除某个具体的记录,比如:delete from users where id = ?,本身就具有幂等性,如果只允许删除成功一次就可以采用更新操作相同的方法(操作缓存token机制

根据以上分析:

为了保证消息幂等性,需要:

  1. 使用数据库的唯一约束去控制
  2. 使用token机制:
    1. 消息具有唯一ID
    2. 发送消息时将消息ID写入Redis
    3. 消费时根据消息ID查询Redis判断是否已经消费,如果已经消费则不再消费

能否百分百保证MQ消息可靠性?

当然不能!

保证消息可靠性分两个方面:保证生产消息可靠性和保证消费消息可靠性

保证生产消息可靠性:

生产消息可靠性是通过MQ是否发送ack回执来进行判断的。如果发nack表示发送消息失败,此时会进行重发或记录到失败消息表,通过定时任务进行补偿发送。

如果Java程序并没有收到回执(如jvm进程异常结束,或断电等因素),此时将无法保证生产消息的可靠性。

保证消费信息消息可靠性:

保证消费信息消息可靠性方案首先保证发送消息设置为持久化,其次通过MQ的消费确认机制保证消费者消费消息成功后再将消息删除。

虽然设置了消息持久化,消息进入MQ首先是在缓存存储,MQ会根据一定的规则进行刷盘,(比如每隔几毫秒进行刷盘,如果在消息还没有保存到磁盘时MQ进程终止,此时将会丢失消息)虽然可以使用镜像队列(用于在RabbitMQ集群中复制队列的消息,这样做的目的是提高队列的可用性和容错性,以防止在单个节点故障时导致消息的丢失)但是也不能百分百保证消息不丢失。

我们虽然加了很多的保证可靠性的机制,但是这只能去提高消息的可靠性,最终还是不能做到百分百的可靠,因此使用MQ的场景就必须要考虑消息可靠性问题的存在,做好补偿处理任务

如何保证Canal+MQ同步消息的顺序性

首先明确Canal解析binlog日志信息按顺序发到MQ的队列中,现在是要保证消费端如何按顺序消费队列中的消息。

生产中同一个服务会启动多个jvm进程,每个进程作为canal-mq-jzo2o-foundations的消费者,如下图:

现在对服务名称先修改为aa再修改为bb,在MQ中的有两个消息:

修改服务名称为aa

修改服务名称为bb

预期:最终将服务名称修改为bb

此时两条消息会被分发给两个jvm进程,假设“修改服务名称为aa”的消息发给jvm进程1,“修改服务名称为bb”的消息发给jvm进程2,两个进程分别去消费,此时无法控制两个消息的先后顺序,可能导致服务名称最终并非修改为bb。

解决方法:

多个jvm进程监听同一个队列保证只有消费者活跃,即只有一个消费者接收消息。

消费队列中的数据使用单线程。

队列需要增加x-single-active-consumer参数,表示否启用单一活动消费者模式

配置完成查保证队列上存在SAC标识,如下图:

当有多个jvm进程都去监听该队列时,只有一个为活跃状态

如果使用x-single-active-consumer参数需要修改为如下代码:

在Queue中添加:arguments={@Argument(name="x-single-active-consumer", value = "true", type = "java.lang.Boolean") }

如下所示:

@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "canal-mq-jzo2o-foundations",arguments={@Argument(name="x-single-active-consumer", value = "true", type = "java.lang.Boolean") }),exchange = @Exchange(name="exchange.canal-jzo2o",type = ExchangeTypes.TOPIC),key="canal-mq-jzo2o-foundations"),concurrency="1")
public void onMessage(Message message) throws Exception{parseMsg(message);
}

concurrency=”1“表示 指定消费线程为1。

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

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

相关文章

node.js版本管理

概述 遇到了版本升级后&#xff0c;以前项目不兼容的问题。 下载一个node.js的版本管理工具&#xff0c;官网下载地址&#xff0c;可以选择版本下载&#xff0c;我选择的1.11.1版本的。下载完成后点击安装&#xff0c;分别选择nvm安装目录和nodejs的安装目录&#xff0c;点击安…

leetcode-热题100(3)

leetcode-74-搜索二维矩阵 矩阵最后一列升序排序&#xff0c;在最后一列中查找第一个大于等于target的元素 然后在该元素所在行进行二分查找 bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target) {int n matrixSize;int m matrixColSize[0];in…

在HarmonyOS NEXT 开发中,如何指定一个号码,拉起系统拨号页面

大家好&#xff0c;我是 V 哥。 《鸿蒙 HarmonyOS 开发之路 卷1 ArkTS篇》已经出版上市了哈&#xff0c;有需要的朋友可以关注一下&#xff0c;卷2应用开发篇也马上要出版了&#xff0c;V 哥正在紧锣密鼓的写鸿蒙开发实战卷3的教材&#xff0c;卷3主要以项目实战为主&#xff0…

音视频入门基础:MPEG2-TS专题(26)——通过FFmpeg命令使用RTP发送TS流

音视频入门基础&#xff1a;MPEG2-TS专题系列文章&#xff1a; 音视频入门基础&#xff1a;MPEG2-TS专题&#xff08;1&#xff09;——MPEG2-TS官方文档下载 音视频入门基础&#xff1a;MPEG2-TS专题&#xff08;2&#xff09;——使用FFmpeg命令生成ts文件 音视频入门基础…

(六)窗口表面

这节主要三部分 一&#xff0c;窗口表面的创建和销毁 二&#xff0c;呈现队列 三&#xff0c;与以前实现的图形能力队列的兼容 一&#xff0c;窗口表面 1&#xff0c;创建 VkSurfaceKHR surface; //创建窗口表面 glfwCreateWindowSurface(instance, //vkInstance对象window, …

【零基础入门unity游戏开发——2D篇】SpriteEditor图片编辑器

考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…

云巅之上:数字文明的重构与超越

序章&#xff1a;算力新纪元 2024年初春&#xff0c;当SpaceX的星舰将首批云计算节点送入近地轨道时&#xff0c;地球上的数字原住民们正通过云端AI助手规划着一天的行程。这场静默的革命已悄然进入新阶段——云计算不再只是工具&#xff0c;而是成为数字文明的"第六元素…

【面试篇】多线程

基础概念 线程的生命周期有哪些状态&#xff1f;它们是如何转换的&#xff1f; 答案&#xff1a;线程的生命周期有以下六种状态&#xff1a; 新建&#xff08;New&#xff09;&#xff1a;线程被创建但尚未启动&#xff0c;此时线程对象已被分配内存空间&#xff0c;相关属性已…

unity运行中场景指定模型回放功能(模型是地形并且可以加载预制体进行回放)

回放和加载脚本 using System.Collections.Generic; using UnityEngine;public class TerrainRecorder : MonoBehaviour {[Header("基本设置")]public Terrain targetTerrain;public bool isRecording false;public bool isPlayingBack false;[Range(0.02f, 1f)] …

基于SpringBoot的河道水情大数据可视化分析平台设计与实现(源码+论文+部署讲解等)

需要资料&#xff0c;请文末联系 一、平台介绍 水情监测数据大屏 - 平台首页 日均水位 日均水速 二、论文内容 摘要&#xff08;中文&#xff09; 本文针对河道水情监测领域的数据管理和可视化分析需求&#xff0c;设计并实现了一套河道水情大数据可视化分析平台。该平台基…

Knife4j文档请求异常 空指针

打开swagger文档报空指针异常 java.lang.NullPointerException: nullat springfox.documentation.oas.mappers.SchemaMapper.model(SchemaMapper.java:97)at springfox.documentation.oas.mappers.SchemaMapper.mapModel(SchemaMapper.java:85)at springfox.documentation.oas…

车辆选择解决方案

车辆选择解决方案 /* * Purpose: 添加车辆选择的功能 -> 用户在选择不同的车辆时&#xff0c;重新初始化系统状态&#xff0c;清除之前的定时器&#xff0c;并根据新选择的车辆设置新的定时器&#xff0c;以实现对新车辆状态的实时加载。 * File Name: 车辆选择解决方案 * …

魔塔社区使用llamafactory微调AI阅卷试题系统

启动 LLaMA-Factory 1. 安装 LLaMA-Factory 执行安装指令 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e ".[torch,metrics]"解决依赖冲突 如果遇到依赖冲突&#xff0c;可使用以下命令安装&#xff0c;不…

程序化广告行业(51/89):Cookie映射与移动设备ID映射解析

程序化广告行业&#xff08;51/89&#xff09;&#xff1a;Cookie映射与移动设备ID映射解析 在当今数字化营销的浪潮中&#xff0c;程序化广告已经成为企业精准触达目标客户的重要手段。作为一名对程序化广告充满兴趣的学习者&#xff0c;我希望通过这篇博客和大家一起深入探索…

内网服务器centos7安装jdk17

1. 下载 JDK 17 安装包&#xff08;在外网环境操作&#xff09; 在可联网的机器上下载 JDK 17 的压缩包&#xff08;推荐使用 OpenJDK&#xff09;&#xff1a; OpenJDK 官方源&#xff1a; Adoptium Eclipse Temurin Azul Zulu 直接下载命令示例&#xff08;在外网机器上执行…

【学Rust写CAD】21 2D 点(point.rs)

源码 //matrix/point.rs use std::ops::Mul; use super::algebraic_units::{Zero, One}; use super::generic::Matrix;/// 点坐标结构体 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Point<X, Y>(Matrix<X, Y, One, Zero, Zero, One>);impl<X, Y>…

《AI大模型应知应会100篇》第7篇:Prompt Engineering基础:如何与大模型有效沟通

第7篇&#xff1a;Prompt Engineering基础&#xff1a;如何与大模型有效沟通 摘要 Prompt Engineering&#xff08;提示工程&#xff09;是与大模型高效沟通的关键技能。通过精心设计的Prompt&#xff0c;可以让模型生成更准确、更有用的结果。本文将从基础知识到高级策略&…

Java高频面试题1:Java SE

一、Java概述 1. Java语言的特点&#xff1f; 面向对象&#xff1a;封装、继承、多态。跨平台&#xff1a;通过JVM实现“一次编写&#xff0c;到处运行”。内存管理&#xff1a;自动垃圾回收&#xff08;GC&#xff09;&#xff0c;避免手动内存管理。多线程&#xff1a;内置…

基于RapidIO接口的DSP+GPU工业AI实时计算解决方案

基于RapidIO接口的DSPGPU工业AI实时计算解决方案是一种面向高性能、低延迟工业应用的异构计算架构&#xff0c;适用于工业自动化、机器视觉、预测性维护、机器人控制等场景。以下是该方案的核心设计思路和技术要点&#xff1a; 1. 方案背景与目标 工业需求&#xff1a; 工业…

SQL DB 数据类型

SQL DB 数据类型 引言 在数据库管理系统中,数据类型是定义和存储数据的方式。SQL(结构化查询语言)数据库中的数据类型决定了数据的存储格式、大小、取值范围以及如何处理数据。合理选择和使用数据类型对于确保数据库性能、数据完整性和应用程序的准确性至关重要。 SQL 数…