从零手搓一个【消息队列】实现消息在文件中的存储

news2024/11/26 0:51:20

文章目录

  • 一、序列化 / 反序列化
  • 二、文件存储设计
    • 1, 队列目录
    • 2, 消息数据文件
    • 3, 消息统计文件
  • 三、硬盘管理 -- 文件
    • 1, 创建 MessageFileManager 类
    • 2, createQueueFiles() 创建目录/文件
    • 3, deleteFiles() 删除目录/文件
    • 4, checkFileExists() 检查目录/文件是否存在
    • 5, readStat() 从 stat.txt 文件中读数据
    • 6, writeStat() 从 stat.txt 文件中写数据
    • 7, sendMessage() 发送消息
    • 8, deleteMessage() 删除消息
    • 9, isNeedGC() 是否需要垃圾回收
    • 10, gc() 垃圾回收
    • 11, loadAllMessageFromQueue() 加载所有有效消息
  • 四、小结


创建 Spring Boot 项目, Spring Boot 2 系列版本, Java 8 , 引入 MyBatis, Lombok 依赖

提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

整体目录结构 :
在这里插入图片描述

本文主要实现 server 包


一、序列化 / 反序列化

在这里插入图片描述

Message 对象(或后续的网络请求需要的数据)需要转成⼆进制写⼊⽂件. 并且也需要把⽂件中的⼆进制读出来解析成对象. 此处针对这⾥的逻辑进⾏封装

  • 序列化: 对象 --> 二进制数据
  • 反序列化: 二进制数据 --> 对象

此处使用 Java 标准库提供的序列化 / 反序列化工具, 使⽤ ObjectInputStream 类 / ObjectOutputStream 类进⾏序列化 / 反序列化操作. 通过内部的 readObject / writeObject 即可完成对应操作


    // 序列化
    public static byte[] toByteArray(Object object) throws IOException {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            try (ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream)) {
                outputStream.writeObject(object);
            }
            return byteArrayOutputStream.toByteArray();
        }
    }

    // 反序列化
    public static Object toObject(byte[] array) throws IOException {
        Object object = null;
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(array)) {
            try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
                object = objectInputStream.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return object;
    }
  • 使⽤ ByteArrayInputStream / ByteArrayOutputStream 针对 byte[] 进⾏封装, 相当于提供了一个变长字节数组, (序列化之前不知道这个对象转成字节数组之后有多长, 直接拿 ByteArrayOutputStream 接收即可 )

  • 这两个流对象是纯内存的, 不需要进⾏ close, 此处仍然套上了 try() 也没什么副作用, 养成好习惯~

  • readObject 和 writeObject 这两个方法表示"从哪读", “写到哪”, 取决于和哪个流对象关联, 此处是和 byteArrayOutputStream / byteArrayInputStream, 所以表示"从字节数组里读". “写到字节数组” , 如果和网络( socket 的 输入输出流) 关联, 那就是"从网卡里读", “写到网卡里”

补充说明, 有些序列化 / 反序列场景可以引入"serialVersionUID “, 表示"版本号”, 如下代码所示
public static final long serialVersionUID = 1L;
实际开发中, 代码可能会经常修改和调整, 比如今天定义好了 Message 类, 序列化后写入了文件, 过段时间有给这个类扩充了新功能, 然后再从文件中反序列化出来 message 对象, 大概率是错的, 即使程序不报错, 数据格式也会错乱
为了防止这种情况发生, 修改 Message 类后就把 serialVersionUID 手动更新, 这样再序列化 / 反序列化, 如果版本号不一致, 就会直接报错, 显式的提醒程序员, 而不是使用错乱的数据执行后续代码


二、文件存储设计

1, 队列目录

消息需要在硬盘上存储. 但是并不直接放到数据库中, ⽽是直接使⽤⽂件存储
原因如下:

  1. 对于消息的操作并不需要复杂的 增删改查
  2. 对于⽂件的操作效率⽐数据库会⾼很多

主流 MQ 的实现(包括 RabbitMQ), 都是把消息存储在⽂件中, ⽽不是数据库中

我们给每个队列分配⼀个⽬录. ⽬录的名字为 data + 队列名. 形如 ./data/testQueue
该⽬录中包含两个固定名字的⽂件.

  • queue_data.txt 消息数据⽂件, ⽤来保存消息内容
  • queue_stat.txt 消息统计⽂件, ⽤来保存消息统计信息
    在这里插入图片描述

服务器中可以有 N 个目录, 也就可以有 N 个 queue 目录


2, 消息数据文件

queue_data.txt ⽂件格式 : 使⽤⼆进制⽅式存储, 每个消息分成两个部分

  • 前四个字节, 表⽰ Message 对象的⻓度(字节数)
  • 后⾯若⼲字节, 表⽰ Message 内容

消息和消息之间⾸尾相连, 每个 Message 基于 Java 标准库的 ObjectInputStream / ObjectOutputStream 序列化

在这里插入图片描述

新增和删除消息时, 就需要知道文件中消息的位置, 上篇文章设计核心类的时候, Message 这个类中就给了两个成员属性表示 “偏移量”, 并且使用 transient 修饰, 不被序列化

    // 消息存储在文件中的偏移量(字节, 约定 "[,)" 区间 )
    private transient long offsetBegin = 0;
    private transient long offsetEnd = 0;

在这里插入图片描述


3, 消息统计文件

queue_stat.txt ⽂件格式: 使⽤⽂本⽅式存储
⽂件中只包含⼀⾏, ⾥⾯包含两列(都是整数), 使⽤ \t 分割
第⼀列表⽰当前总的消息数⽬. 第⼆列表⽰有效消息数⽬
形如: 100\t50


三、硬盘管理 – 文件

在这里插入图片描述

1, 创建 MessageFileManager 类

充分结合面向对象思想, 创建一个内部类 Stat 表示消息统计文件中, 定义两个成员属性 totalCount, validCount 表示消息总数和有效消息数

写出获取队列目录, 消息文件, 统计文件的路径

public class MessageFileManager {
    /**
     * 内部类 表示 queue_stat.txt 文件中需要的的字段
     */
     public static class Stat {
        public int totalCount; // 总消息数
        public int validCount; // 有效消息数
    }

    // 获取队列的存储目录
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

    // 获取该队列的消息存储数据文件
    private String getQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    // 获取该队列的消息存储统计文件
    private String getQueueStatPath(String queueName) {
        return getQueueDir(queueName) + "/queue_stat.txt";
    }
}

2, createQueueFiles() 创建目录/文件

创建文件之后别忘了在统计文件中初始化 totalCount 和 validCount

	public void createQueueFiles(String queueName) throws IOException {
        File queueDir = new File(getQueueDir(queueName));
        if (!queueDir.exists()) {
            boolean result1 = queueDir.mkdirs();
            if (!result1) {
                throw new IOException("[MessageFileManager.createQueueFiles()] 创建目录失败: " + 
                        queueDir.getAbsolutePath());
            }
        }
        File dataPath = new File(getQueueDataPath(queueName));
        if (!dataPath.exists()) {
            boolean result2 = dataPath.createNewFile();
            if (!result2) {
                throw new IOException("[MessageFileManager.createQueueFiles()] 创建data文件失败: " + 
                        dataPath.getAbsolutePath());
            }
        }
        File statPath = new File(getQueueStatPath(queueName));
        if (!statPath.exists()) {
            boolean result3 = statPath.createNewFile();
            if (!result3) {
                throw new IOException("[MessageFileManager.createQueueFiles()] 创建stat文件失败: " +
                        statPath.getAbsolutePath());
            }
        }
        // 给 stat.txt 文件中写入默认数据
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName, stat);
    }

3, deleteFiles() 删除目录/文件

如果服务器要删除某个队列, 那么这个队列的消息的相关数据文件也要删除

File 类的 delete 方法只能删除空目录, 因此需要先把内部的闻件先删除掉先删除文件再删除目录

    public void deleteFiles(String queueName) throws IOException {
        File dataPath = new File(getQueueDataPath(queueName));
        boolean result1 = dataPath.delete();
        File statPath = new File(getQueueStatPath(queueName));
        boolean result2 = statPath.delete();
        File queueDir = new File(getQueueDir(queueName));
        boolean result3 = queueDir.delete();
        if (!(result1 && result2 && result3)) {
            throw new IOException("[MessageFileManager.deleteFiles()] 删除目录文件失败: " +
                    queueDir.getAbsolutePath());
        }
    }

4, checkFileExists() 检查目录/文件是否存在

判定该队列的消息⽂件和统计⽂件是否存在. ⼀旦出现缺失, 则不能进⾏后续⼯作

    public boolean checkFileExists(String queueName) {
        File queueDir = new File(getQueueDir(queueName));
        File dataPath = new File(getQueueDataPath(queueName));
        File statPath = new File(getQueueStatPath(queueName));
        return queueDir.exists() && dataPath.exists() && statPath.exists();
    }

5, readStat() 从 stat.txt 文件中读数据

	private Stat readStat(String queueName) {
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

6, writeStat() 从 stat.txt 文件中写数据

    private void writeStat(String queueName, Stat stat) {
        // 使用 PrintWrite 来写文件.
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter writer = new PrintWriter(outputStream);
            writer.write(stat.totalCount + "\t" + stat.validCount);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

7, sendMessage() 发送消息

自定义一个异常类

public class MQException extends Exception {
    public MQException(String message) {
        super(message);
    }
}

这里的发送消息, 并不是把生产者把消息发送给服务器, 而是这个把消息写入到文件存储起来, 只是作为发送消息这个功能的背后的一环

最后别忘了更新 stat 统计文件中的数据

	public void sendMessage(MessageQueue queue, Message message) throws IOException, MQException {
        // 1, 检查目录和文件是否存在
        boolean result = checkFileExists(queue.getName());
        if (!result) {
            throw new MQException("[MessageFileManager.sendMessage()] queueName=" + queue.getName() + "的文件不存在");
        }
        // 2, 消息序列化
        byte[] messageBinary = BinaryUtil.toByteArray(message);
        synchronized (queue) {
            // 3, 设置消息在文件中的偏移量
            String dataPath = getQueueDataPath(queue.getName());
            File dataFile = new File(dataPath);
            message.setOffsetBegin(dataFile.length() + 4);
            message.setOffsetEnd(dataFile.length() + 4 + messageBinary.length);
            // 4, 写入文件
            try (OutputStream outputStream = new FileOutputStream(dataPath, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    dataOutputStream.writeInt(messageBinary.length); // 消息长度(4字节)
                    dataOutputStream.write(messageBinary); // 消息本体(messageBinary.length字节)
                }
            }
            // 5, 更新 stat.txt 文件中的数据
            Stat stat = readStat(queue.getName());
            stat.validCount += 1;
            stat.totalCount += 1;
            writeStat(queue.getName(), stat);
        }
    }
  • 考虑线程安全, 按照队列维度进⾏加锁
  • 使⽤ DataOutputStream 进⾏⼆进制写操作. ⽐原⽣ OutputStream 要⽅便
  • 需要记录 Message 对象在⽂件中的偏移量. 后续的删除操作依赖这个偏移量定位到消息. offsetBegin 是原有⽂件⼤⼩的基础上, 再 + 4, 4 个字节是存放消息⼤⼩的空间. (参考下图)
    在这里插入图片描述

8, deleteMessage() 删除消息

此处的删除只是 “逻辑删除”, 即把 Message 类中的 isValid 字段设置为 0.

	public void deleteMessage(MessageQueue queue, Message message) throws IOException {
        synchronized (queue) {
            // 1, 随机访问文件, 根据偏移量读出消息 (rw 表示打开方式, 可读也可写)
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                // 2, 读取二进制消息
                byte[] messageBinaryFrom = new byte[(int) (message.getOffsetEnd() - message.getOffsetBegin())];
                randomAccessFile.seek(message.getOffsetBegin()); // 光标移动
                randomAccessFile.read(messageBinaryFrom);
                // 3, 二进制消息-->消息对象
                Message messageInFile = (Message) BinaryUtil.toObject(messageBinaryFrom);
                // 4, 修改isValid字段
                messageInFile.setIsValid((byte) 0x0);
                // 5, 消息对象-->二进制消息
                byte[] messageBinaryTo = BinaryUtil.toByteArray(messageInFile);
                // 6, 写入文件
                randomAccessFile.seek(message.getOffsetBegin()); // 光标移动
                randomAccessFile.write(messageBinaryTo);
            }
            // 5, 更新 stat.txt 文件中的数据
            Stat stat = readStat(queue.getName());
            stat.validCount -= 1;
            writeStat(queue.getName(), stat);
        }
    }
  • 同样使用 synchronized 针对队列加锁
  • 使⽤ RandomAccessFile 来随机访问到⽂件的内容
  • 根据 Message 中的 offsetBegin 和 offsetEnd 定位到消息在⽂件中的位置. 通过
    randomAccessFile.seek 操作⽂件指针(相当于鼠标光标)偏移过去. 再读取
  • 读出的结果解析成 Message 对象, 修改 isValid 字段, 再重新写回⽂件. 注意写的时候要重新设定⽂件指针(光标)的位置. ⽂件指针会随着上述的读操作产⽣改变
  • 最后, 要记得更新统计⽂件, 把合法消息 - 1

9, isNeedGC() 是否需要垃圾回收

此处我们自定义一个策略 : 当文件中消息总数超过 2k, 但有效消息不足 30% 触发垃圾回收

    public boolean isNeedGC(String queueName){
        Stat stat = readStat(queueName);
        return stat.totalCount > 2000 && (stat.validCount * 1.0 / stat.totalCount < 0.3);
    }

10, gc() 垃圾回收

之前的删除操作, 只是把消息在⽂件上标记成了⽆效. 并没有腾出硬盘空间. 最终⽂件⼤⼩可能会越积越多. 因此需要定期的进⾏批量清除, 此处参考使用 JVM 的复制算法
GC 的时候会把所有有效消息加载出来(后面介绍这个方法), 写⼊到⼀个新的消息⽂件中, 使⽤新⽂件, 代替旧⽂件即可

	public void gc(MessageQueue queue) throws IOException, MQException {
        String queueName = queue.getName();
        // 1, 记录开始执行时间
        long startGC = System.currentTimeMillis();
        // 2, 创建(临时的)new_queue_data.txt 文件
        File newDataFile = new File(getNewQueueDataPath(queueName));
        if (newDataFile.exists()) {
            throw new MQException("[MessageFileManager.gc()] new_queue_data.txt 文件已经存在, queueName = " + queueName);
        }
        boolean result1 = newDataFile.createNewFile();
        if (!result1) {
            throw new MQException("[MessageFileManager.gc()] new_queue_data.txt 文件创建失败, newDataFile = " + newDataFile.getAbsolutePath());
        }
        // 3, 读取 data.txt 文件中的所有有效消息
        LinkedList<Message> ValidMessages = loadAllMessageFromQueue(queueName);
        // 4, 把所有有效消息写入 new_queue_data.txt
        try (OutputStream outputStream = new FileOutputStream(getNewQueueDataPath(queueName))) {
            try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                for (Message message : ValidMessages) {
                    // 消息对象-->二进制消息
                    byte[] messageTo = BinaryUtil.toByteArray(message);
                    dataOutputStream.writeInt(messageTo.length);
                    dataOutputStream.write(messageTo);
                }
            }
        }
        // 5, 删除 data.txt, 重命名 new_queue_data.txt --> data.txt
        File dataFile = new File(getQueueDataPath(queueName));
        boolean result2 = dataFile.delete();
        if (!result2) {
            throw new MQException("[MessageFileManager.gc()] 删除 queue_data.txt 文件失败! dataFile = " + dataFile.getAbsolutePath());
        }
        boolean result3 = newDataFile.renameTo(dataFile);
        if (!result3) {
            throw new MQException("[MessageFileManager.gc()] 重命名 new_queue_data.txt 文件失败! newDataFile = " + newDataFile.getAbsolutePath());
        }
        // 6, 更新 stat.txt 文件
        Stat stat = readStat(queueName);
        stat.totalCount = ValidMessages.size();
        stat.validCount = ValidMessages.size();
        writeStat(queueName, stat);
        // 7, 记录结束时间
        long endGC = System.currentTimeMillis();
        System.out.println("[MessageFileManager.gc()] 执行时间: " + (endGC - startGC) + "ms");
    }

    // 垃圾回收(复制算法)中获取到(临时)数据文件
    public String getNewQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/new_queue_data.txt";
    }

11, loadAllMessageFromQueue() 加载所有有效消息

把消息内容从⽂件加载到内存中. 这个功能在服务器重启, 和垃圾回收的时候都很关键

	public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MQException {
        LinkedList<Message> messagesLinkedList = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)) { //这个流可以读取字节
                int currentOffsetBegin = 0;
                // 循环读取所有有效消息
                while (true) {
                    // 1, 读取消息长度
                    int messageLength = dataInputStream.readInt();
                    // 2, 读取消息内容
                    byte[] messageBinaryFrom = new byte[messageLength];
                    int messageActualLength = dataInputStream.read(messageBinaryFrom);
                    if (messageActualLength != messageLength) {
                        throw new MQException("[MessageFileManager.loadAllMessageFromQueue()] 读取消息时文件错乱, queueName = " + queueName);
                    }
                    // 3, 二进制消息-->消息对象
                    Message messageInFile = (Message) BinaryUtil.toObject(messageBinaryFrom);
                    // 4, 判断是否为有效消息
                    if (messageInFile.getIsValid() != 0x1) {
                        currentOffsetBegin += (4 + messageActualLength);
                        continue;
                    }
                    // 5, 由于偏移量没有序列化, 需要再消息对象中设置偏移量后再存入链表
                    messageInFile.setOffsetBegin(currentOffsetBegin + 4);
                    messageInFile.setOffsetEnd(currentOffsetBegin + 4 + messageActualLength);
                    messagesLinkedList.add(messageInFile);
                    currentOffsetBegin += 4 + messageActualLength;
                }
            } catch (EOFException e) {
                // 这个 catch 并非真是处理 "异常", 而是处理 "正常" 的业务逻辑. 文件读到末尾, 会被 readInt 抛出该异常.
                // 这个 catch 语句中也不需要做啥特殊的事情
                System.out.println("[MessageFileManager.loadAllMessageFromQueue()] 读取 Message 数据完成!");
            }
        }
        return messagesLinkedList;
    }
  • 使⽤ DataInputStream 读取数据. 先读 4 个字节为消息的⻓度, 然后再按照这个⻓度来读取实际消息内容.
  • 读取完毕之后, 转换成 Message 对象.
  • 同时计算出该对象的 offsetBegin 和 offsetEnd.
  • 最终把结果整理成链表, 返回出去.
  • 注意, 对于 DataInputStream 来说, 如果读取到 EOF, 会抛出⼀个 EOFException , ⽽不是返回特定值. 因此需要注意上述循环的结束条件, 抛出⼀个 EOFException 就会退出循环, 但对程序无影响

四、小结

本文主实现了两点:

  • 1, 实现了序列化 / 反序列化, 为了保证数据能在文件中存储以及后续的网络传输
  • 2, 持久化存储 --> 硬盘管理 --> 文件
    • 2.1, 设计了消息在文件中存储的格式和规范
    • 2.2, 设计了垃圾回收机制(复制算法), 防止垃圾数据持续堆积
    • 2.3, 编写了文件管理的基本操作: 关于文件和目录的创建 / 删除, 关于消息的添加 / 删除等

上篇文章 主要实现了硬盘上数据库的管理, 本篇主要实现了营盘山文件的管理, 所以下篇文章会对数据库文件的进一步整合, 封装成对硬盘上数据的统一管理, 为上层(服务器)提供 API , 并且实现对内存中数据的管理


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

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

相关文章

MonkeyRunner自动化测试

一&#xff1a;简介 MonkeyRunner提供了一个API&#xff0c;使用此API写出的程序可以在Android代码之外控制Android设备和模拟器。通过monkeyrunner&#xff0c;您可以写出一个Python程序去安装一个Android应用程序或测试包&#xff0c;运行它&#xff0c;向它发送模拟击键&…

单目标应用:基于狐猴优化算法(Lemurs Optimizer,LO)的微电网优化调度MATLAB

一、狐猴优化算法 狐猴优化算法&#xff08;Lemurs Optimizer&#xff0c;LO&#xff09;由Ammar Kamal Abasi等人于2022年提出&#xff0c;该算法模拟狐猴的跳跃和跳舞行为&#xff0c;具有结构简单&#xff0c;思路新颖&#xff0c;搜索速度快等优势。 狐猴头体长约为30-45…

【通意千问】大模型GitHub开源工程学习笔记(2)--使用Qwen进行推理的示例代码解析,及transformers的库使用

使用Transformers来使用模型 如希望使用Qwen-chat进行推理,所需要写的只是如下所示的数行代码。请确保你使用的是最新代码,并指定正确的模型名称和路径,如Qwen/Qwen-7B-Chat和Qwen/Qwen-14B-Chat 这里给出了一段代码 from transformers import AutoModelForCausalLM, Aut…

Promise击鼓传花

Promise击鼓传花 Promise系列导航前言一、Promise.prototype.then()1.语法2.代码及说明&#xff08;1&#xff09;代码段&#xff1a;&#xff08;2&#xff09;代码段&#xff1a;&#xff08;3&#xff09;代码段&#xff1a;&#xff08;4&#xff09;代码段&#xff1a;&am…

select完成服务器并发

服务器 #include <myhead.h>#define PORT 4399 //端口号 #define IP "192.168.0.191"//IP地址//键盘输入事件 int keybord_events(fd_set readfds); //客户端交互事件 int cliRcvSnd_events(int , struct sockaddr_in*, fd_set *, int *); //客户端连接事件 …

cloudCompare教程:一、可视化、点、线编辑

依据高度等准则(都在Scalar Fields中)渲染点云&#xff08;首先要打开Tools -> Projection -> Export coordinate to SF&#xff09; 在上述准则之外的&#xff0c;设置为不显示&#xff1a; 软件的显示设置&#xff08;首先打开右边的彩色柱状图&#xff0c;点击左边属性…

Qt::工程框架-工具栏停靠|悬浮-QDockWidget

二维矢量动画智能制作软件开发合集 链接&#xff1a;软件开发技术分享及记录合集 个人开发二维矢量动画智能制作软件界面如下&#xff1a; ​目录 一、界面停靠原理 二、界面停靠代码实现 三、界面停靠软件测试视频 结束语 一、工具栏停靠|悬浮原理 本软件的窗口设置如下…

2023年中国肠胃炎用药行业现状分析:随着老龄化进程明显加速,市场规模同比增长7%[图]

急性肠胃炎是一种因为饮食不当而引起的消化系统疾病&#xff0c;通常是因为摄入了含有病原菌的变质食物&#xff0c;或者过量食用刺激性食物&#xff0c;从而导致肠胃道黏膜发生急性炎症反应&#xff1b;慢性肠胃炎是一种长期存在症状并持续引发胃黏膜和肠黏膜发生慢性炎症反应…

超声雷达传感器与三角定位

1.概述 超声波雷达的工作原理是通过超声波发射装置向外发出超声波&#xff0c;到通过接收器接收到发送过来超声波时的时间差来测算距离。 对温度敏感性&#xff0c;使得超声波的速度具有如下特性 或 2.超声波雷达车位探测与障碍物检测 汽车超声波类装配方案多为前后向共8个UP…

28269-2012 座椅用蛇形弹簧 技术条件

声明 本文是学习GB-T 28269-2012 座椅用蛇形弹簧 技术条件. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了用圆截面材料制造的座椅用蛇形弹簧的技术要求、试验方法、检验规则及标志、包装、 运输、贮存。 本标准适用于车辆座椅…

APA技术架构与说明

1.自动泊车的硬件架构 2.APA自动泊车辅助系统 1&#xff09;APA主要包括以下典型功能 &#xff08;1&#xff09;泊车入库&#xff1a;利用超声波雷达或环视摄像头实现车位识别&#xff0c;并计算出合适行驶轨迹&#xff0c;对车辆进行横向/纵向控制使车辆驶入车位&#xff1…

20分钟---Vue2->Vue3

Vue官网地址&#xff1a;gVue.js - The Progressive JavaScript Framework | Vue.js 选项式vs组合式 vue的两种风格&#xff0c;搬运官网源码&#xff1a; 选项式 API (Options API)​ 使用选项式 API&#xff0c;我们可以用包含多个选项的对象来描述组件的逻辑&#xff0c…

28295-2012 高温合金管材通用技术条件

声明 本文是学习GB-T 28295-2012 高温合金管材通用技术条件. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 1.1 本标准规定了经过热、冷加工生产的变形高温合金管材产品交货的技术要求、试验方法、检验规则 和交货条件等技术内容。 1.2 本标…

Linux常用指令(二)

目录 一、 删除空目录&#xff08;rmdir&#xff09; 二、ln 硬链接与软链接 三、新建空文件或更新文件的时间戳&#xff08;touch&#xff09; 四、比较文件内容的差异&#xff08;diff&#xff09; 五、显示当前时间或设置系统时间&#xff08;date&#xff09; 六、显…

关于解决 unable to start ssh-agent service, error :1058

前言 操作系统&#xff1a;win11 命令终端&#xff1a;Powershell 当我在终端输入命令 启动 ssh-agent 代理的时候 ssh-agent -s 很不幸出现了 unable to start ssh-agent service, error :1058以下错误 问题的解决 查看我们ssh-agent 服务是否运行&#xff0c;执行如下命令…

自动驾驶技术:现状与未来

自动驾驶技术&#xff1a;现状与未来 文章目录 引言自动驾驶技术的现状自动驾驶技术的挑战自动驾驶技术的未来结论结论 2023星火培训【专项营】Apollo开发者社区布道师倾力打造&#xff0c;包含PnC、新感知等的全新专项课程上线了。理论与实践相结合&#xff0c;全新的PnC培训不…

代码随想录刷题 Day 22

235. 二叉搜索树的最近公共祖先 具体思路就是当小于pq的时候就往右取遍历&#xff0c;当大于的时候就往左遍历&#xff1b; lass Solution { public:TreeNode* traversal(TreeNode* current, TreeNode* p, TreeNode* q) {if (current->val > p->val && curre…

企业风险管理策略终极指南

企业风险管理不一定是可怕的。企业风险管理是一个模糊且难以定义的主题领域。它涵盖了企业的多种风险和程序&#xff0c;与传统的风险管理有很大不同。 那么&#xff0c;企业风险管理到底是什么&#xff1f;在本文中&#xff0c;我们将确定它是什么&#xff0c;提出两种常见的…

程序的编译与生成可执行文件学习笔记(二)

gcc不是编译器&#xff0c;它是一个程序 GCC&#xff08;GNU Compiler Collection&#xff09;是一款常用的编译器&#xff0c;它支持分段编译&#xff0c;可以将源代码分为预处理、编译、汇编和链接等不同的阶段进行处理。下面是GCC分段编译流程的命令示例&#xff1a; 1. 预…

【数据结构】外部排序、多路平衡归并与败者树、置换-选择排序(生成初始归并段)、最佳归并树算法

目录 1、外部排序 1.1 基本概念 1.2 方法 2、多路平衡归并与败者树 2.1 K路平衡归并 2.2 败者树 3、置换-选择排序&#xff08;生成初始归并段&#xff09;​编辑 4、最佳归并树 4.1 理论基础​编辑 4.2 构造方法 ​编辑 5、各种排序算法的性质 1、外部排序 1.1 基本概…