线上环境OOM频发,MyBatis有坑...

文章讲述了在生产环境中遇到服务频繁出现OutOfMemoryError(OOM)的问题,通过分析日志和源码发现Mybatis在处理大量SQL参数时可能导致内存无法释放,从而引发OOM。在复现问题后,作者优化SQL以避免大体积SQL拼接,并调整Docker配置以留存内存dump文件,以备后续故障排查。

图片

继上次线上 CPU 出现了报警,这次服务又开始整活了,风平浪静了没几天,看生产日志服务的运行的时候,频繁的出现 OutOfMemoryError,就是我们俗称的 OOM,这可还行!

 频繁的 OOM 直接会造成服务处于一个不可用的情况,最严重的一天,它重启了 5 次。我通过 Skywalking 查看链路调用,基本全报红了,基本处于一个瘫痪状态,因为生产该服务是分布式部署,k8s 故障恢复当即对该服务进行重启,因为是 B 端的产品,先让公司业务能用起来了,保证服务的正常使用,然后紧急查看问题。

当然这个问题很多同事不会排查,老板就让我亲自动手了。既然分配给我了,咱高低给它查出来,并且修复了。

BUG之路

先来了解下OutOfMemoryError出现的原因,无非就是两类堆内存空间不足、元空间不足。

  1. 堆内存空间不足:意味着程序存在一直有引用的对象(强引用),主要对象在引用的状态就无法被GC回收,撑爆了-Xmx堆拓展的最大值,内存不足自然就会触发堆内存溢出。

  2. 元空间:Java 8 引入了元空间概念,代替了之前堆的永久代,由于元空间属于堆外内存,不需要有对象引用,通过指针的方式表示类和元数据,之所以引用元空间就是一种 JDK 的升级优化,避免了永久代的内存溢出。详细内容参见https://t.zsxq.com/0fdOtNgEQ

常见堆内存溢出的几种情况

  1. 查询数据库返回的数据量过大,加载到内存中导致内存溢出;

  2. 代码中出现死循环情况,导致大对象一直被引用不能被 GC 回收;

  3. 资源链接池、io 流在使用完没有进行手动释放;

  4. 静态集合类里面存在引用对象,始终存在引用关系,没有进行清除;

以上属于常见的几种堆内存溢出的场景,当然有时候我们的遇到的问题都是稀奇古怪的问题,常见的问题总是很少能遇到…

现象分析

根据生产环境的报错日志来看,这边属于 Mybatis 报出的一个内存溢出情况,通过去看 Mybatis 源码发现,底层也是通过一些集合类来存放拼接的 sql,那么当然也有可能出现堆内存溢出,而且在 sql 体积比较大的情况下,接收 sql 的集合就会变的非常大,如果回收不了那么就会导致内存溢出。

图片

内存

由于我们 docker 容器里面没有一些 jstack、jmap 的工具,并且 dump 文件也没有进行保存…导致我无法通过看线程高占用内存的对象,来分析具体是什么操作发生的内存溢出,这就难了… 于是只能去网上搜搜看了,没想到真的给到我一些启发,并且有点思路大概知道是哪里的问题。

老天真的赏饭吃,我搜到了一篇关于惨遭 DruidDataSource 和 Mybatis 暗算的 OOM 文章。看起来,和我的情况很像。

给我带来了新的启发,这是 Mybatis 带来的 OOM。主要是因为 Mybatis 拼接 SQL 的时候生成的占位符和参数对象,存放在 Map 里,当 SQL 的参数多导致 SQL 太长的时候,Map 持有这些 SQL 时间较长,并且多线程同时操作,这时候内存占用就很高,从而发生 OOM。

Mybatis源码分析

通过对DynamicContext类源码查看,DynamicContext 又一个 ContextMap 类型的参数bindings,继承了 HashMap 相当于一个Map集合,接着看这个类中的 getBindings 方法,看到了 ForEachSqlNode 这类调用了 getBindings 方法,简单的说就是 ForEachSqlNode 通过 getBindings 方法,将 SQL 参数和参数的占位符统一 put 到 ContextMap 这个集合里面,主要是这里面的参数和占位符无法被 GC 回收,并发查询量多的情况下就会导致 OOM。

图片

Mybatis源码分析

图片

Mybatis源码分析

图片

Mybatis源码分析

情景复现

随后我做了线上场景的复现,通过将 SQL 语句的拼接,将 IN 里面的参数变大,然后创建 50 个线程进行执行,将 JVM 堆内存设为-Xmx256m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

图片

OOM 复现

图片

OOM 复现

这里看控制台打印的日志,服务在频繁的进行 Full GC,导致 OOM。

图片

频繁的Full GC

总结

既然发现了问题出现的原因,接下来就是对代码 SQL 进行优化,尽量避免在 sql 拼接的时候体积过大。这里告诫我们代码不能乱写,SQL 语句也不能随意写啊,有时候把问题想的过于简单确实会带来不可预知的风险。

另外,docker 中也配置了 oom 留存 dump 文件。后面如果万一再出现故障,也不至于太被动。

作者:Lxlxxx

https://juejin.cn/post/7221461552343072828

<think>我们需要在单元测试中构建一个不依赖mybatis-config.xml的SqlSessionFactory,并指定ExecutorType为BATCH。根据引用[3]中的示例,我们可以使用代码直接配置SqlSessionFactory,而不是通过XML配置文件。步骤如下:1.创建一个Configuration对象,并手动设置数据源和事务工厂。2.将Mapper接口注册到Configuration中。3.使用SqlSessionFactoryBuilder构建SqlSessionFactory。4.在打开SqlSession时传入ExecutorType.BATCH参数。注意:在批处理操作中,要注意适时提交以防止内存溢出,如引用[2]所示。以下是一个在单元测试中实现的示例代码:</think> 要在不依赖 `mybatis-config.xml` 文件的情况下使用 `ExecutorType.BATCH` 进行批量操作,您可以通过编程方式配置 `SqlSessionFactory`。以下是完整实现步骤: ### 一、核心实现代码 ```java // 创建数据源(使用H2内存数据库作为示例) PooledDataSource dataSource = new PooledDataSource(); dataSource.setDriver("org.h2.Driver"); dataSource.setUrl("jdbc:h2:mem:batchdb;DB_CLOSE_DELAY=-1"); dataSource.setUsername("sa"); dataSource.setPassword(""); // 创建事务工厂 TransactionFactory transactionFactory = new JdbcTransactionFactory(); // 手动创建Environment配置 Environment environment = new Environment("batch_env", transactionFactory, dataSource); // 创建Configuration对象并添加Mapper Configuration configuration = new Configuration(environment); configuration.addMapper(YourMapper.class); // 替换为实际的Mapper接口 // 构建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); // 使用ExecutorType.BATCH开启批处理会话 try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) { YourMapper mapper = sqlSession.getMapper(YourMapper.class); // 批量操作示例 for (int i = 0; i < batchSize; i++) { mapper.insert(yourEntity); // 执行批处理操作 // 每1000条提交一次防止内存溢出 if (i % 1000 == 999) { sqlSession.commit(); sqlSession.clearCache(); // 清除缓存释放内存[^2] } } sqlSession.commit(); // 提交剩余操作 } ``` ### 二、关键配置说明 1. **数据源配置**: ```java // 使用连接池数据源(支持MySQL/Oracle等) dataSource.setUrl("jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true"); ``` 添加 `rewriteBatchedStatements=true` 参数可提升批量插入性能[^2] 2. **批处理优化**: ```java // 添加批处理特定配置 configuration.setDefaultExecutorType(ExecutorType.BATCH); configuration.setCacheEnabled(false); // 禁用二级缓存提升性能 ``` 3. **事务控制**: ```java // 开启自动提交(不推荐用于生产) try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, true)) // 手动事务控制(推荐) try { sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); // 回滚事务 } ``` ### 三、单元测试完整示例 ```java @Test void testBatchInsertWithoutConfig() { // 1. 配置数据源 PooledDataSource dataSource = new PooledDataSource(); dataSource.setDriver("org.h2.Driver"); dataSource.setUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); // 2. 构建Environment Environment env = new Environment("test", new JdbcTransactionFactory(), dataSource); // 3. 创建Configuration Configuration config = new Configuration(env); config.addMapper(UserMapper.class); // 注册Mapper // 4. 创建SqlSessionFactory SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(config); // 5. 执行批处理 try (SqlSession session = factory.openSession(ExecutorType.BATCH)) { UserMapper mapper = session.getMapper(UserMapper.class); List<User> users = generateTestUsers(5000); // 生成测试数据 for (int i = 0; i < users.size(); i++) { mapper.insert(users.get(i)); if (i % 1000 == 999) { session.commit(); } } session.commit(); // 提交最终批次 // 验证插入结果 Assertions.assertEquals(5000, mapper.count()); } } ``` ### 四、性能优化建议 1. **参数调优**: ```java // JDBC批处理参数优化 dataSource.setPoolMaximumActiveConnections(20); dataSource.setPoolMaximumIdleConnections(10); ``` 2. **内存管理**: ```java // 定期清理缓存 if (i % 1000 == 0) { sqlSession.clearCache(); // 释放内存[^2] } ``` 3. **异常处理**: ```java try { sqlSession.commit(); } catch (PersistenceException ex) { // 处理批处理中的单条失败 if (ex.getCause() instanceof BatchUpdateException) { int[] updateCounts = ((BatchUpdateException)ex.getCause()).getUpdateCounts(); // 定位失败行并重试 } } ``` > **技术要点**: > 1. 无需任何XML配置文件,完全通过Java API配置 > 2. `ExecutorType.BATCH` 用于批量操作优化[^1] > 3. 批处理需配合定期commit()和clearCache()防止OOM[^2] > 4. 测试使用H2内存数据库避免外部依赖
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值