求求你,别再用 Mybatis Plus 的伪批量新增了!

news2024/9/21 13:20:18

8b07fb70d72d0a1dbf4217f5cd90f11f.png

前言

大家好,我是小哈~

本文节选自小哈写的《Mybatis Plus 教程》中的批量新增一节,旨在帮助大家如何在 Mybatis Plus 中,实现 MySQL 真实的批量新增,而不是伪批量新增。

最近,小哈在带小伙伴做 前后端分离项目,手摸手教学,后端 + 前端全栈开发,从 0 到 1 手敲,1v1 答疑,直到项目上线,后续上新更多项目 , 戳我加入

TIP : 教程发布在个站:犬小哈教程 www.quanxiaoha.com 上,欢迎围观。

什么是批量插入?优势在哪里?

先抛出一个问题:假设老板给你下了个任务,向数据库中添加 100 万条数据,并且不能耗时太久!

通常来说,我们向 MySQL 中新增一条记录,SQL 语句类似如下:

INSERT INTO `t_user` (`name`, `age`, `gender`) VALUES ('犬小哈0', 0, 1);

如果你需要添加 100 万条数据,就需要多次执行此语句,这就意味着频繁地 IO 操作(网络 IO、磁盘 IO),并且每一次数据库执行 SQL 都需要进行解析、优化等操作,都会导致非常耗时。

幸运的是,MySQL 支持一条 SQL 语句可以批量插入多条记录,格式如下:

INSERT INTO `t_user` (`name`, `age`, `gender`) VALUES ('犬小哈0', 0, 1), ('犬小哈1', 0, 1), ('犬小哈3', 0, 1);

和常规的 INSERT 语句不同的是,VALUES 支持多条记录,通过 , 逗号隔开。这样,可以实现一次性插入多条记录。

数据量不多的情况下,常规 INSERT 和批量插入性能差距不大,但是,一旦数量级上去后,执行耗时差距就拉开了,在后面我们会实测一下它们之间的耗时对比。

表与实体类

先创建一个测试表 t_user, 执行脚本如下:

DROP TABLE IF EXISTS user;

CREATE TABLE `t_user` (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) NULL DEFAULT NULL COMMENT '年龄',
  `gender` tinyint(2) NOT NULL DEFAULT 0 COMMENT '性别,0:女 1:男',
  PRIMARY KEY (`id`)
) COMMENT = '用户表';

再定义一个名为 User 实体类:

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2022-12-13 14:13
 * @version: v1.0.0
 * @description: TODO
 **/
@Data
@TableName("t_user")
public class User {
    /**
     * 主键 ID, @TableId 注解定义字段为表的主键,type 表示主键类型,IdType.AUTO 表示随着数据库 ID 自增
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private Integer gender;
}

TIP:@Data 是 Lombok 注解,偷懒用的,加上它即可免写繁杂的 getXXX/setXXX 相关方法,不了解的小伙伴可自行搜索一下如何使用。

Mybatis Plus 伪批量插入

在前面《新增数据》小节中,我们已经知道了 Mybatis Plus 内部封装的批量插入 savaBatch() 是个假的批量插入,示例代码如下:

List<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    User user = new User();
    user.setName("犬小哈" + i);
    user.setAge(i);
    user.setGender(1);
    users.add(user);
}
// 批量插入
boolean isSuccess = userService.saveBatch(users);
System.out.println("isSuccess:" + isSuccess);

通过打印实际执行 SQL , 我们发现还是一条一条的执行 INSERT:

1a12e2c5192a52a382d0120dcfb7c541.jpeg

并且还带着大家看了内部实现的源码,这种方式比起自己 for 循环一条一条 INSERT 插入数据性能要更高,原因是在会话这块做了优化,虽然实际执行并不是真的批量插入。

利用 SQL 注入器实现真的批量插入

接下来,小哈就手把手带你通过 Mybatis Plus 框架的 SQL 注入器实现一个真的批量插入。

示例项目结构

先贴一张示例项目的结构:

46e2054d951d3f99900c5be8889f51eb.jpeg

注意看我红线标注的部分,主要关注这 4 个类与接口。

新建批量插入 SQL 注入器

在工程 config 目录下创建一个 SQL 注入器 InsertBatchSqlInjector  :

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2023-01-05 14:42
 * @version: v1.0.0
 * @description: 批量插入 SQL 注入器
 **/
public class InsertBatchSqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
     // super.getMethodList() 保留 Mybatis Plus 自带的方法
        List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
        // 添加自定义方法:批量插入,方法名为 insertBatchSomeColumn
        methodList.add(new InsertBatchSomeColumn());
        return methodList;
    }
}

说说 InsertBatchSomeColumn

InsertBatchSomeColumn 是 Mybatis Plus 内部提供的默认批量插入,只不过这个方法作者只在 MySQL 数据测试过,所以没有将它作为通用方法供外部调用,注意看注释:

010722a435738764c5a8eccf5ec6bde9.jpeg

源码复制出来,如下:

/**
 * 批量新增数据,自选字段 insert
 * <p> 不同的数据库支持度不一样!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!! </p>
 * <p> 除了主键是 <strong> 数据库自增的未测试 </strong> 外理论上都可以使用!!! </p>
 * <p> 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!! </p>
 * <p>
 * 自己的通用 mapper 如下使用:
 * <pre>
 * int insertBatchSomeColumn(List<T> entityList);
 * </pre>
 * </p>
 *
 * <li> 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值 </li>
 *
 * <p>
 * 常用的 {@link Predicate}:
 * </p>
 *
 * <li> 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段 </li>
 * <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段 </li>
 * <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段 </li>
 *
 * @author miemie
 * @since 2018-11-29
 */

@SuppressWarnings("serial")
public class InsertBatchSomeColumn extends AbstractMethod {

    /**
     * 字段筛选条件
     */
    @Setter
    @Accessors(chain = true)
    private Predicate<TableFieldInfo> predicate;

    /**
     * 默认方法名
     */
    public InsertBatchSomeColumn() {
     // 方法名
        super("insertBatchSomeColumn");
    }

    /**
     * 默认方法名
     *
     * @param predicate 字段筛选条件
     */
    public InsertBatchSomeColumn(Predicate<TableFieldInfo> predicate) {
        super("insertBatchSomeColumn");
        this.predicate = predicate;
    }

    /**
     * @param name      方法名
     * @param predicate 字段筛选条件
     * @since 3.5.0
     */
    public InsertBatchSomeColumn(String name, Predicate<TableFieldInfo> predicate) {
        super(name);
        this.predicate = predicate;
    }

    @SuppressWarnings("Duplicates")
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) +
            this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
        String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) +
            this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
        insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);
        String keyProperty = null;
        String keyColumn = null;
        // 表包含主键处理逻辑,如果不包含主键当普通字段处理
        if (tableInfo.havePK()) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                /* 自增主键 */
                keyGenerator = Jdbc3KeyGenerator.INSTANCE;
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else {
                if (null != tableInfo.getKeySequence()) {
                    keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
                    keyProperty = tableInfo.getKeyProperty();
                    keyColumn = tableInfo.getKeyColumn();
                }
            }
        }
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
    }

}

配置 SQL 注入器

config 包下创建 MybatisPlusConfig 配置类:

/**
 * @Author: 犬小哈
 * @From: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @Date: 2022-12-15 18:29
 * @Version: v1.0.0
 * @Description: TODO
 **/
@Configuration
@MapperScan("com.quanxiaoha.mybatisplusdemo.mapper")
public class MybatisPlusConfig {


    /**
     * 自定义批量插入 SQL 注入器
     */
    @Bean
    public InsertBatchSqlInjector insertBatchSqlInjector() {
        return new InsertBatchSqlInjector();
    }

}

新建 MyBaseMapper

config 包下创建 MyBaseMapper 接口,让其继承自 Mybatis Plus 提供的 BaseMapper, 并定义批量插入方法:

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2022-12-13 14:13
 * @version: v1.0.0
 * @description: TODO
 **/
public interface MyBaseMapper<T> extends BaseMapper<T> {

 // 批量插入
    int insertBatchSomeColumn(@Param("list") List<T> batchList);

}

注意:方法名必须为 insertBatchSomeColumn, 和 InsertBatchSomeColumn 内部定义好的方法名保持一致。

新建 UserMapper

mapper 包下创建 UserMapper 接口,注意继承刚刚自定义的 MyBaseMapper, 而不是 BaseMapper :

/**
 * @author: 犬小哈
 * @from: 公众号:小哈学Java, 网站:www.quanxiaoha.com
 * @date: 2022-12-13 14:13
 * @version: v1.0.0
 * @description: TODO
 **/
public interface UserMapper extends MyBaseMapper<User> {
}

测试批量插入

完成上面这些工作后,就可以使用 Mybatis Plus 提供的批量插入功能了。我们新建一个单元测试,并注入 UserMapper :

@Autowired
private UserMapper userMapper;

单元测试如下:

@Test
void testInsertBatch() {
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        User user = new User();
        user.setName("犬小哈" + i);
        user.setAge(i);
        user.setGender(1);
        users.add(user);
    }

    userMapper.insertBatchSomeColumn(users);
}

控制台实际执行 SQL 如下:

b0631acce0e4ae420a0a1a9a51007018.jpeg

可以看到这次是真实的批量插入了,舒服了~

性能对比

我们来测试一下插入 105000  条数据,分别使用 for 循环插入数据、savaBatch() 伪批量插入、与真实批量插入三种模式,看看耗时差距多少。

小哈这里的机器配置如下:

23f25de8c72b6572e3cc052e154410b0.jpeg

for 循环插入

单元测试代码如下:

@Test
void testInsert1() {
    // 总耗时:722963 ms, 约 12 分钟
    long start = System.currentTimeMillis();
    for (int i = 0; i < 105000; i++) {
        User user = new User();
        user.setName("犬小哈" + i);
        user.setAge(i);
        user.setGender(1);
        userMapper.insert(user);
    }
    System.out.println(String.format("总耗时:%s ms", System.currentTimeMillis() - start));
}

savaBatch() 伪批量插入

单元测试代码如下:

@Test
void testInsert2() {
    // 总耗时:95864 ms, 约一分钟30秒左右
    long start = System.currentTimeMillis();
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 105000; i++) {
        User user = new User();
        user.setName("犬小哈" + i);
        user.setAge(i);
        user.setGender(1);
        users.add(user);
    }
    userService.saveBatch(users);
    System.out.println(String.format("总耗时:%s ms", System.currentTimeMillis() - start));
}

真实批量插入

注意,真实业务场景下,也不可能会将 10 万多条记录组装成一条 SQL 进行批量插入,因为数据库对执行 SQL 大小是有限制的(这个数值可以自行设置),还是需要分片插入,比如取 1000 条执行一次批量插入,单元测试代码如下:

@Test
    void testInsertBatch1() {
        // 总耗时:6320 ms, 约 6 秒
        long start = System.currentTimeMillis();
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 1006; i++) {
            User user = new User();
            user.setName("犬小哈" + i);
            user.setAge(i);
            user.setGender(1);
            users.add(user);
        }

        // 分片插入(每 1000 条执行一次批量插入)
        int batchSize = 1000;
        int total = users.size();
        // 需要执行的次数
        int insertTimes = total / batchSize;
        // 最后一次执行需要提交的记录数(防止可能不足 1000 条)
        int lastSize = batchSize;
        if (total % batchSize != 0) {
            insertTimes++;
            lastSize = total%batchSize;
        }

        for (int j = 0; j < insertTimes; j++) {
            if (insertTimes == j+1) {
                batchSize = lastSize;
            }

            // 分片执行批量插入
            userMapper.insertBatchSomeColumn(users.subList(j*batchSize, (j*batchSize+batchSize)));
        }
        System.out.println(String.format("总耗时:%s ms", System.currentTimeMillis() - start));
    }

耗时对比

方式总耗时
for 循环插入722963 ms, 约 12 分钟
savaBatch() 伪批量插入95864 ms, 约一分钟30秒左右
真实批量插入6320 ms, 约 6 秒

耗时对比非常直观,在大批量数据新增的场景下,批量插入性能最高。

结语

本小节中,我们学习了如何通过 Mybatis Plus 的 SQL 注入器实现真实的批量插入,同时最后还对比了三种不同方式插入 10 万多数据的耗时,很直观的看到在海量数据场景下,批量插入的性能是最强的。


👉 欢迎加入小哈的Java项目实战知识星球手摸手带你做前后端分离项目,手摸手教学,后端 + 前端包办,从 0 到 1 手敲,1v1 答疑,直到项目上线,后续上新更多项目

 
 

a98ecd532e3de1927c95db149e820ca4.gif

 
 
 
 
 
 

1. 前后端分离,开源的 Spring Boot + Vue 3.2 的博客,泰裤辣!

2. 这款轻量级 Java 表达式引擎,真不错!

3. 京东二面:10w+订单每秒热点数据架构如何优化?

4. WebSocket 的 6 种集成方式

482ca1d9903499f3003af95cd93f2c23.gif

最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦

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

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

相关文章

2023年MySQL-8.0.34保姆级安装教程

重点放前面&#xff1a;演示环境为windows环境。 MySQL社区版本安装教程如下&#xff1a; 一、MySQL安装包下载二、安装配置设置三、配置环境变量 大体分为3个步骤&#xff1a;①安装包的下载&#xff1b;②安装配置设置&#xff1b;③配置环境变量 一、MySQL安装包下载 下载官…

界面长的像算抄袭吗?

昨晚&#xff0c;在GitHub和X上&#xff0c;被一次疑似抄袭的问题刷了一会儿屏&#xff0c;主要是下面这个issue&#xff1a; 相关的开源项目是小米的米效&#xff0c;英文名&#xff1a;Mone。一个以微服务为中心的一站式企业协同研发平台。支持公有云、私有云、混合云等多种部…

Redis Cluster集群运维与核心原理剖析

Redis集群方案比较 哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态&#xff0c;如果master节点异常&#xff0c;则会做主从切换&#xff0c;将某一台slave作为master&#xff0c;哨兵的配置略微复杂&#xff0c;并且性能和高可用性…

2023/09/07 c++qt day2

#include <iostream>using namespace std; //封装一个学生类 struct stu { private://存放学生的成绩int stu_score[256];//记录学生个数int stu_num; public://用于设置学生个数void setNum(){cout<<"请输入学生的个数"<<" ";cin>&g…

神策数据发布汽车行业 CJO 解决方案,打造客户旅程全新体验

最近&#xff0c;围绕数字化客户经营&#xff0c;神策数据基于“客户旅程编排&#xff08;Customer Journey Orchestration&#xff0c;简称 CJO&#xff09;”理念&#xff0c;发布汽车行业全新解决方案&#xff0c;通过全渠道打通给客户带来一致的、个性化的体验&#xff0c;…

如何做好自己的职业规划

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 进入公司以后&#xff0c;就是进入了人生的下一个阶段&#xff0c;通过前面几个章节&#xff0c;我们谈到了入职新公司后应该如何开展工作。这节我们来聊一聊如何做好职业规…

2594. 修车的最少时间

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;二分枚举答案 写在最后 Tag 【二分枚举答案】【数组】 题目来源 2594. 修车的最少时间 题目解读 给你一个表示机械工能力的数组 ranks&#xff0c;ranks[i] 表示第 i 位机械工可以在 r a n k s [ i ] ∗ n 2 ranks[…

【三维】NeRF神经辐射场构建三维模型

论文地址&#xff1a;paper 代码地址&#xff1a;code 视频地址&#xff1a;油管 目录 0.&#x1f308;&#x1f308;摘要 1.&#x1f308;&#x1f308;nerf主要原理 2.&#x1f308;&#x1f308;网络结构 2.1&#x1f4cc;渲染 2.2&#x1f4cc;消融实验 3.&#x…

链表反转问题

链表反转常用的两种方式 1.建立虚拟头结点辅助反转 Java 实现 public static ListNode reverseListByDummyNotCreate(ListNode head) {ListNode ans new ListNode(-1);ListNode cur head;while (cur ! null) {ListNode next cur.next;cur.next ans.next;ans.next cur;cur…

抖音集团都在用的画质评估工具,确定不试试吗?

导读 本文从抖音集团内部画质评估体系的建设历程着笔&#xff0c;主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路&#xff0c;希望能抛砖引玉&#xff0c;为遇到类似困扰的伙伴们提供有价值的…

[C++学习] 多进程通信共享内存

ref:https://blog.csdn.net/qq_35733751/article/details/82872197 多线程共享进程的地址空间&#xff0c;如果多个线程需要访问同一块内存&#xff0c;用全局变量即可。 在多进程中&#xff0c;每个进程的地址空间是独立的&#xff0c;不共享的&#xff0c;如果多个进程需要访…

Redis持久化、主从与哨兵架构详解

Redis持久化 RDB快照&#xff08;snapshot&#xff09; 在默认情况下&#xff0c; Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。 你可以对 Redis 进行设置&#xff0c; 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时&#xff0c; 自动保存一次数…

yolov7添加pconv模块

连接pconv 1、复制到models-common.py文件最后 2、添加模块到yolo.py 3、修改网络&#xff0c;建议替换3x3的卷积&#xff0c;后面的参数不要了 4、不能替换步长为2的卷积

CSS笔记(黑马程序员pink老师前端)浮动,清除浮动

浮动可以改变标签的默认排列方式。浮动元素常与标准流的父元素搭配使用. 网页布局第一准则:多个块级元素纵向排列找标准流&#xff0c;多个块级元素横向排列找浮动。 float属性用于创建浮动框&#xff0c;将其移动到一边&#xff0c;直到左边缘或右边缘触及包含块或另一个浮动框…

【算法系列篇】分治-快排

文章目录 前言什么是分冶1.颜色分类1.1 题目要求1.2 做题思路1.3 Java代码实现 2. 排序数组2.1 题目要求2.2 做题思路2.3 Java代码实现 3.数组中的第k个最大元素3.1 题目要求3.2 做题思路3.3 Java代码实现 4. 最小的k个数4.1 题目要求4.2 做题思路4.3 Java代码实现 总结 前言 …

前端面试题合集(一)

前端面试题合集 1.js异步方案2.文件上传如何限制文件类型3. 说出 与的区别4.多维数组如何降维5.如何给一个按钮绑定两个onclick事件 1.js异步方案 js异步方法分为两种&#xff0c;分别为defer和async,如果没有写其中一种的话代码从上到下同步执行&#xff0c;遇到脚本代码之后…

埋头干活不会汇报,别说 996 就算 007 也没用!

​ 见字如面&#xff0c;我是军哥&#xff01; 经调研发现 80% 的程序员认为工作汇报就是形式主义&#xff0c;无聊至极&#xff0c;但是我要和你说&#xff0c;做好工作汇报非常重要&#xff0c;这直接关系到你在这家公司能否快速成长和晋升加薪&#xff0c;而且要告诉你一件扎…

vue3:18、Pinia持久化(pinia-plugin-persistedstate)

安装插件 npm i pinia-plugin-persistedstate main.js中引入 import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import piniaPluginPersistedstate from pinia-plugin-persistedstate // createApp(App).use(CreatePinia()).mou…

Day58|leetcode 739. 每日温度、496.下一个更大元素 I

今天开始单调栈&#xff01; leetcode 739. 每日温度 题目链接&#xff1a;739. 每日温度 - 力扣&#xff08;LeetCode&#xff09; 视频链接&#xff1a;单调栈&#xff0c;你该了解的&#xff0c;这里都讲了&#xff01;LeetCode:739.每日温度_哔哩哔哩_bilibili 题目概述 …

Unity——脚本与导航系统

Unity内置了一个比较完善的导航系统&#xff0c;一般称为Nav Mesh&#xff08;导航网格&#xff09;&#xff0c;用它可以满足大多数游戏中角色自动导航的需求。 一、导航系统相关组件 Unity的导航系统由以下几个部分组成&#xff1a; Nav Mesh。Nav Mesh与具体的场景关联&…