記錄因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";}

推薦閱讀