MyBatis批量更新性能优化实战:从原理到生产环境调优
1. 从单条到批量为什么批量更新是性能优化的必选项在任何一个处理数据持久化的Java后端项目中MyBatis都是绕不开的核心框架。我们每天都在写update语句更新用户信息、修改订单状态、调整库存数量……这些操作在开发初期单条执行看起来毫无压力。但随着业务量的增长尤其是面对定时任务、数据同步、批量操作后台管理功能时问题就来了循环里执行成百上千条update每次都要创建连接、编译SQL、执行、提交事务数据库连接池的压力瞬间飙升接口响应时间从毫秒级直接跳到秒级甚至触发超时。我经历过一个真实的案例一个夜间运行的报表数据校正任务需要根据上游系统的变动更新本系统约10万条记录的不同字段。最初采用简单的for循环配合MyBatis的update方法跑一次需要近30分钟数据库服务器的CPU和IO长时间处于高位。后来改造为真正的批量更新后执行时间缩短到了2分钟以内。这个性能差距就是批量更新技术带来的最直观价值。所谓批量更新核心思想是将多条结构相似但数据不同的更新语句合并成一次数据库交互。这减少了网络往返次数Round-Trips和数据库事务日志的写入开销是应对批量数据修改场景最有效的手段之一。对于MyBatis而言实现批量更新主要有几种流派使用foreach标签动态拼接SQL、利用ExecutorType.BATCH模式配合循环提交、或者依赖数据库特有的批量更新语法如MySQL的CASE WHEN。每种方法都有其适用的场景和需要避开的“坑”。接下来的内容我将抛开官方文档式的简单罗列结合我多年在高压、高并发场景下的实战和踩坑经验为你深入剖析MyBatis批量更新的各种实现方式、背后的原理、性能对比以及那些教科书上不会写的、却足以让你加班到深夜的细节问题。2. 动态SQL拼接法foreach标签的利与弊这是最直观、也是很多开发者首先想到的方法。思路很简单在MyBatis的Mapper XML文件中利用OGNL表达式和foreach标签动态生成一条包含多个WHEN ... THEN ...条件的SQL语句。2.1 基础实现与SQL生成逻辑假设我们有一个user表需要根据用户ID批量更新其状态status和邮箱email。对应的Mapper接口方法可能如下int batchUpdateUsers(Param(list) ListUser userList);在XML中我们这样编写update idbatchUpdateUsers UPDATE user SET status CASE id foreach collectionlist itemitem WHEN #{item.id} THEN #{item.status} /foreach END, email CASE id foreach collectionlist itemitem WHEN #{item.id} THEN #{item.email} /foreach END WHERE id IN foreach collectionlist itemitem open( separator, close) #{item.id} /foreach /updateMyBatis在运行时会将传入的ListUser展开最终生成一条类似下面的SQL语句UPDATE user SET status CASE id WHEN 1 THEN active WHEN 2 THEN inactive WHEN 3 THEN active END, email CASE id WHEN 1 THEN user1example.com WHEN 2 THEN user2example.com WHEN 3 THEN user3example.com END WHERE id IN (1, 2, 3);这条SQL通过一次数据库请求就完成了所有记录的更新。CASE WHEN语句是SQL标准语法因此这种方法理论上对支持标准SQL的数据库MySQL, PostgreSQL, Oracle等都通用。2.2 优势与隐藏的成本这种方法的最大优势在于清晰和“一次性”。逻辑直接写在SQL里易于理解和调试。对于几千条量级的更新性能提升非常显著。但是它隐藏着几个不容忽视的成本SQL长度爆炸每条记录都会在SQL中增加若干个WHEN子句。如果批量更新1万条记录每条记录更新2个字段生成的SQL字符串将极其庞大。这不仅会增加数据库解析SQL的时间更可能触及数据库服务器或网络传输包的大小限制如MySQL的max_allowed_packet参数。参数占位符限制MyBatis内部使用PreparedStatement其参数占位符?数量有上限取决于数据库驱动和JDBC实现。超长的IN语句或巨量的CASE WHEN可能导致占位符超限抛出异常。事务回滚风险这是一条完整的SQL。如果其中某条数据的更新条件比如某个ID不存在导致部分更新失败不同的数据库有不同的行为。有些会整体失败回滚这是我们期望的有些可能部分成功部分失败造成数据不一致。实操心得我曾在一个项目中用此法批量更新约5000条数据SQL字符串长度超过了MySQL默认的max_allowed_packet4MB导致更新失败。解决方法一是调大该参数二是必须对数据进行分片Batch例如每500条执行一次批量SQL。所以务必在代码中加入分片逻辑不要一次性传入所有数据。2.3 动态字段更新的进阶技巧上面的例子是更新固定的字段status和email。但有时需求是一个列表里不同对象需要更新的字段可能不同。比如有的只更新状态有的只更新邮箱有的两者都更新。此时简单的CASE WHEN就力不从心了。一种更灵活的做法是使用MyBatis的script标签和if标签进行动态拼接。但这会极大增加SQL的复杂度且容易出错。对于这种“稀疏更新”场景我个人的建议是重新评估设计或者考虑采用下一节将要介绍的ExecutorType.BATCH模式它在这种场景下更具优势。3. 批处理执行器模式ExecutorType.BATCH的深度解析这是MyBatis原生支持的、更接近JDBC底层批处理的机制。其核心不在于拼接一条大SQL而在于利用PreparedStatement的addBatch()和executeBatch()方法。3.1 原理与配置启用在JDBC中可以对同一个PreparedStatement对象多次设置参数然后调用addBatch()将其加入批处理队列最后调用executeBatch()一次性发送给数据库执行。MyBatis的ExecutorType.BATCH模式封装了这一过程。要使用此模式你需要在获取SqlSession时指定执行器类型// 注意这里需要手动控制事务和session的关闭 try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper sqlSession.getMapper(UserMapper.class); for (User user : userList) { mapper.updateUser(user); // 单条更新的方法 } sqlSession.commit(); // 所有addBatch操作在此刻才真正执行 }关键在于在BATCH模式下mapper.updateUser(user)方法并不会立即执行SQL。它只是将参数设置到同一个PreparedStatement对象中并调用addBatch()。直到你调用sqlSession.commit()或sqlSession.flushStatements()时MyBatis才会执行executeBatch()将批处理队列中的所有操作一次性发送到数据库。3.2 性能表现与适用场景这种方式的性能优势体现在网络开销最小化多次addBatch只在内存中操作最终一次网络交互传输所有数据。SQL预编译复用同一条UPDATE语句只编译一次后续只是参数替换节省了数据库的SQL解析开销。避免长SQL问题完全不受max_allowed_packet或占位符数量的限制因为最终传输的是二进制参数流而非巨型SQL字符串。它特别适用于以下场景更新字段不固定每条记录可以调用不同的Mapper方法更新不同字段MyBatis会为每种SQL语句单独维护一个批处理队列。海量数据分批处理可以轻松结合循环每积累一定数量如1000条就执行一次flushStatements()然后继续既能提升性能又能控制内存。与插入、删除操作混合在一个BATCH会话中可以混合执行insert,update,delete语句它们会被分别批量化。3.3 必须警惕的“坑”与事务边界这是最容易出错的地方请务必仔细阅读。第一个大坑获取自增主键。在BATCH模式下insert操作后立即通过Options(useGeneratedKeys true)或selectKey获取自增ID是不可靠的。因为批处理延迟执行executeBatch()之前数据库根本没有生成ID。解决方案是避免在批处理会话中获取即时自增ID或者改用其他主键生成策略。第二个大坑一级缓存与重复更新。MyBatis的一级缓存SqlSession级别在BATCH模式下依然有效。如果你在同一个BATCH会话中先select了一条数据然后又依据这条数据去update它MyBatis可能会因为缓存判断而跳过这次更新。我的建议是在纯批处理场景下考虑关闭这个SqlSession的一级缓存或者在循环中及时清除特定缓存。第三个大坑事务提交与回滚。如代码所示必须手动调用sqlSession.commit()。如果循环中发生异常必须调用sqlSession.rollback()。忘记提交所有操作都不会生效忘记回滚可能造成部分数据被提交取决于数据库的自动提交设置和驱动行为。务必使用try-catch-finally或try-with-resources语句确保资源正确释放。第四个大坑连接持有时间。BATCH会话会长时间占用一个数据库连接直到commit。如果批处理数据量极大、耗时很长这个连接会被长时间占用可能影响连接池中其他线程获取连接。因此对于超大批量一定要做分片batch within batch每处理完一个分片就commit一次释放连接。踩坑实录我们有一个数据迁移任务在BATCH会话中循环处理10万条数据中间没有分片提交。任务运行了15分钟不仅该任务慢整个应用的其他数据库操作都变慢了因为连接池被耗尽了。后来改造为每处理1000条就commit并flushStatements()整体时间缩短到3分钟且对连接池无压力。4. 数据库方言与第三方扩展的选型除了MyBatis自带的两种方式我们还可以借助数据库特性或第三方工具库它们往往能提供更极致的性能或更简洁的语法。4.1 基于数据库特有语法的优化不同的数据库为批量更新提供了“方言”级别的优化。MySQL的ON DUPLICATE KEY UPDATE 这严格来说是“插入或更新”但对于基于唯一键如主键的批量更新可以变通使用。它能将多条UPDATE合并成一条INSERT ... ON DUPLICATE KEY UPDATE ...语句性能极高。INSERT INTO user (id, status, email) VALUES (1, active, aa.com), (2, inactive, bb.com), (3, active, cc.com) ON DUPLICATE KEY UPDATE status VALUES(status), email VALUES(email);在MyBatis中可以用foreach拼接VALUES部分。注意这要求id是主键或唯一索引且它本质是INSERT触发器、审计日志等可能会按插入记录。PostgreSQL的UPDATE ... FROM VALUES PostgreSQL支持更优雅的批量更新语法可以直接将值列表作为一个虚拟表来关联更新。UPDATE user SET status v.status, email v.email FROM (VALUES (1, active, aa.com), (2, inactive, bb.com) ) AS v(id, status, email) WHERE user.id v.id;这种语法清晰且性能优秀在MyBatis中同样可以通过动态SQL拼接实现。4.2 使用MyBatis-Plus等增强框架国内流行的MyBatis-PlusMP对批量操作提供了进一步封装。它的saveOrUpdateBatch等方法内部采用了最优策略通常是ExecutorType.BATCH并处理了分片等细节让开发者几乎可以“无脑”调用。// MyBatis-Plus 示例 ListUser userList ...; userService.saveOrUpdateBatch(userList);MP会判断批次大小自动选择执行方式大大简化了代码。但使用前需要了解其默认的分片大小默认是1000以及它背后使用的具体机制以便在出现性能问题时进行调优。4.3 选型决策矩阵如何选择最适合的方案面对这么多选择在实际项目中该如何决策我总结了一个简单的决策矩阵供参考场景特征推荐方案理由与注意事项更新量小100条字段固定动态SQLforeach实现简单SQL直观小数据量下无性能瓶颈。更新量大1000条字段固定动态SQLforeach 手动分片避免长SQL问题需在代码中实现分片逻辑如每500条执行一次。更新量大字段不固定或混合操作ExecutorType.BATCH灵活性强能处理不同SQL的批处理需注意事务和连接占用。基于唯一键的覆盖式更新数据库方言如MySQLON DUPLICATE KEY UPDATE性能最高但需注意语义是“插入或更新”可能触发插入相关的约束或逻辑。追求开发效率团队熟悉MPMyBatis-Plus 批量方法开箱即用减少样板代码需接受框架的默认行为和潜在的学习成本。超大批量十万级以上数据迁移ExecutorType.BATCH 分片提交 关闭一级缓存对内存和连接池最友好可控性最强但代码最复杂。核心原则是没有银弹。你需要根据数据量、更新模式、数据库类型、团队技术栈以及对性能、可靠性和开发效率的权衡来做出选择。在关键业务中我强烈建议针对候选方案编写性能测试代码用真实的数据量和表结构进行验证。5. 生产环境下的性能调优与监控选定了批量更新方案并不意味着可以高枕无忧。在生产环境中我们需要关注其运行状态并做好调优。5.1 关键性能指标与监控点执行时间这是最直接的指标。监控批量更新任务的耗时并设定基线。耗时异常增长往往是数据量变化、数据库负载升高或代码出现问题的信号。数据库资源关注批量更新执行期间数据库服务器的CPU使用率、IOPS磁盘读写、网络流量以及锁等待情况。一个糟糕的批量更新可能拖垮整个数据库。应用端资源监控JVM内存特别是用于存储批处理参数的内存、数据库连接池的活跃连接数、等待连接数。BATCH模式长时间不提交会占满连接。SQL执行计划对于动态拼接的复杂SQL尤其是带有巨大IN子句或CASE WHEN的要关注其执行计划是否合理是否使用了正确的索引。5.2 分片Batch Size大小的艺术无论是动态SQL还是BATCH执行器分片大小都是核心调优参数。大小太小如10则网络和事务开销占比过高失去批量意义。太大如10000则可能导致内存溢出OOM、长SQL问题、长事务锁竞争。如何确定需要通过压测找到一个“甜蜜点”。通常可以从100、500、1000、2000这几个典型值开始测试。观察不同大小下总执行时间、数据库负载、应用内存的变化曲线。通常500-2000是一个在多数场景下比较安全的范围。动态调整更高级的策略是根据当前系统负载或数据特征动态调整分片大小。例如在数据库负载低时用大分片负载高时用小分片。5.3 事务与一致性的保障批量更新必须考虑失败场景。原子性你希望这“一批”更新是原子操作吗如果是必须确保它们在一个数据库事务内。对于动态SQL一条SQL本身就是一个事务。对于BATCH模式你需要保证一个分片内的所有addBatch操作在同一个sqlSession.commit()中。部分失败处理如果一批1000条中第999条失败了怎么办是整体回滚还是记录失败项继续下一批业务上必须明确。通常我会采用“批次原子性”即一个分片内的操作要么全成功要么全回滚。失败的分片记录日志由人工或后续任务补偿。幂等性设计批量更新任务很可能被重试。要确保重复执行不会造成错误数据。可以通过在更新条件中加入状态判断如UPDATE ... SET ... WHERE id? AND statusold_status或者使用版本号、更新时间戳等乐观锁机制。6. 从MyBatis日志中洞察批量更新行为MyBatis的日志是调试和优化批量更新的宝贵工具。但默认配置下你可能看不到批处理的细节。6.1 开启完整的执行日志在application.yml或logback-spring.xml中配置MyBatis打印执行的SQL语句和参数logging: level: com.yourapp.mapper: DEBUG # 你的Mapper接口包路径 org.apache.ibatis: TRACE # 如需更详细日志可开启TRACE对于ExecutorType.BATCH你可能会看到类似这样的日志DEBUG ... - Preparing: UPDATE user SET status?, email? WHERE id? DEBUG ... - Parameters: active(String), aa.com(String), 1(Integer) DEBUG ... - Parameters: inactive(String), bb.com(String), 2(Integer) ... DEBUG ... - Flushing batch statements. DEBUG ... - Updates: 1000注意前面多条Parameters日志对应多次addBatch但只有一条Preparing。直到Flushing batch statements出现才真正执行。Updates: 1000显示了本次批量执行影响的行数。6.2 解读日志与常见问题排查问题日志显示每条都执行了Preparing。诊断这表示没有启用批处理。检查是否在正确的SqlSession上设置了ExecutorType.BATCH或者是否在循环中错误地获取了新的Mapper代理每次都会创建新的Statement。问题Updates返回值不等于预期条数。诊断在BATCH模式下executeBatch()返回的是一个int[]每个元素代表对应语句影响的行数。MyBatis日志打印的可能是总和。如果这个总和不对可能是更新条件不匹配如ID不存在或者触发了行锁导致更新失败。需要检查数据库返回的具体结果。问题日志中SQL异常但程序未捕获。诊断在BATCH模式下有些数据库驱动会将批处理中的多个异常合并为一个BatchUpdateException。你需要遍历这个异常对象的getUpdateCounts()数组来找出具体是哪几条语句失败了。掌握从日志中分析批量更新行为的能力能让你在出现性能问题或数据不一致时快速定位到根因而不是盲目地猜测和试错。这需要结合具体的数据库驱动行为和MyBatis的日志输出格式进行经验积累。

相关新闻

非模式生物GO富集分析:基于UniProt自建注释库的完整实战指南

非模式生物GO富集分析:基于UniProt自建注释库的完整实战指南

1. 从“无库可用”到“自建为王”:非模式生物GO富集的破局之路在生物信息学分析里,GO富集分析几乎是解读高通量测序结果的“标配”动作。无论是转录组、蛋白组还是代谢组,拿到一长串差异基因/蛋白列表后,我们总想看看它们到底在哪…

2026/7/31 3:43:24阅读更多 →
SQL注入第一天

SQL注入第一天

1.报错注入:输入单引号后直接显示数据库错误:?id1abcde2. 联合查询注入:先判断字段数量:?id1 ORDER BY 1 ?id1 ORDER BY 2 ?id1 ORDER BY 3 ?id1 ORDER BY 4 从正常到报错的临界值可以确定字段数量。例如 ORDER BY 4 报错&…

2026/7/31 3:43:24阅读更多 →
Termux SSH配置全攻略:从密钥登录到设备互联

Termux SSH配置全攻略:从密钥登录到设备互联

1. 从移动终端到云端:为什么Termux的SSH如此重要在移动办公和远程运维越来越普遍的今天,我们早已不满足于只能在桌面电脑上敲命令。想象一下,你正在通勤的地铁上,突然收到服务器告警,或者需要紧急查看一个Git仓库的日志…

2026/7/31 3:43:23阅读更多 →
游戏性能优化实战:解决GC频繁触发导致的帧率卡顿问题

游戏性能优化实战:解决GC频繁触发导致的帧率卡顿问题

1. 项目概述:当游戏帧率被GC“偷袭”做游戏开发,尤其是移动端或者对性能要求苛刻的平台,最怕的就是画面突然卡顿。玩家正沉浸在激烈的对战或者精美的场景中,突然画面一滞,帧率(FPS)从流畅的60直…

2026/7/31 4:53:46阅读更多 →
功能开发插件的智能代理架构

功能开发插件的智能代理架构

功能开发插件的智能代理架构 【免费下载链接】claude-code Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows -…

2026/7/31 4:53:46阅读更多 →
ACS712霍尔电流传感器PCB布局与软件校准实战指南

ACS712霍尔电流传感器PCB布局与软件校准实战指南

1. 从电流检测到PCB布局:ACS712的实战设计全解 最近在做一个需要精确监测电机电流的项目,选型时ACS712这个经典的霍尔效应电流传感器芯片又一次进入了我的视线。它结构简单、隔离性好,但真要把它用稳、用准,尤其是在PCB布线环节&a…

2026/7/31 4:53:46阅读更多 →
稳压二极管限流电阻选型详解:从公式到实战避坑指南

稳压二极管限流电阻选型详解:从公式到实战避坑指南

1. 从一次电路冒烟说起:为什么限流电阻不是“随便选”那天下午,我正在调试一块新设计的电源板。板上需要一个简单的5V基准电压,给一颗运放供电。电路再简单不过:输入是12V,用一个1N4733A(5.1V稳压管&#x…

2026/7/31 4:53:46阅读更多 →
关于linux里面的exec系列

关于linux里面的exec系列

其实有六种以exec开头的函数,统称exec函数: exec 系列函数&#xff1a;调用后直接替换当前进程的代码段&#xff01; 成功执行 → 原有 main 函数后续代码永远不会运行 只有调用失败&#xff0c;函数才会返回&#xff0c;继续往下执行。 #include <unistd.h> int execl(…

2026/7/31 4:53:46阅读更多 →
拔掉网线后TCP连接状态变化与检测机制深度解析

拔掉网线后TCP连接状态变化与检测机制深度解析

1. 先搞清楚面试官到底想问什么这个问题看似简单&#xff0c;但90%的候选人会栽在细节理解上。面试官问“拔掉网线后TCP连接是否存在”&#xff0c;其实是在考察三个层面的理解&#xff1a;第一层&#xff1a;TCP协议层面的连接状态如何变化。第二层&#xff1a;操作系统内核中…

2026/7/31 4:51:46阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

&#x1f539; 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具&#xff0c;凭借本地离线运行、可视化图形操作和任务自动化三大核心特性&#xff0c;赢得了众多用户的青睐。与普通在线对话AI工具不同&#xff0c;它属于能够直接操控本机软硬件的智能数字员工…

2026/7/30 15:03:16阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接&#xff0c;是用激光束对阀座壳体&#xff08;通常为不锈钢或铝合金&#xff09;进行密封焊接&#xff0c;使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/30 12:22:27阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX&#xff1a;三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/30 15:13:02阅读更多 →
物理复制比逻辑复制好在哪?数据库复制原理详解

物理复制比逻辑复制好在哪?数据库复制原理详解

数据库复制是把主库数据同步到备库的机制&#xff0c;分为逻辑复制和物理复制两种。逻辑复制传输的是 SQL 语句或行变更事件&#xff0c;物理复制传输的是存储引擎底层的物理日志。阿里云 PolarDB&#xff08;云原生数据库&#xff09;采用物理复制&#xff0c;在同步延迟、数据…

2026/7/31 0:00:40阅读更多 →
BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown&#xff1a;3分钟学会B站视频下载的终极指南 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader &#x1f633; 项目地址: https://gitcode.com/gh_mirrors/bi/Bilib…

2026/7/31 0:00:41阅读更多 →
有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

当前&#xff0c;游戏行业的“DataAI融合”已从概念验证进入价值落地阶段。根据IDC 2025年数据&#xff0c;中国AI游戏云市场规模已达18.6亿元&#xff1b;同时&#xff0c;游戏研发环节AI渗透率高达86%&#xff0c;生成式AI内容普及率超过50%。面对庞大的市场&#xff0c;游戏…

2026/7/31 0:00:41阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时&#xff0c;发现推理速度只有可怜的 1-2 FPS&#xff0c;而别人的演示视频却能跑到 30 FPS 以上&#xff0c;那么问题很可能不在模型本身&#xff0c;而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后&#xff0c;会直接使用官方示例…

2026/7/31 0:49:33阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一&#xff1a;为什么你需要了解 Coze 和 Dify&#xff1f;如果你对 AI 应用开发感兴趣&#xff0c;但一看到“大模型”、“智能体”、“工作流”这些词就头疼&#xff0c;觉得门槛太高&#xff0c;那这篇文章就是为你准备的。很多开发者&#xff0c;包括我自己&#…

2026/7/30 4:47:18阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会&#xff1a;配图一直是个让人头疼的问题。2026年&#xff0c;AI生图工具已经非常成熟了&#xff0c;但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1&#xff1a;速度之王2026年6月11日&#xff0c…

2026/7/30 15:43:46阅读更多 →