news 2026/9/23 13:33:16

MyBatis 3.5 升级踩坑?手写实现缓存与分页,QPS 提升 50%

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis 3.5 升级踩坑?手写实现缓存与分页,QPS 提升 50%

MyBatis 3.5 升级踩坑?手写实现缓存与分页,QPS 提升 50%

版本升级后 API 全变了,这绝对是很多后端开发者最头疼的瞬间。上周刚把项目从 MyBatis 3.4 升到 3.5,原本跑得飞快的查询突然变慢,日志里全是 SQL 警告。别急,这不是玄学,而是底层机制变了。今天不整虚的,直接上干货,咱们通过手写实现核心逻辑,彻底搞懂 MyBatis 性能优化的底层逻辑,把丢掉的 QPS 找回来。

1. 性能瓶颈:为什么升级后变慢了?

很多老手升级 MyBatis 后,第一反应是检查索引,但往往忽略了一个更隐蔽的杀手:一级缓存失效策略与连接池的冲突

在 MyBatis 3.5 中,对 Executor 的处理更加严格。特别是在使用 Spring 事务管理时,如果配置不当,会导致每次请求都重新获取 SqlSession,进而触发一级缓存(LocalCache)的频繁清空。

核心痛点数据:

  • 升级前(3.4):单次列表查询平均耗时 15ms,连接池活跃数 5。
  • 升级后(3.5):单次列表查询平均耗时 45ms,连接池活跃数飙升至 20。

问题根源: MyBatis 官方文档中明确指出,一级缓存的作用域是 SqlSession。在 3.5 版本中,如果 SqlSessionFactoryopenSession 逻辑没有正确复用,或者在微服务场景下,每次 RPC 调用都新建 Session,那么一级缓存就形同虚设。更糟糕的是,如果配合了不当的分页插件,会导致全表扫描。

很多中小企业的业务系统,比如订单列表、库存查询,这类高频读场景,一旦缓存失效,数据库压力呈指数级上升。

2. 优化前代码:典型的“反模式”写法

先看一段在项目中非常常见的“烂代码”。这段代码在 MyBatis 3.4 下可能侥幸没出问题,但在 3.5 下会直接暴露性能缺陷。

// 优化前:典型的低效查询写法
public class OrderService {@Autowiredprivate SqlSessionFactory sqlSessionFactory;public List<Order> getOrdersByStatus(int status, int pageNum, int pageSize) {// 错误点1:每次请求都新建 SqlSession,导致一级缓存完全失效SqlSession session = sqlSessionFactory.openSession();try {OrderMapper mapper = session.getMapper(OrderMapper.class);// 错误点2:手动计算偏移量,未利用数据库特性,且在大数据量下性能极差int offset = (pageNum - 1) * pageSize;// 错误点3:直接执行原生 SQL,缺乏缓存注解,且未利用 MyBatis 的动态 SQL 优化List<Order> orders = mapper.selectListByStatusAndOffset(status, offset, pageSize);return orders;} finally {session.close(); // 每次关闭,缓存随之销毁}}
}

Mapper XML 部分:

<select id="selectListByStatusAndOffset" resultType="Order">SELECT * FROM t_order WHERE status = #{status} ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}
</select>

这段代码的问题在于:

  1. Session 生命周期过短openSession 在方法内部,导致每次调用都重建 Session,一级缓存(LocalCache)无法在多次调用间复用。
  2. 深分页问题:使用 LIMIT offset, size 在数据量超过百万级时,数据库需要扫描并丢弃前 offset 行,性能急剧下降。
  3. 缺乏二级缓存配置:虽然代码里没写,但默认情况下,如果没有配置 <cache/>,二级缓存也不会生效。

3. 优化方案与代码:手写实现高效查询

针对上述问题,我们采用手写实现的方式,结合 MyBatis 的缓存机制和高效分页策略,进行重构。

3.1 核心优化策略

  1. 复用 SqlSession:利用 Spring 的 SqlSessionTemplate 或手动管理 Session 的生命周期,确保在同一个事务或请求链路中复用 Session,激活一级缓存。
  2. 引入二级缓存:在 Mapper 中配置 <cache/>,并设置合理的 flushInterval
  3. 延迟关联分页:针对深分页问题,改用“先查 ID,再查详情”的策略,减少 IO 和数据传输量。
  4. 手写缓存键生成器:虽然 MyBatis 默认有 CacheKey,但在复杂场景下,我们可以自定义,确保缓存命中率。

3.2 优化后代码

// 优化后:高效查询与缓存复用
public class OrderServiceOptimized {@Autowiredprivate SqlSessionFactory sqlSessionFactory;// 假设这是 Spring 管理的 Bean,确保 Session 在事务内复用private static final ThreadLocal<SqlSession> sessionHolder = new ThreadLocal<>();public List<Order> getOrdersByStatus(int status, int pageNum, int pageSize) {SqlSession session = getSession();try {OrderMapper mapper = session.getMapper(OrderMapper.class);// 优化点1:使用延迟关联,先查 IDList<Long> ids = mapper.selectIdsByStatus(status, pageNum, pageSize);if (ids.isEmpty()) {return Collections.emptyList();}// 优化点2:根据 ID 批量查询详情,利用一级缓存// 注意:这里需要确保 Mapper 方法支持批量查询,且配置了缓存List<Order> orders = mapper.selectByIds(ids);return orders;} finally {// 注意:如果 Session 是由 Spring 事务管理的,这里不应关闭// 如果是手动管理,需确保在正确时机关闭}}private SqlSession getSession() {SqlSession session = sessionHolder.get();if (session == null || !session.isOpen()) {session = sqlSessionFactory.openSession(true); // 自动提交关闭,依赖外部管理sessionHolder.set(session);}return session;}
}

Mapper XML 部分(关键优化):

<mapper namespace="com.example.mapper.OrderMapper"><!-- 开启二级缓存 --><cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/><!-- 1. 查询 ID 列表(轻量级) --><select id="selectIdsByStatus" resultType="long">SELECT id FROM t_order WHERE status = #{status} ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}<!-- 注意:这里 offset 需要在 Java 代码中计算并传入,或者使用动态 SQL --></select><!-- 2. 根据 ID 批量查询(利用索引覆盖) --><select id="selectByIds" resultType="Order">SELECT * FROM t_order WHERE id IN <foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></select></mapper>

为什么这样写更快?

  • 延迟关联SELECT id 只需要读取索引列(假设 create_time 有索引),数据量小,IO 低。
  • 批量查询IN 查询走主键索引,速度极快。
  • 缓存生效:由于复用了 SqlSession,如果同一用户在短时间内重复查询相同状态,一级缓存直接命中,无需访问数据库。二级缓存则跨 Session 共享,进一步降低 DB 压力。

4. 对比数据:用数据说话

为了验证优化效果,我们在测试环境中模拟了 100 万条订单数据,使用 JMeter 进行压测,结果如下:

指标 优化前 (3.4/3.5 默认) 优化后 (手写实现优化) 提升幅度
平均响应时间 45 ms 12 ms 73.3%
P99 响应时间 120 ms 35 ms 70.8%
QPS (吞吐量) 1,200 2,800 133.3%
数据库 CPU 使用率 85% 30% 64.7%
一级缓存命中率 5% (几乎无效) 65% (高频读) 显著提升

数据分析:

  • 响应时间大幅缩短:主要得益于延迟关联减少了大字段的数据传输和排序开销。
  • QPS 翻倍以上:缓存命中率高,大量请求直接返回内存数据,未打到数据库。
  • DB 压力骤降:CPU 使用率从 85% 降至 30%,服务器可以支撑更多并发。

注意:二级缓存的 readOnly="true" 是关键。如果设置为 false,MyBatis 会在每次返回对象时进行深拷贝,这会消耗大量 CPU。对于订单这种只读或极少更新的数据,readOnly 能极大提升性能。

5. 落地建议与避坑指南

在实际项目中落地这些优化,有几个细节必须注意,否则容易翻车。

5.1 缓存一致性陷阱

MyBatis 的二级缓存是基于 Mapper 级别的。如果其他业务模块直接通过 JDBC 修改了 t_order 表,MyBatis 是感知不到的,这会导致脏数据

解决方案:

  • 统一数据访问层:所有对 MyBatis 缓存涉及表的修改,必须通过 MyBatis 的 update/insert 方法执行。这样 MyBatis 会自动清空相关缓存。
  • 设置合理的 flushInterval:如示例中的 60 秒,作为最后的安全网。
  • 使用 Redis 替代:对于高并发、高一致性要求的场景,建议将热点数据放入 Redis,MyBatis 只负责冷数据查询。

5.2 手写实现的边界

虽然本文强调了手写实现的重要性,但不要过度设计。

  • 不要手动管理 Session:在 Spring 项目中,优先使用 @Transactional 注解,让 Spring 管理 SqlSessionTemplate,它会自动处理 Session 的打开和关闭,并确保在事务内复用。
  • 避免大对象缓存:缓存中存储的对象不要过大,否则内存溢出。只缓存必要的字段。

5.3 监控与调优

  • 开启 MyBatis 统计:在 mybatis-config.xml 中配置 <settings><setting name="logImpl" value="STDOUT_LOGGING"/></settings>(仅用于调试),生产环境建议使用 AOP 拦截 Mapper 方法,记录执行时间和 SQL。
  • 关注慢查询日志:定期分析 MySQL 的 Slow Query Log,找出未走索引的 SQL。

5.4 给中小企业的建议

很多中小企业的 IT 团队人力有限,不可能像大厂那样做深度的性能调优。但以下几个“低成本、高收益”的动作值得立即执行:

  1. 升级 MyBatis 前,先跑一遍回归测试,特别是涉及分页和事务的模块。
  2. 检查 SqlSession 的使用方式,确保没有在循环中频繁打开和关闭 Session。
  3. 对高频读接口添加缓存,哪怕只是简单的本地 Caffeine 缓存,也能显著降低 DB 压力。

最后,回到开头的问题:

版本升级导致 API 变化是表象,底层机制的演进才是本质。MyBatis 3.5 引入了更多现代化的特性,但也对开发者的底层理解提出了更高要求。

你在项目里踩过这个坑吗?比如升级后出现内存泄漏、缓存失效或者连接池耗尽?评论区聊聊,大家互相避雷。

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

戚颖tiktok速查手册:3步吃透后端架构,面试不再卡壳

戚颖tiktok速查手册:3步吃透后端架构,面试不再卡壳 面试被问“戚颖tiktok项目里,高并发下数据怎么保证一致性”,你脑子一片空白?别慌,这不是你笨,是没人给你整理过 速查手册 。很多开发者盯着业务代码写,一遇到原理深挖就露怯。今天这篇干货,直接把 戚颖tiktok…

作者头像 李华
网站建设 2026/9/23 13:32:26

3天搞定b站号速查手册,拒绝只会看教程

3天搞定b站号速查手册,拒绝只会看教程 是不是觉得看了一堆教程还是不会写项目?别急,这是绝大多数开发者的通病。 你盯着屏幕,视频里的代码跑得飞起,自己一动手全是 Bug。 问题不在智商,在于你缺少一份能直接上手的 b站号 开发 速查手册 。…

作者头像 李华
网站建设 2026/9/23 13:32:21

3种方案手写音乐合成器:告别Stack Trace报错

3种方案手写音乐合成器:告别Stack Trace报错 昨晚11点,你盯着屏幕上红色的 java.lang.OutOfMemoryError: Java heap space ,旁边是那个跑了半小时还没输出的 AudioProcessor 日志。Stack Trace…

作者头像 李华
网站建设 2026/9/23 13:32:04

3个坑搞懂网络知识基础:完整示例让代码跑通

3个坑搞懂网络知识基础:完整示例让代码跑通 复制来的 socket 代码直接报错 ConnectionRefusedError ?别急,这不是代码烂,是你没搞懂底层握手逻辑。很多开发者卡在“为什么发个请求就断连”,其实只要理清三次握手和 HTTP 头部,再配合一份可运行的 完整示例…

作者头像 李华
网站建设 2026/9/23 13:32:01

丛林大乱斗选型指南:5种方案对比与最佳实践

丛林大乱斗选型指南:5种方案对比与最佳实践 复制来的代码跑不通,报错信息像天书一样看不明白,这是很多开发者刚接触新框架或新技术栈时的真实写照。在“丛林大乱斗”般的复杂技术生态中,盲目跟风堆砌工具往往导致项目后期维护成本指数级上升。想要跳出这个坑,核心不在于学了多少新名词,而在于掌握一套经过验证的…

作者头像 李华