news 2026/10/21 9:51:45

微服务架构下MyBatis-Plus性能调优与高级用法实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微服务架构下MyBatis-Plus性能调优与高级用法实战

这几年微服务改造基本成了通用架构,但很多团队到了服务拆分这一步,会发现原来在单体里跑得挺顺的 MyBatis-Plus,换到分布式环境后开始陆续暴露问题:分页越来越慢、多数据源配置混乱、批量写入卡顿、租户隔离怎么加都容易漏。这篇文章我结合在几个项目里实际打磨的经验,聊聊 MyBatis-Plus 在微服务里真正需要关注的高级用法和性能调优思路——适合已经在用 MyBatis-Plus,但想在服务拆分后把持久层基础能力做扎实的读者。

1. 服务拆分之后,MyBatis-Plus 为什么突然"不听话"了

单体应用里 MyBatis-Plus 的配置路径很清晰:一个数据源、一个 SqlSessionFactory、一套拦截器,实体类扫描一遍,开箱即用。服务一拆,情况就变了。每个服务要管自己的库,有些服务还要跨库访问,数据库连接的分配、拦截器的生效范围、事务的边界全都需要重新捋一遍。

1.1 连接池分配:一个容易被忽视的隐性故障

我见过一个实际案例。某公司的业务系统拆成了订单、库存、支付、用户四个服务,每个服务部署两个实例。Druid 连接池默认配置下每个实例的初始连接数是 10,最大连接数也是 10 左右。粗看每个服务最多占 20 个连接,四个服务加起来 80 个,数据库 max_connections 设置的是 200,理论上没问题。

但实际跑起来之后,报表服务、定时任务服务、消息消费服务陆续接进来,每个服务又各配了一套连接池参数。真正高峰时数据库连接数直接冲到 190 多,新连接申请开始排队,整条链路跟着变慢。排查了很久才定位到是连接池累加超限。

微服务场景下连接池规划不能只看单个服务,要按"实例数 × 连接池上限"做总量核算。建议给每个服务设置合理的最大连接数,比如普通业务服务 10-20,纯计算类服务甚至可以更小。同时要设置连接池的maxWait和connectionTimeout,让拿不到连接时快速失败而不是无限阻塞,否则会引发服务雪崩。

1.2 拦截器必须在每个数据源上生效

MyBatis-Plus 的分页插件、乐观锁插件、租户插件本质上都是基于 MyBatis 的 Interceptor 机制实现的。在微服务里,如果你用的是多数据源方案(比如 dynamic-datasource 这类动态数据源组件),插件需要注入到每一个 SqlSessionFactory 里,而不是只配一个主数据源完事。

这个坑很隐蔽。我之前在一个项目里给主数据源配置了分页插件,但有一个次要数据源没配。运行中对这个数据源执行selectPage时,分页条件完全没生效,白白把整个表的数据拉到内存里。几百万行数据直接导致 OOM。排查时翻了配置才发现,两个 SqlSessionFactory 的插件配置漏了其中一个。

如果使用 dynamic-datasource,推荐在主配置类里写一个全局MybatisPlusInterceptor的 Bean,这个 Bean 会被自动应用到所有数据源关联的 SqlSessionFactory 上。手动创建多个 SqlSessionFactory 时,一定要记得把同一个拦截器对象逐个 add 进去。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 如果有多租户需求,再加 TenantLineInnerInterceptor return interceptor; }

1.3 本地事务的边界问题

服务拆分之后还有一个高频问题:@Transactional注解默认只对当前数据源生效,单服务内跨多个数据源的操作,本地事务根本管不住。有人以为加上注解就能保证一致性,实际上 A 库提交成功、B 库回滚失败的场景非常普遍。

跨服务的数据一致性应该交给事务消息或最终一致性方案,跨数据源的事务则需要引入分布式事务中间件,不能靠一个注解硬撑。在这些框架接入之前,最简单的做法是控制事务粒度和范围,让一个事务方法只操作同一个数据源,避免在事务里做远程调用。

2. 多租户隔离:租户插件不是加一个注解那么简单

很多商业系统的数据模型都带租户概念,MyBatis-Plus 的TenantLineInnerInterceptor可以自动在 SQL 上拼接租户条件,省去每个 Mapper 方法里手动拼条件的工作。但在微服务架构下,租户信息的传递链路比想象中长得多。

2.1 租户插件的生效原理和配置细节

租户插件的核心逻辑是:在执行查询、更新、删除 SQL 时,自动在 WHERE 条件中加入tenant_id = 当前租户ID,在插入时自动填充租户 ID 字段。它的实现基于 SQL 解析,会改写原始的 SQL 语句。

配置时需要提供一个TenantLineHandler,指定租户字段名和当前租户 ID 的获取方式。这里最常犯的错误是把所有表都做了租户隔离,但有些表是全局共享的,比如系统字典表、行政区划表。这类表要用ignoreTable方法主动排除掉,否则每次查询都会被强制加上租户条件,查不到数据。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(); tenantInterceptor.setTenantLineHandler(new TenantLineHandler() { @Override public Expression getTenantId() { // 从上下文中取当前租户ID return new LongValue(TenantContextHolder.getTenantId()); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public Set<String> ignoreTable() { Set<String> tables = new HashSet<>(); tables.add("sys_dict"); tables.add("sys_area"); return tables; } }); interceptor.addInnerInterceptor(tenantInterceptor); return interceptor; }

2.2 跨服务调用时租户 ID 的透传问题

单体应用里租户 ID 通常存在 ThreadLocal 里,MyBatis-Plus 在执行时直接取就行。微服务拆开后,一个请求会经过网关、订单服务、库存服务等多个节点,每个服务都有自己独立的线程上下文。如果订单服务拿到租户 ID 之后,调用库存服务时没有把这个参数传过去,库存服务的租户插件就拿不到租户 ID,要么报错,要么把数据查错。

正确的做法是:在网关层解析出租户 ID 后写入请求头,各服务通过过滤器统一从请求头读取并放入自己的 TenantContextHolder。如果用到 Feign,还需要配置RequestInterceptor把租户 ID 从当前服务的上下文传递到下一个服务。消息队列场景更麻烦,消费消息时没有请求头可用,需要在消息体里带上租户 ID,消费端消费时手动设置上下文。

@Bean public RequestInterceptor feignTenantInterceptor() { return requestTemplate -> { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId != null) { requestTemplate.header("X-Tenant-Id", String.valueOf(tenantId)); } }; }

2.3 数据库索引必须考虑租户条件

这是多租户项目里最隐性、也最容易被忽略的一点。很多团队在建设表结构时,索引设计只考虑了业务字段,比如订单表的索引建在order_no上。引入租户插件后,所有查询最终都会变成WHERE tenant_id = ? AND order_no = ?,如果索引只有order_no这一个列,MySQL 大概率不会选择这个索引,而是退化为全表扫描或者扫描完索引后再逐行过滤。

线上遇到过一张 500 万行的订单表,没有把tenant_id放进联合索引,查询耗时从几十毫秒直接飙升到三秒以上。加上租户条件不是临时加一个功能,而是从表设计阶段就要规划进去。所有业务索引都应该是"租户 ID + 业务字段"的联合索引,才能让租户插件的改动对执行计划完全不产生负面影响。

3. 自定义 SQL 注入器:把重复的 Mapper 代码消灭在基类里

官方 BaseMapper 提供了不少现成的 CRUD 方法,但在真实业务里总会有一些"每个表都需要、官方又没有内置"的方法。最常见的两个是:真正能拼接多值的insertBatchSomeColumn,以及ON DUPLICATE KEY UPDATE的 upsert 操作。逐个表去写 XML 很蠢,正确思路是用 SQL 注入器,在启动时自动给每个实体绑定这些通用方法。

3.1 SQL 注入器的运行机制

MyBatis-Plus 的 BaseMapper 本身不写 SQL 文件,它是在运行时通过SqlInjector把方法名和 SQL 模板注册到 MappedStatement 上的。比如selectById、insert这些方法,都有对应的BaseMethod实现类,里面写好了 SQL 模板和执行逻辑。

自定义注入器的做法就是继承DefaultSqlInjector,重写getMethodList方法,在返回的列表里追加自己的AbstractMethod实现。每个自定义方法里需要指定 SQL 模板,模板里的#{et}指代实体对象,#{ew.customSqlSegment}指代 QueryWrapper 的条件片段。

3.2 一个完整的 upsert 实现示例

以 MySQL 的 upsert 为例。实现一个通用方法,让任意实体都能调用,不存在则插入,存在则更新指定字段。核心步骤三件事:定义BaseMethod子类、重写injectMappedStatement生成 MappedStatement、在SqlInjector中注册。

public class Upsert extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { String sql = String.format( "INSERT INTO %s(%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s", tableInfo.getTableName(), tableInfo.getKeyInsertSqlColumn(false), tableInfo.getKeyInsertSqlProperty(false, "et"), tableInfo.getAllSqlSet(false, "et.") ); SqlSource sqlSource = languageDriver.createSqlSource( configuration, sql, modelClass ); return this.addInsertMappedStatement(mapperClass, modelClass, "upsert", sqlSource); } }
public class CustomSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); methodList.add(new Upsert()); methodList.add(new InsertBatchSomeColumn()); return methodList; } }

然后在配置类里把自定义注入器替换成默认的 Bean:

@Bean public SqlInjector customSqlInjector() { return new CustomSqlInjector(); }

之后每个继承 BaseMapper 的 Mapper 接口都自动获得了upsert方法,直接用userMapper.upsert(user)就能完成一行数据的"不存在插入、存在更新"。配合代码生成器,几百个实体表在生成时就把这些通用方法带出来了,之后几乎不需要再手写任何 XML 文件。

3.3 开发中需要注意的几个细节

第一,自定义方法的 SQL 模板里不能用 MyBatis 的动态标签直接拼接用户输入,实体字段的正确取值要经过 TableInfo 处理,防止 SQL 注入风险。第二,InsertBatchSomeColumn这类方法在并发量高的场景下要配合批处理参数和合理的事务边界使用,否则一次插入几千行不仅慢,还可能撑爆事务日志。第三,注入器一旦在项目里做了扩展,建议在团队内沉淀成公共基础组件,避免每个服务各自实现一套,维护成本会成倍增加。

4. 分页查询的性能优化:从 LIMIT 到键集分页

分页插件确实是 MyBatis-Plus 最实用的能力之一,但也是微服务里出性能问题最多的地方。原因很简单:数据量到百万级之后,传统LIMIT offset, size这种翻页方式的效率急剧下降。这个问题不是 MyBatis-Plus 本身造成的,但 MyBatis-Plus 默认的selectPage走的就是这条路径,所以用的时候必须有意识地规避。

4.1 深度分页为什么越来越慢

LIMIT 200000, 20的语义是:数据库从第一行开始扫描,数到第 200020 行,然后丢掉前 200000 行,只返回最后 20 行。前面这些被扫过又被丢掉的数据,全部要耗费磁盘 IO 和 CPU。表里数据量越大,前面堆积的偏移量越多,查询就越慢,哪怕你加了索引也没太大帮助。

某订单列表接口在压测时出现的情况很典型:数据量只有 80 万,前 100 页响应时间基本都在 50ms 以内,翻到第 5000 页时单次查询耗时稳定在 2-3 秒。把这条查询单独拿出来EXPLAIN看,扫描行数就是 offset 的量级。这种情况下优化 SQL 本身作用有限,方案选择的思路得变。

4.2 键集分页:适合大多数业务场景的替代方案

键集分页(Keyset Pagination)的核心思路是不用页码,而是记住"上一页最后一条数据的位置",下一次查询时直接从这个位置往后取。比如订单列表按创建时间倒序排,第一页查WHERE create_time < 某个时间点 ORDER BY create_time DESC LIMIT 20,第二页把上一页最后一条记录的 create_time 作为条件继续往下查。MySQL 可以直接走索引定位,扫描行数始终控制在 20 行以内,性能非常稳定。

MyBatis-Plus 没有内置直接的键集分页 API,但可以通过 QueryWrapper 自己封装。实现时要注意排序字段唯一性,否则可能出现相邻页数据重复。最可靠的做法是排序字段选主键或者"业务字段 + 主键"的组合,保证排序绝对稳定。

public <T> List<T> pageByCursor(QueryWrapper<T> wrapper, String cursorColumn, Object lastCursorValue, int size) { wrapper.last("LIMIT " + size); if (lastCursorValue != null) { wrapper.lt(cursorColumn, lastCursorValue); } wrapper.orderByDesc(cursorColumn); return baseMapper.selectList(wrapper); }

适合键集分页的场景非常多:订单列表、日志查询、消息记录、资讯流。不适合的场景是用户需要精确跳转页码的表格类页面,这种情况下页面不想改交互,后端只能继续用传统分页,但可以配合延迟关联来减负。

4.3 延迟关联和覆盖索引的配合

延迟关联的思路是:先只查主键,再做一次主键回表拿完整数据。因为 MySQL 的 InnoDB 引擎里,辅助索引的叶子节点存的是主键值,如果能用一个辅助索引满足 WHERE 条件,第一步的扫描量就小很多。

举个例子,订单表有一个(tenant_id, create_time)的联合索引。传统分页是SELECT * FROM order WHERE tenant_id = ? ORDER BY create_time DESC LIMIT 200000, 20,优化后的写法是:

SELECT * FROM order JOIN (SELECT id FROM order WHERE tenant_id = ? ORDER BY create_time DESC LIMIT 200000, 20) t ON order.id = t.id

子查询里只查 id,扫描完 200000 行也就是读一个索引,比读完整行代价小得多。MyBatis-Plus 里可以通过自定义 Mapper 方法,把这段 SQL 写成 XML,selectPage用不了,但可以自己写一个返回 IPage 的方法。实测下来,百万级数据传统分页 2 秒多的查询,用延迟关联后基本能压到 200ms 以内。

4.4 分页 COUNT 的优化空间

分页插件每次查询都自动带上一条 COUNT 语句。如果业务 SQL 本身很复杂,COUNT 的开销可能比数据查询还大。MyBatis-Plus 有OptimizeCountSql配置,会自动尝试移除 ORDER BY 和部分无用条件来简化 COUNT。但对于特别复杂的聚合查询,建议手动指定 COUNT 逻辑,或者用缓存把 COUNT 结果短暂缓存几秒,避免每个分页请求都重复计算总数。这个取舍在高并发列表页里非常实用。

5. 批量写入:从逐条 insert 到真正批处理的性能跨越

微服务里经常有数据同步、日志落库、批量导入这类场景,一次要写几千甚至几万条记录。大多数人刚开始用 MyBatis-Plus 的saveBatch,以为调了批量方法就万事大吉,实际上如果不调整 MySQL 连接参数,saveBatch的性能提升非常有限。

5.1 批量写入的四种实现方式对比

我先用一张表格直观对比一下,在 MySQL 8.0、1000 条记录的情况下,四种方式的实际表现差异:

写入方式实际执行行为1000 条耗时参考说明
循环单条 insert执行 1000 次 INSERT约 8-15 秒每次都有完整 SQL 解析、网络 RTT
MyBatis-Plus saveBatch 未开参数内部一批 PreparedStatement 逐条提交约 5-8 秒服务端并没有合并 SQL
MyBatis-Plus saveBatch + rewriteBatchedStatementsMySQL 驱动自动合并为多值 INSERT约 1-2 秒推荐常用方式
自定义 InsertBatchSomeColumn直接生成多值 INSERT约 0.8-1.5 秒受 SQL 长度限制,需控制批次

这个测试数值会受网络、实例规格影响,但整体量级关系参考价值很高。关键差异就在rewriteBatchedStatements这个参数上。

5.2 rewriteBatchedStatements 的真实作用

MySQL 的 JDBC 驱动收到一批PreparedStatement后,默认行为是把每一条语句单独发送到服务端,不会自动合并。rewriteBatchedStatements=true之后,如果这批语句的结构完全一致,驱动会把它们改写为一条多值的INSERT INTO ... VALUES (...),(...),(...)语句批量发送。一次网络往返处理 1000 条数据,性能自然完全不一样。

连接串写法很直接:

jdbc:mysql://your-database-host:3306/dbname?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true

不过要注意,这个参数只是"驱动层帮你合批",如果执行环境里没有走到驱动层的批量接口,也不会生效。MyBatis 的 Executor 类型要配合ExecutorType.BATCH,或者干脆用自定义的InsertBatchSomeColumn方法,直接生成多值 SQL,不依赖驱动行为。

5.3 单批次大小和 max_allowed_packet 的取舍

多值 INSERT 的语句长度会和批次数同步增长,超过 MySQL 的max_allowed_packet(默认通常是 64MB,实际环境可能更小)就会直接报错。更常见的现象是批次过大时,一条 SQL 占用的内存和锁时间都会明显增加,反而拖慢插入性能。

比较稳妥的做法是单批次控制在 500-1000 条之间。如果每条记录字段很多,比如大文本字段,批次还要再缩小一些。批量插入方法外面要显式包上事务,开启事务后 MySQL 会使用表级锁和更快的插入路径,也能避免每批提交的 fsync 开销。实测中,一批 1000 条、每条 20 个字段的场景,从我刚才说的 8 秒降到 1 秒左右,就是一个连接参数加一个事务注解的事。

5.4 批量写入时还有一个容易踩的坑

ExecutorType.BATCH 模式下,同一个会话里第一次查询之后如果执行了 insert,再查同一条数据,MyBatis 可能会返回缓存里还没刷入数据库的旧值。所以在批量写入的事务里,不要假设插入后立刻能查询到这一批数据。如果业务逻辑是"插入后马上按某个唯一键反查",建议先flushStatements()强制刷写,或者拆成两段事务。

6. 慢 SQL 治理:从日志观察到压测复盘的全链路

性能优化做到最后,拼的不是哪个函数写得巧妙,而是能不能把问题从系统里干净地捞出来。MyBatis-Plus 生成的 SQL 大多数是动态拼接的,逻辑上容易看懂,但执行计划是否合理,必须结合数据库侧的数据来做判断。

6.1 慢 SQL 的观测链路设计

在微服务里,每个服务都该有统一的日志格式,至少包含 traceId、服务名、方法名、SQL 耗时。这样从调用链入口一进来,每一跳的耗时都能串起来。MyBatis-Plus 这边可以通过配置开启 SQL 执行日志打印,但生产环境不建议每次都打印完整 SQL,会刷爆日志。更好的办法是开启 MySQL 的慢查询日志,阈值设置在 200ms 或 500ms,由数据库层主动暴露真正的慢语句。

如果使用 Druid 连接池,StatFilter里也能直接看到每个 Mapper 方法的执行次数、耗时和并发情况。压测期间把这些指标打开,QPS 上不去时很快就能定位瓶颈出在哪个具体 SQL 上。

6.2 一次真实的压测复盘:条件顺序和索引不匹配

某个日报统计接口压测时 QPS 只能到 80 左右,接口本身逻辑很简单,就是按省份和时间范围从订单表里做聚合。翻看日志发现慢 SQL 是一个二十多行的聚合查询,执行计划显示 type 为 ALL,也就是全表扫描。

当时怀疑是条件字段没有索引,检查后发现索引是建在(province_code, create_time)上的,但 MyBatis-Plus 用 LambdaQueryWrapper 拼接出来的条件顺序是create_time在前、province_code在后。MySQL 里联合索引要遵循最左前缀规则,create_time不是索引第一列,这个条件组合没法命中联合索引,只能全表扫描。

解决办法不是改 SQL,而是先去看一下实体表上索引定义能否覆盖所有查询分支,或者直接让查询条件按索引顺序拼接。实际项目里这类问题很常见,尤其是查询条件通过 Wrapper 动态拼接时,开发人员对底层索引结构的感知被完全切断了。这也是我建议团队在写复杂查询时,一定保留EXPLAIN检查这一步的根本原因。

6.3 逻辑删除字段对执行计划的影响

MyBatis-Plus 的逻辑删除功能会自动把is_deleted条件拼到所有查询 SQL 上。这个字段本身取值只有 0 和 1,区分度很低,如果业务查询条件里没有其他高区分度的过滤字段,MySQL 很可能会放弃索引而选择全表扫描。

有一个印象很深的案例:一个用户表按mobile查询时索引生效,但分页列表里按status和create_time组合查询时执行计划变得非常差,排查后发现是组合查询本来就扫得多,加上is_deleted条件后索引选择器对成本评估发生了偏移。这类问题的通用解法是尽量把is_deleted放入联合索引的最后一列,或者直接保证查询条件里有高区分度字段。

6.4 一页 EXPLAIN 的输出比一段臆测更有价值

性能排查时最怕"我觉得应该走索引"这种话。每次遇到慢 SQL,我都会把 MyBatis-Plus 打印出来的完整 SQL 复制出来,在前面加上EXPLAIN跑一遍。重点看三列:type 是不是从ALL变成了range或ref,key 是否真的用上了目标索引,rows 的估算扫描行数和实际数据量级是否匹配。只要这三列正常,慢 SQL 大概率不是查询本身的问题,而是网络、锁等待或者业务逻辑上的事,那就得换一条路排查。

做了几个项目的慢 SQL 治理之后,我的体会是:MyBatis-Plus 这类 ORM 框架给了开发者很大的便利,也让很多人失去了对 SQL 执行到数据库底层时的敏感度。框架的高级用法可以提升开发效率,但最终能不能扛住生产环境的流量,还是取决于表结构设计、索引规划和执行计划检查这些基础功夫。定制化 SQL 注入器、分页策略、批量写入优化这些手段,本质上都是在基础功夫做扎实之后,再往上叠加的效率杠杆。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 9:51:27

从零搭建图表设计系统:数据模型、布局算法与SVG渲染实践

说实话&#xff0c;我一开始看到"diagram-design"这个标题的时候&#xff0c;脑子里闪过的第一个念头是&#xff1a;这不就是画流程图吗&#xff1f;有什么好设计的&#xff1f;但等我真正把这套图表设计系统从零搭起来、跑到上线、再被各种需求反复折腾过之后&#…

作者头像 李华
网站建设 2026/10/21 9:51:19

Spring Boot自动装配到Redis缓存:大厂Java面试核心链路串讲

如果你正在准备Java后端面试&#xff0c;我猜你对下面这几句话多少有点熟悉&#xff1a;“讲一下Spring Boot的自动装配原理”、“Redis的缓存穿透、击穿、雪崩有什么区别”、“先更新数据库还是先删缓存”。这些问题单拎出来都不算难&#xff0c;但很多人真正坐到面试官对面时…

作者头像 李华
网站建设 2026/10/21 9:51:00

如何写好README:开源项目的第一印象与用户留存指南

我最早对README产生执念&#xff0c;是因为看过太多项目毁在一份糟糕的README上。明明代码写得不错&#xff0c;功能也齐全&#xff0c;但打开仓库第一眼看到的是“项目名称”加三行没有重点的废话&#xff0c;连安装命令都要猜&#xff0c;用户试了两分钟就关掉了页面。反过来…

作者头像 李华
网站建设 2026/10/21 9:50:28

拖拽式图表设计器核心实现:数据模型、交互与渲染方案

我前前后后做过好几个与“可视化搭建”相关的项目&#xff0c;其中代号diagram-design的这个拖拽式图表设计器&#xff0c;是我个人比较满意的一个。它面向的核心场景是“让非研发人员也能通过拖拽和配置&#xff0c;快速产出结构清晰、层级明确、可直接用于汇报或文档的图表”…

作者头像 李华
网站建设 2026/10/21 9:47:52

艺术感与手作工艺结合:artcraft木质收纳盒完整制作流程

artcraft 这个名字&#xff0c;我第一眼看到就琢磨上了。art 是艺术&#xff0c;craft 是工艺&#xff0c;两词拼在一起&#xff0c;指向一个我一直很感兴趣的方向&#xff1a;艺术创意如何真正落地成物件&#xff0c;而不是永远停在屏幕和图纸上。我花了两周时间做了一个完整的…

作者头像 李华
网站建设 2026/10/21 9:46:45

Abaqus双洞隧道仿真建模:偏压浅埋小净距工况实战要点

做岩土数值仿真这些年&#xff0c;Abaqus里让我最纠结的模型&#xff0c;不是超高层基础也不是大坝填筑&#xff0c;而是隧道开挖。尤其是双洞隧道&#xff0c;再加上偏压、浅埋、小净距这些特殊工况&#xff0c;一个模型里全叠上&#xff0c;建模但凡手一抖&#xff0c;后面算…

作者头像 李华