Sharding Jdbc批量操作引發(fā)fullGC解決
正文
周五晚上告警群突然收到了一條告警消息,點(diǎn)開一看,應(yīng)用 fullGC 了。

于是趕緊聯(lián)系運(yùn)維下載堆內(nèi)存快照,進(jìn)行分析。
內(nèi)存分析
使用 MemoryAnalyzer 打開堆文件
mat 下載地址:http://chabaoo.cn/zt/matlab.html
下載下來(lái)后需要調(diào)大一下 MemoryAnalyzer.ini 配置文件里的-Xmx2048m
打開堆文件后如圖:

發(fā)現(xiàn)有 809MB 的一個(gè)占用,應(yīng)該問題就出在這塊了。然后點(diǎn)擊 Dominator Tree,看看有什么大的對(duì)象占用。

我們找大的對(duì)象,一級(jí)級(jí)往下點(diǎn)看看具體是誰(shuí)在占用內(nèi)存。點(diǎn)到下面發(fā)現(xiàn)是 sharding jdbc 里面的類,然后再繼續(xù)往下發(fā)現(xiàn)了一個(gè) localCache。

原來(lái)是一個(gè)本地緩存占了這么大的空間
為什么有這個(gè) LocalCache 呢?
帶著這個(gè)疑惑我們?nèi)ゴa里看看它是怎么使用的,根據(jù)堆內(nèi)存分析上的提示,我直接打開了 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);
}
}
他這個(gè)里面有個(gè) LoadingCache 類型的 sqlStatementCache 對(duì)象,這個(gè)就是我們要找的緩存對(duì)象。
從 parse 方法可以看出,它這里是想用本地緩存做一個(gè)優(yōu)化,優(yōu)化通過 sql 解析 SQLStatement 的速度。
在普通的場(chǎng)景使用應(yīng)該是沒問題的,但是如果是進(jìn)行批量操作場(chǎng)景的話就會(huì)有問題。
就像下面這個(gè)語(yǔ)句:
@Mapper
public 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 的個(gè)數(shù)不一樣,會(huì)拼出很多不同的 sql,生成不同的 SQLStatement,都會(huì)被放入到緩存中
2)因?yàn)榕坎僮鞯钠唇?,sql 本身長(zhǎng)度也很大。如果我傳入的 orders 的 size 是 1000,那么這個(gè) sql 就很長(zhǎng),也比普通的 sql 更占用內(nèi)存。
綜上,就會(huì)導(dǎo)致大量的內(nèi)存消耗,如果是請(qǐng)求速度很快的話,就就有可能導(dǎo)致頻繁的 FullGC。
解決方案
因?yàn)槭菂?shù)個(gè)數(shù)不同而導(dǎo)致的拼成 Sql 的不一致,所以我們解決參數(shù)個(gè)數(shù)就行了。
我們可以將傳入的參數(shù)按我們指定的集合大小來(lái)拆分,即不管傳入多大的集合,都拆為{300, 200, 100, 50, 25, 10, 5, 2, 1}這里面的個(gè)數(shù)的集合大小。如傳入 220 大小的集合,就拆為[{200},{10},{10}],這樣分三次去執(zhí)行 sql,那么生成的 SQL 緩存數(shù)也就只有我們指定的固定數(shù)字的個(gè)數(shù)那么多了,基本不超過 10 個(gè)。
接下來(lái)我們實(shí)驗(yàn)一下,改造前和改造后的 gc 情況。
測(cè)試代碼如下:
@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 情況如圖所示:

cache 里面存有元素:

修改代碼后:
@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";
}
GC 情況如下:

cache 里面存有元素:

可以看出 GC 次數(shù)有減少,本地緩存的條數(shù)由 600 多減到了 11 個(gè),如果導(dǎo)出堆內(nèi)存還能看出至少降低了幾百 M 的本地內(nèi)存占用。
另外,這個(gè) cache 是有大小限制的,如果因?yàn)橐粋€(gè) sql 占了 600 多個(gè)位置,那么其他的 sql 的緩存就會(huì)被清理,導(dǎo)致其他 SQL 性能會(huì)受到影響,甚至如果機(jī)器本身內(nèi)存不高,還會(huì)因?yàn)檫@個(gè) cache 過大而導(dǎo)致頻繁的 Full GC
大家以后在使用 Sharding JDBC 進(jìn)行批量操作的時(shí)候就需要多注意了
另附上拆分為固定大小的數(shù)組的工具方法如下:
public class ShardingUtils {
private static Integer[] nums = new Integer[]{800,500,300, 200, 100, 50, 25, 10, 5, 2, 1};
public static <T> List<List<T>> shard(final List<T> originData) {
return shard(originData, new ArrayList<>());
}
private static <T> List<List<T>> shard(final List<T> originData, List<List<T>> result) {
if (originData.isEmpty()) {
return result;
}
for (int i = 0; i < nums.length; i++) {
if (originData.size() >= nums[i]) {
List<T> ts = originData.subList(0, nums[i]);
result.add(ts);
List<T> ts2 = originData.subList(nums[i], originData.size());
if (ts2.isEmpty()) {
return result;
} else {
return shard(ts2, result);
}
}
}
return result;
}
}
以上就是Sharding Jdbc批量操作引發(fā)fullGC解決的詳細(xì)內(nèi)容,更多關(guān)于Sharding Jdbc引發(fā)fullGC的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一分鐘入門Java Spring Boot徹底解決SSM配置問題
Spring Boot是由Pivotal團(tuán)隊(duì)提供的全新框架,其設(shè)計(jì)目的是用來(lái)簡(jiǎn)化新Spring應(yīng)用的初始搭建以及開發(fā)過程。該框架使用了特定的方式來(lái)進(jìn)行配置,從而使開發(fā)人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力于在蓬勃發(fā)展的快速應(yīng)用開發(fā)領(lǐng)域成為領(lǐng)導(dǎo)者2021-10-10
springboot jpa實(shí)現(xiàn)優(yōu)雅處理isDelete的默認(rèn)值
如果多個(gè)實(shí)體類都有 isDelete 字段,并且你希望在插入時(shí)為它們統(tǒng)一設(shè)置默認(rèn)值時(shí)改怎么做呢,本文為大家整理了一些方法,希望對(duì)大家有所幫助2024-11-11
Java負(fù)載均衡策略的實(shí)現(xiàn)詳解
這篇文章主要介紹了Java負(fù)載均衡策略的實(shí)現(xiàn),負(fù)載均衡在Java領(lǐng)域中有著廣泛深入的應(yīng)用,不管是大名鼎鼎的nginx,還是微服務(wù)治理組件如dubbo,feign等,負(fù)載均衡的算法在其中都有著實(shí)際的使用,需要的朋友可以參考下2022-07-07
JAVA格式化時(shí)間日期的簡(jiǎn)單實(shí)例
這篇文章主要介紹了JAVA格式化時(shí)間日期的簡(jiǎn)單實(shí)例,有需要的朋友可以參考一下2013-11-11
Java實(shí)戰(zhàn)之客戶信息管理系統(tǒng)
這篇文章主要介紹了Java實(shí)戰(zhàn)之客戶信息管理系統(tǒng),文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04
Java發(fā)送帶html標(biāo)簽內(nèi)容的郵件實(shí)例代碼
下面小編就為大家?guī)?lái)一篇Java發(fā)送帶html標(biāo)簽內(nèi)容的郵件實(shí)例代碼。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-11-11

