news 2026/10/14 7:56:41

微服务架构下MyBatis-Plus实战:分页、逻辑删除、乐观锁与多租户配置全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微服务架构下MyBatis-Plus实战:分页、逻辑删除、乐观锁与多租户配置全解析

“微服务1 -- MybatisPlus”这个标题挂在我笔记里大半年了,现在终于有空把它整理成一篇能直接给团队看的实操记录。当时我们做某电商系统的微服务化改造,第一刀切的就是 DAO 层,而 DAO 层的技术选型直接决定了后面每个服务的代码风格和踩坑频率。这篇内容适合正在纠结用 MyBatis 还是 MyBatis-Plus 的团队,也适合已经在用 MP 但想把分页插件、逻辑删除、乐观锁、多租户这些功能一次配对儿的开发者。我会把项目里真实用到的配置和踩过的坑原样放出来,能少走弯路就少走弯路。

1. 为什么微服务项目里兜兜转转还是选了 MybatisPlus

1.1 从 SSM 到微服务:ORM 选型的几个岔路口

先交代一下背景。我们之前的老系统是典型的 SSM 单体结构,MyBatis 写 SQL 写了五六年,虽然说不上痛苦,但每个新服务都要把单表 CRUD 的 XML 从头抄一遍,实体类、Mapper 接口、Service、Controller 四层结构循环复制粘贴,时间久了就有点麻木。

做微服务改造的时候,团队里其实有三种声音。第一种是继续用原生 MyBatis,理由是大家熟、可控、改造成本最低。第二种是想试 JPA/Hibernate,理由是实体映射省事,还带一级缓存、二级缓存。第三种才是 MybatisPlus。我们最后选了 MP,不是因为 JPA 不好,而是因为微服务场景下 JPA 那套自动建表、实体直接关联查询的玩法,在多团队并行开发时反而容易失控——你根本不知道哪个服务把哪张表改了。MP 走的是增强 MyBatis 而不是替换它的路线,单表 CRUD 不用写 SQL,手写复杂查询的能力又完全保留,正好卡在“效率”和“可控”中间。

1.2 微服务对 Dao 层真正苛刻的几件事

微服务拆分之后,Dao 层面对的问题和单体时代完全不是一回事。

首先,数据源按服务隔离了。每个服务只管自己的库,不允许跨服务查别人的表。这种情况下,单表操作的频率比单体时代高得多,因为很多 join 在微服务里被拆成了多次单表查询 + 内存组装。既然单表查询是高频操作,那谁能在单表 CRUD 上省时间,谁就赢。

其次,SQL 性能问题被放大了。单体系统里一个慢 SQL 可能只拖慢一个页面;微服务里一次请求要经过四五个服务,任何一个服务的 Dao 层慢 500ms,整个调用链的延迟都会像滚雪球一样涨。所以 Dao 层必须能快速写出带索引的规范 SQL,同时还能方便地兜底查慢查询日志。

最后,团队代码风格真的很难统一。六个人的小组写 DAO 层有三种习惯:有的人喜欢全部 XML,有的人喜欢注解 SQL,有的人直接 JDBC。MP 能把方法名、返回结果、逻辑删除、悲观乐观锁这些规范强制收口,大家 review 代码的时候看的是业务逻辑而不是在哪找 SQL。

1.3 选型结论:增强而非替换

最终选 MP 的核心原因就一句话:它是 MyBatis 的增强插件,不是替代品。任何时候你觉得 MP 碍事了,完全可以绕过去写原生 SQL,Mapper 接口里的方法签名和 XML 映射照样工作。这种“能退能进”的灵活性,在微服务这种需要长期演进的架构里特别重要。换句话说,选 MP 不是押注某一种框架哲学,而是给自己保留了一条随时可以手写 SQL 的后路。

2. 项目落地第一步:MybatisPlus 依赖、版本与配置

2.1 Spring Boot 2/3 下版本怎么配

先给版本选择,这个坑我见人踩过很多次。如果你用的还是 Spring Boot 2.x,直接引mybatis-plus-boot-starter;如果项目已经上了 Spring Boot 3.x,官方单独提供了mybatis-plus-spring-boot3-starter。别自己在 pom 里手动拼mybatis和mybatis-plus-boot-starter,版本对不上会出各种莫名奇妙的报错,比如 Mapper 方法找不到、分页插件不生效。

拿我们项目举例,Spring Boot 3.2 + MybatisPlus 3.5.7 这个组合目前很稳。依赖写法如下:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>

建议:MP 版本不要追太新,等发版一两个月后再升,社区反馈稳定了再动。我们踩过一次 3.5.3 刚发布时多租户插件和分页插件顺序出问题的情况,后来回退到 3.5.2 才稳定。

2.2 配置文件里最容易埋雷的三个点

MP 的配置项很多,但真正在微服务项目里每天影响我们的,其实就那么几个。直接上一份我们线上在用的 yaml 片段:

mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

逐项说为什么这么配:

map-underscore-to-camel-case这个很多人以为 MP 默认开了,其实不一定。数据库字段create_time映射到 Java 属性createTime,靠的就是它。不开启的话,查出来结果全是 null,而且你还很难发现是配置问题,因为控制台不报错。

banner: false是关掉启动时那个大 Logo,没啥技术含量,但在微服务多模块并行启动的时候能省不少终端刷屏的时间。

id-type: assign_id是让主键默认走 MP 内置的雪花算法生成分布式 ID。微服务里不能用数据库自增主键,因为多服务往同一个逻辑库写数据时,自增 ID 没法保证全局唯一,这个后面会展开说。

2.3 分页插件必须显式注册

MP 的分页依赖一个拦截器,而这个拦截器不会因为你引了依赖就自动生效。新手最容易犯的错就是:写了一个Page<User>查询,发现 SQL 里根本没有LIMIT,数据全查出来然后在内存里截断,线上直接 OutOfMemory。

注册方式是在配置类里声明MybatisPlusInterceptor,然后往里面加PaginationInnerInterceptor:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(200L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }

setMaxLimit(200L)是我比较推荐加的一行。它会把单页最大条数限制在 200,防止有人在前端传个pageSize=999999直接把数据库拖垮。setOverflow(false)表示超过最大页数时抛出异常而不是返回空数据,这样问题暴露得更早。

注意:如果你的项目同时用了多租户插件和分页插件,addInnerInterceptor的顺序是有讲究的。官方推荐顺序是多租户、动态表名、分页、乐观锁。顺序颠倒可能导致分页 SQL 里漏掉租户条件。

3. 微服务业务里最高频的三个增强功能怎么用

3.1 逻辑删除:删库不如打标记

微服务业务系统里,“删除”这件事绝大多数是假删除。订单记录删了之后财务要对账,用户删了之后运营要恢复,这些场景物理删除都是灾难。MP 的逻辑删除用起来很省事,实体上标一个注解,全局配置里指定好值就行。

@Data public class User { private Long id; private String name; @TableLogic private Integer deleted; }

之后调用deleteById(1L),MP 生成的 SQL 会自动变成UPDATE user SET deleted=1 WHERE id=? AND deleted=0,而普通的selectById也会自动带上AND deleted=0。你不用在每个查询里手动写删除条件,这对代码整洁度的提升是很明显的。

但这里埋着一个普遍存在的坑:逻辑删除和数据库唯一索引冲突。比如用户表有唯一索引uk_mobile(mobile),用户 A 注册后“删除”了,再注册同手机号的用户 B,你以为可以插进去,结果数据库报唯一键冲突——因为那条旧记录还物理存在。我们当时的处理方案是把唯一索引改成逻辑删除标记联合唯一索引,或者用删除时间戳作为逻辑删除标记,而不是简单的 0/1 开关。具体怎么设计要看业务,但这个问题一定要提前想清楚。

3.2 乐观锁:并发更新覆盖问题的标准解法

微服务里并发更新同一个订单状态的场景很常见。两个请求同时读到订单状态为“待支付”,都去改“已支付”,最后谁后提交谁赢,前一个操作被静默覆盖。数据库的行锁能解决一部分问题,但很多时候我们需要的是业务层面的版本控制,而不是单纯锁一行。

MP 的乐观锁实现非常轻量:实体上放一个@Version字段,更新时它会自动在 SQL 里带上版本条件。

@Version private Integer version;

来看一个实际的更新过程。执行updateById(entity)时,MP 生成的 SQL 大致长这样:

UPDATE order SET status='已支付', version=2 WHERE id=123 AND version=1

version 条件匹配,则更新成功并且版本号自增;如果另一个请求已经把它从 1 改成了 2,这一条更新的影响行数就是 0。所以判断乐观锁冲突,不能只看方法有没有抛异常,而是要看返回值。

我们项目中有一个统一的重试封装:如果影响行数为 0,重新查一遍最新数据,把必要字段合并后重试,最多重试三次,三次还不成功就抛出“操作太频繁,请稍后再试”的提示。这个方案简单可靠,不需要引入额外的锁中间件,绝大多数并发覆盖场景都能解决。

3.3 字段自动填充:把 createTime 这些脏活统一收口

每个表都有create_time、update_time,如果每个 Service 里都手动 set,代码冗余不说,还容易漏。MP 的MetaObjectHandler可以在插入和更新时自动填充字段。

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "creator", String.class, UserContext.getUserId()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, "updater", String.class, UserContext.getUserId()); } }

微服务场景里我特别强调creator和updater的自动填充。因为在微服务架构下,操作人通常是网关解析完 Token 后放到请求上下文里的,Service 层如果不统一从上下文取,很容易出现到处从 ThreadLocal 取值的情况。用自动填充收口之后,任何服务写入数据都天然带上操作人,审计和排障会舒服很多。

实体类上记得给这些字段加@TableField(fill = FieldFill.INSERT)或者@TableField(fill = FieldFill.INSERT_UPDATE)注解,否则处理器不会触发。

4. 分布式 ID、多租户、多数据源:微服务接触到的三座山

4.1 分布式 ID:雪花算法与多实例的 workerId

为啥微服务里不能用数据库自增主键,用一句话说就是:服务拆了,库也可以拆,但 ID 生成必须全局唯一。两张分表各自从 1 自增,合到一起就冲突了。所以我们在全局配置里把id-type设为assign_id,让 MP 用内置的雪花算法生成 64 位长整型 ID。

雪花算法本身其实不复杂,核心就是时间戳 + 机器 ID + 序列号三段式拼接。真正需要注意的,是多实例部署时 workerId(机器 ID)的分配。如果你在 Kubernetes 里跑 10 个副本,又没有给每个实例分配不同的 workerId,那同一个毫秒内两个实例生成的 ID 就可能冲突。我们当时踩过这个坑——服务一扩容,偶尔就出现主键重复,排查了半天才发现是 workerId 都用了默认值 0。

建议把 workerId 和 datacenterId 配置到配置中心,每次实例启动时从配置里拉取唯一值。如果实在不想单独管理,业界也有用 Redis 生成 ID 或者用数据库号段模式的方案,但 MP 内置的雪花已经够用了,不要为了用分布式 ID 而额外引入一个中间件。

4.2 多租户插件:SaaS 下的自动条件拼接

如果你们做的是 SaaS 系统,租户数据隔离基本是刚需。最省事的办法是每张表加tenant_id字段,所有查询和写入都带租户条件。但问题是,开发时漏写一个条件,就会把 A 租户的数据查给 B 租户,属于严重事故。

MP 的TenantLineInnerInterceptor可以在 SQL 执行前自动给表加上租户条件。注册方式如下:

interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { return new LongValue(TenantContext.getTenantId()); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean ignoreTable(String tableName) { // 这些全局表不需要租户隔离 return "sys_config".equals(tableName) || "dict".equals(tableName); } }));

ignoreTable这个方法是用来排除不需要租户隔离的表的,比如系统配置表、数据字典表。这里要特别提醒:那些表如果不排除,MP 会自动给它们拼上tenant_id条件,查出来的全局配置直接为空,而且这个问题特别隐蔽,往往只有线上数据才能复现。

多租户插件还有一个使用边界:它适合单表操作和简单查询,业务复杂的连表 update/delete 还是建议在 XML 里手动写 SQL,否则自动拼接的租户条件可能把你的 SQL 改得面目全非。团队里要有这个意识,别把复杂 SQL 硬塞给 MP。

4.3 多数据源:一个服务多个库怎么切

正常情况下微服务提倡一个服务一个库,但现实中总有些历史包袱:比如某个服务需要同时读写 MySQL 和 PostgreSQL,或者主从分离需要读从库。这个时候可以用dynamic-datasource-spring-boot-starter,在配置里声明多个数据源,然后在方法上加@DS注解切换。

spring: datasource: dynamic: primary: master datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db username: root password: root slave: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db_slave username: root password: root
@DS("slave") public List<Order> queryFromSlave() { return orderMapper.selectList(...); }

这个方案用起来很顺手,但有一个必须知道的边界:本地事务@Transactional只能覆盖当前线程正在使用的那个数据源。如果你在一个事务里先@DS("master")写了主库,再切到@DS("slave")读写从库,两边并不会有真正的事务一致性。对于读多写少的场景这没问题,一旦涉及跨库写操作,别指望本地事务帮你兜底,老老实实考虑分布式事务方案。

5. 代码生成器:几十张表的 CRUD 一把梭出来

5.1 先定好 BaseEntity,否则生成出来的东西没法看

微服务改造时,旧系统里有几十张表,一张张手写实体类不现实。MP 的代码生成器可以一次性生成实体、Mapper、Service、Controller,输出效率提升非常明显。但前提是——先把 BaseEntity 定好。

我们项目里所有实体都继承同一个基类:

@Data public abstract class BaseEntity { @TableId(type = IdType.ASSIGN_ID) private Long id; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted; @Version private Integer version; }

这个基类把微服务 Dao 层最常用的三个横切能力——分布式 ID、逻辑删除、乐观锁——全部收口了。子类里只写业务字段,生成器生成的代码一下子整洁很多。团队新人接手时也很容易理解:这些字段是框架基类负责的,不需要手动管。

5.2 FastAutoGenerator 实操与模板注意点

从 MP 3.5.1 开始,官方推荐用FastAutoGenerator,配置起来比老的AutoGenerator简洁太多。我贴一个我们实际在用的生成配置:

FastAutoGenerator.create( "jdbc:mysql://localhost:3306/order_db", "user", "password") .globalConfig(builder -> builder .author("dev") .outputDir("/path/to/src/main/java") .disableOpenDir()) .packageConfig(builder -> builder .parent("com.example.order") .moduleName("order") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller")) .strategyConfig(builder -> builder .addInclude("t_order", "t_order_item") .addTablePrefix("t_") .entityBuilder() .enableLombok() .superClass(BaseEntity.class) .addSuperEntityColumns("id", "create_time", "update_time", "deleted", "version") .controllerBuilder() .restStyle() .enableFileOverride()) .execute();

几个参数我解释一下:

addInclude指定要生成的表;addTablePrefix("t_")会把表名t_order转换成实体名Order,而不是TOrder。superClass(BaseEntity.class)加上addSuperEntityColumns指定哪些字段是基类字段,这样生成器就不会在子类里重复生成 id、createTime 这些字段。

restStyle()是让 Controller 生成的是@RestController风格的接口,而不是传统页面跳转风格。enableFileOverride()允许覆盖已存在的文件,这个在重新生成的时候很有用,但也容易把手工改动覆盖掉,用之前一定要确认当前文件是否已经是生成物。

5.3 生成之后还要改什么

生成器生成的东西只能当草稿,距离上线标准还差几步。

首先,Controller 的返回类型一般不直接用实体,而是统一返回一个R<T>包装对象。我们的做法是生成后把 Controller 里的返回类型从Order改成R<Order>,并把接口路径统一加/api/order前缀。微服务间调用时,统一的返回结构能省不少下游解析的麻烦。

其次,Mapper 接口里可以根据业务补自定义方法。比如订单详情要按订单号 + 门店号查询,生成器不会知道这些,我们就会在 Mapper 里手写一个方法,配上 XML 或者注解 SQL。这就是我前面说的,MP 可以增强但不绑架你的原因。

最后,代码生成器不是只跑一次就完事。业务表结构调整后,记得重新生成一遍实体类。我们团队的习惯是每个月跑一次批量生成,然后把新表和变更表的实体同步进代码库,review 之后再提交。这样能避免手写实体类跟数据库表结构越来越脱节的问题。

6. SQL 性能与慢 SQL 排查:微服务里慢 SQL 会被放大

6.1 深分页优化:覆盖索引加游标分页

MP 的分页插件对使用者很友好,但对数据库不友好。分页越深,LIMIT offset, size的 offset 越大,数据库要扫描并丢弃前面的所有行,性能就急剧下降。我们线上有一条订单查询,原来用LIMIT 100000, 20,执行耗时跑到 2 秒以上。在微服务链路里,这一个查询就拖垮了整个下单详情接口。

优化方案有两个,两个都简单。

第一种是覆盖索引。让查询的 WHERE 条件和 ORDER BY 字段尽量走同一个索引,且 SELECT 的字段都在这个索引里,这样数据库扫描的是索引而不是回表查全行。MP 生成的单表查询不影响你建索引,但你要分析QueryWrapper里的查询条件,确认有没有可用的联合索引。

第二种是游标分页。业务如果允许,把深分页改成基于 ID 的翻页:

SELECT * FROM t_order WHERE status = 1 AND id > #{lastId} ORDER BY id ASC LIMIT 20;

前端翻页传来上一页最后一条记录的 ID,每次查询都从id > lastId开始,数据库走主键索引,速度能到几十毫秒。这个方案在订单列表、日志列表这种按时间倒序的场景特别实用。需要注意的只是排序字段必须和索引匹配,如果业务要求按某个非唯一字段 ORDER BY,要自行处理同值排序的边界情况。

6.2 SQL 日志:开发环境看,生产别打

很多团队刚开始用 MP 的时候,为了调试方便,会在全局配置里把log-impl设成StdOutImpl,控制台能看到完整 SQL。这个操作在开发环境没问题,但一旦这套配置被复制到生产环境,就是灾难。生产环境每个请求吞吐量都是几百上千,全量打印 SQL 会把磁盘 IO、日志系统全部打爆,而且 SQL 里往往带了用户手机号等敏感字段,合规风险也不小。

我们项目的做法是:多环境配置文件拆分。开发环境用StdOutImpl,测试环境用NoLoggingImpl+ 慢 SQL 日志,生产环境直接关闭 SQL 日志,依赖数据库层的慢查询日志去定位问题。

如果确实需要在生产排查某条慢 SQL,不要全局打开日志,用线上数据库的slow_query_log,或者临时开一个 TraceID 打印链路,查完立刻关掉。这是我在实际运维中踩过坑后的忠告。

6.3 saveBatch 也没有想象中那么省心

MP 的saveBatch批量插入接口看起来很强大,一次传一个 List,理论上帮你拼成多条 INSERT。但它默认行为其实是分批执行,而且如果没有正确的 JDBC 连接参数,批处理的提升远没有想象中那么大。

MySQL 的 JDBC 驱动默认是不开启真正的批处理的,需要在连接串上加一个参数:

jdbc:mysql://localhost:3306/order_db?rewriteBatchedStatements=true

这个参数加上之后,executeBatch()才会把多条 INSERT 重写成一条多 VALUES 的 INSERT,性能能提升一个数量级。我们导数据的时候测试过,同样的 1 万条记录,不加这个参数要 40 秒,加上后不到 5 秒。

还有一点:saveBatch内部有默认分批大小,默认一次批处理 1000 条。如果你传入一个 10 万的 List,不要指望它一条 SQL 搞定,它还是会按批拆开执行的。所以批量插入之前还是要注意 List 大小,避免一次性堆积太多对象在内存里。

7. 事务与数据一致性:MybatisPlus 管不到的边界

7.1 @Transactional 在微服务里最常见的失效方式

用了 MP 之后,很多人就把 Dao 层 CRUD 全部交给 BaseMapper,然后在 Service 方法上标一个@Transactional就完事了。但微服务拆开后,事务失效的坑比单体现得更隐蔽。

第一种典型场景:同类内部方法调用。比如 Service 里一个方法调用本类的另一个@Transactional方法,Spring 的代理机制不会拦截这次调用,事务直接失效。解决办法是把事务方法拆到另一个 Bean,或者自己注入代理对象再调用。

第二种是异常被吞。@Transactional默认只在 RuntimeException 时回滚,如果你 try-catch 里把异常吞掉了,事务自然提交。我们项目里有过一个血泪教训:批量导入时某个数据格式错误,被 catch 后记录日志继续执行,结果前面几万条数据一起提交了,后面发现业务逻辑错误,只能手工在数据库里翻数据回滚。

第三是非 public 方法上的注解不生效。Spring 事务代理是基于接口或者代理对象的,private 方法根本不会被拦截。

我的建议是:事务注解尽量放在对外暴露的 Service 接口实现上,颗粒度要明确,一个大方法里混着多个内部调用时,要想清楚事务边界到底在哪,而不是随手加注解。

7.2 本地事务不解决跨服务一致性问题

这是微服务场景下必须认清的一件事。订单服务在自己的本地事务里写订单表,然后通过 HTTP 或者消息队列调用商品服务扣库存。即使你两个服务都加了@Transactional,那也只是各自数据库的本地事务,两个事务之间没有任何关系。订单事务提交了,商品服务扣库存失败了,数据就不一致了。

MP 本身是 ORM 层的工具,天然管不到这个层面。我们在项目里的处理思路是:不强求强一致,通过最终一致性来保障。具体就是本地消息表,业务操作和发消息放进同一个本地事务,消息表里的消息由定时任务扫描后可靠投递,下游消费成功后回执,失败则重试。这套方案虽然要多写一些代码,但比引入外部分布式事务中间件要稳定得多。

还有一个实操提醒:调用远程服务的方法千万别放在本地事务里。否则远程服务响应慢时,数据库连接会被事务一直占着,连接池很快耗尽,整个服务跟着瘫痪。我们经历过一次线上事故,事后加了一个规则:@Transactional方法里不允许出现 HTTP 调用。

8. 常见问题与避坑速查表

8.1 高频问题对照表

下面这张表我是按实际踩坑频率排序的,基本都是开发中会反复遇到的问题,直接照着排查就行。

问题现象根本原因解决方案
分页查询返回全部数据没有注册PaginationInnerInterceptor在MybatisPlusInterceptor中显式添加分页拦截器
逻辑删除数据导致唯一索引冲突旧记录还在表里,被deleted=1标记改唯一索引为(business_key, deleted)联合唯一索引
updateById返回 0 但数据没变化多次更新同一实体,版本号被@Version拦截重新查询最新数据后合并字段再更新,或处理冲突
查询结果字段全是 nullmap-underscore-to-camel-case没开启开启驼峰映射配置
多租户插件生效后查不到全局配置租户拦截器拼接了tenant_id条件在ignoreTable中排除字典、配置等全局表
SQL 日志在生产环境刷屏log-impl设置成StdOutImpl多环境配置,生产关闭 SQL 日志
saveBatch批量插入很慢MySQL 驱动未开启批量重写连接串加rewriteBatchedStatements=true
事务没回滚异常被 catch 吞掉或同类内部调用按代理规则拆分事务边界,RPC 调用移出事务

8.2 我踩过而且不希望你再踩的三个坑

第一个坑是@TableField的 fill 属性填错。有次重构把实体类的updateTime从INSERT_UPDATE改成了INSERT,结果所有更新操作都不再刷新时间,业务方反馈“订单更新时间怎么一直不变”,排查了很久才发现是注解问题。这种注解类 bug 编译器不会报错,只能通过数据对账发现,所以改实体类字段时一定要把注解一起 review 掉。

第二个坑是 Wrapper 过于复杂。MP 的QueryWrapper支持and、or、嵌套条件,看起来很方便,但过度使用会让业务代码变成一团乱麻。我们后来定了一条铁律:超过三层嵌套或者超过五个查询条件的,一律写 XML SQL,不在 Java 里硬拼 Lambda。这么做不仅性能可控,review 时一眼也能看出 SQL 全貌。

第三个坑是忽略全局配置的继承问题。如果多个微服务都引入了同一个公共配置依赖,某个服务里覆盖了global-config的部分配置,可能会导致该服务的逻辑删除、ID 策略跟其他服务不一致。建议公共配置统一维护,不要在各自服务里随意覆盖 MP 全局配置。

9. 写在系列的第一篇之后

我个人在实际操作中的体会是:MybatisPlus 适合微服务项目里 80% 的 Dao 层场景,但不要把 20% 的复杂查询也强行往里塞。它最舒服的用法是配合明确的工程规范——所有简单单表单操作走 MP,复杂查询显式写 XML,逻辑删除和乐观锁作为全局约定。梳理清楚这个边界,MP 就不会变成技术债,而是实实在在的开发效率提升。

最后再分享一个小技巧:如果你们的数据库表字段变动频繁,给代码生成器配一个统一的执行入口,每次改表后一键重新生成实体类,再配合 diff 工具逐条 review,可以省掉大量手改映射文件的时间。这套流程我们跑了两个迭代,Dao 层的维护成本明显降下来了。

下一篇系列文章我准备聊聊微服务之间的调用链路与超时重试设计,喜欢这个方向的可以留意。

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

用Comsol计算声子晶体能带:三角格与六角格建模与扫参实操指南

最近重跑了一批Comsol声子晶体的能带模型&#xff0c;把三角格和六角格两种晶格从头到尾过了一遍。说句实话&#xff0c;能带计算这个方向&#xff0c;真正拦住人的往往不是物理&#xff0c;而是软件里的那几个关键开关&#xff1a;布洛赫周期边界怎么加、布里渊区高对称点怎么…

作者头像 李华
网站建设 2026/10/14 7:55:59

人工神经网络算法基础精讲:从感知机到反向传播的数学逻辑

简介&#xff1a;人工神经网络算法基础精讲课件&#xff0c;内容系统覆盖人工神经网络的基本概念、发展简史、生物神经元结构与信息处理机理&#xff0c;并引入人工神经元模型、激活函数及BP学习算法等核心知识点。课件从1943年M-P模型讲起&#xff0c;串联Hebb规则、感知器、H…

作者头像 李华
网站建设 2026/10/14 7:55:57

U2-Flash登顶SVG-Bench:统一字典与可微分渲染揭秘

1. SVG-Bench到底在考什么&#xff1a;先搞懂度量衡&#xff0c;再谈登顶先说个背景。代码生成图形这个赛道&#xff0c;这几年突然热闹起来&#xff0c;但一直有个尴尬的问题&#xff1a;大家都在吹自己的模型能画图&#xff0c;可一对比就发现鸡同鸭讲——有的比的是位图效果…

作者头像 李华
网站建设 2026/10/14 7:55:57

顺序表区间删除:内存布局与边界安全的深度解析

1. 这道题为什么值得花20分钟认真拆解“删除顺序表中下标i~j的所有元素”——看起来就是一道教材课后习题&#xff0c;甚至可能被很多备考同学划进“背过代码就行”的类别。但我在带某高校数据结构实验班的三年里&#xff0c;连续四届学生在模拟考试中栽在这道题上&#xff0c;…

作者头像 李华
网站建设 2026/10/14 7:55:34

getdb-RAK.pdf数据库导出方案落地与避坑指南

简介&#xff1a;《RAK&#xff1a;使用get_db进行数据库访问》是Cadence官方为Innovus、Tempus、Genus三款工具打造的Common UI数据库访问技术指南&#xff0c;面向需要频繁操作设计数据库的IC后端工程师。资源包为单个PDF文件&#xff0c;大小约1.25MB。指南从查询属性值、点…

作者头像 李华
网站建设 2026/10/14 7:54:53

PHP+Redis实现轻量消息队列的实战指南

1. 项目概述&#xff1a;为什么一个“简单消息队列”值得花一整天搭出来&#xff1f;PHP Redis 实现简单消息队列——这八个字&#xff0c;是我过去三年在某中型互联网公司后端团队里&#xff0c;被问得最多、也复用率最高的一句技术方案开场白。它不是高大上的分布式消息中间…

作者头像 李华