记录因Sharding Jdbc批量操作引发的一次fullGC

周五晚上告警群突然收到了一条告警消息,点开一看,应用 fullGC 了 。

记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
于是赶紧联系运维下载堆内存快照,进行分析 。
内存分析使用 MemoryAnalyzer 打开堆文件
mat 下载地址:https://archive.eclipse.org/mat/1.8/rcp/MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip
下载下来后需要调大一下 MemoryAnalyzer.ini 配置文件里的-Xmx2048m
【记录因Sharding Jdbc批量操作引发的一次fullGC】打开堆文件后如图:
记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
发现有 809MB 的一个占用 , 应该问题就出在这块了 。然后点击 Dominator Tree , 看看有什么大的对象占用 。
记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
我们找大的对象,一级级往下点看看具体是谁在占用内存 。点到下面发现是 sharding jdbc 里面的类,然后再继续往下发现了一个 localCache 。
记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
原来是一个本地缓存占了这么大的空间
为什么有这个 LocalCache 呢?带着这个疑惑我们去代码里看看它是怎么使用的,根据堆内存分析上的提示 , 我直接打开了 SQLStatementParserEngine 类 。
public final class SQLStatementParserEngine {private final SQLStatementParserExecutor sqlStatementParserExecutor;private final LoadingCache<String, SQLStatement> sqlStatementCache;public SQLStatementParserEngine(String databaseType, SQLParserRule sqlParserRule) {this.sqlStatementParserExecutor = new SQLStatementParserExecutor(databaseType, sqlParserRule);this.sqlStatementCache = SQLStatementCacheBuilder.build(sqlParserRule, databaseType);}public SQLStatement parse(String sql, boolean useCache) {return useCache ? (SQLStatement)this.sqlStatementCache.getUnchecked(sql) : this.sqlStatementParserExecutor.parse(sql);}}他这个里面有个 LoadingCache 类型的 sqlStatementCache 对象,这个就是我们要找的缓存对象 。
从 parse 方法可以看出 , 它这里是想用本地缓存做一个优化,优化通过 sql 解析 SQLStatement 的速度 。
在普通的场景使用应该是没问题的,但是如果是进行批量操作场景的话就会有问题 。
就像下面这个语句:
@Mapperpublic interface OrderMapper {Integer batchInsertOrder(List<Order> orders);}<insert id="batchInsertOrder" parameterType="com.mmc.sharding.bean.Order" >insert into t_order (id,code,amt,user_id,create_time)values<foreach collection="list" item="item" separator=",">(#{item.id},#{item.code},#{item.amt},#{item.userId},#{item.createTime})</foreach></insert>1)我传入的 orders 的个数不一样,会拼出很多不同的 sql,生成不同的 SQLStatement,都会被放入到缓存中
2)因为批量操作的拼接,sql 本身长度也很大 。如果我传入的 orders 的 size 是 1000,那么这个 sql 就很长,也比普通的 sql 更占用内存 。
综上 , 就会导致大量的内存消耗,如果是请求速度很快的话,就就有可能导致频繁的 FullGC 。
解决方案因为是参数个数不同而导致的拼成 Sql 的不一致 , 所以我们解决参数个数就行了 。
我们可以将传入的参数按我们指定的集合大小来拆分,即不管传入多大的集合,都拆为{300, 200, 100, 50, 25, 10, 5, 2, 1}这里面的个数的集合大小 。如传入 220 大小的集合,就拆为[{200},{10},{10}] , 这样分三次去执行 sql,那么生成的 SQL 缓存数也就只有我们指定的固定数字的个数那么多了,基本不超过 10 个 。
接下来我们实验一下,改造前和改造后的 gc 情况 。
测试代码如下:
@RequestMapping("/batchInsert")public String batchInsert(){for (int j = 0; j < 1000; j++) {List<Order> orderList = new ArrayList<>();int i1 = new Random().nextInt(1000) + 500;for (int i = 0; i < i1; i++) {Order order=new Order();order.setCode("abc"+i);order.setAmt(new BigDecimal(i));order.setUserId(i);order.setCreateTime(new Date());orderList.add(order);}orderMapper.batchInsertOrder(orderList);System.out.println(j);}return "success";}GC 情况如图所示:
记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
cache 里面存有元素:
记录因Sharding Jdbc批量操作引发的一次fullGC

文章插图
修改代码后:
@RequestMapping("/batchInsert")public String batchInsert(){for (int j = 0; j < 1; j++) {List<Order> orderList = new ArrayList<>();int i1 = new Random().nextInt(1000) + 500;for (int i = 0; i < i1; i++) {Order order=new Order();order.setCode("abc"+i);order.setAmt(new BigDecimal(i));order.setUserId(i);order.setCreateTime(new Date());orderList.add(order);}List<List<Order>> shard = ShardingUtils.shard(orderList);shard.stream().forEach(orders->{orderMapper.batchInsertOrder(orders);});System.out.println(j);}return "success";}

推荐阅读