本来主要针对批量插入sql导致线上服务内存溢出事故的分析复盘,并给出一些关于批量插入的建议。
事件过程
- 晚上,收到了告警,容器实例异常退出,几分钟后容器实例回复正常了。
- 第二天好奇就去看了一下日志,看到了熟悉的outOfMemoryError,系统是保留了那个时刻的堆栈文件。非常顺利下载到当时的文件hprof文件,
- 分析hprof文件有一个简单实用的技巧,idea本身就可以,当然你也可以选择mat等工具。
- 打开后,非常直观的就可以看到问题,有一个1.02G文件大小的char数组。
然后我们打开这个,看一下这个数组存在的什么内容。会发现大量的都是这种sql插入。进而我们知道是什么业务代码。
- 代码文件大致是
for(int i=0;i<n;i++){
insertSql(是一个批量插入的sql,批次大小是100,数据量本身就比较大) 没有开启事务
}
问题分析
- 框架使用mybatis,数据库连接池来进行管理,我们要知道mybatis是如何工作的。执行sql。mybatis是有一个预编译的过程,然后将其放到内存里面,这样当存在大量的这种编译后的语句,占用了大量内存
解决方案:数据库连接池有一个参数设置可以设置缓存的预编译语句最大数量。
- 数据库连接池有一个设置,
- pool-prepared-statements: true:
这个配置项用于启用连接池的预编译语句缓存功能。当设置为 true 时,连接池会缓存预编译的 SQL 语句(PreparedStatement)。预编译语句可以提高 SQL 执行的性能,因为数据库只需要对 SQL 语句进行一次解析和优化,后续相同结构的 SQL 语句执行时可以直接复用之前的解析结果。这在多次执行相同结构的 SQL 语句(如插入多条数据、查询操作等)时非常有用,能减少数据库的处理开销,提升应用程序的整体性能。
- max-pool-prepared-statement-per-connection-size: 20:
这个配置项指定了每个数据库连接可以缓存的预编译语句的最大数量。在连接池中的每个连接都会有自己的预编译语句缓存,此参数设置了该缓存的上限为 20 条。如果应用程序尝试缓存超过这个数量的预编译语句,那么连接池可能会按照一定的策略(如 LRU - 最近最少使用)移除一些旧的预编译语句,为新的语句腾出空间。这样做可以避免单个连接的预编译语句缓存占用过多的内存资源,从而保证系统的稳定性和资源的合理利用。
总体来说,这两个配置项结合使用,可以在一定程度上优化数据库操作的性能并合理管理资源。 具体的效果还会受到应用程序中 SQL 语句使用模式的影响。