最近项目上新上的应用服务使用了sharding jdbc 5.2版本作为分库分表组件,上线后发现fullgc比较频繁。本文主要分析以及解决因sharding导致的full gc问题
问题复现
我模拟了上线后情况写了一个demo,具体git路径如下:github.com/hongyuwen/H...
复现思路:不断触发不同的sql,以便sharding jdbc客户端缓存,导致缓存越来越大,最终fullgc
关键代码如下: CouponInfoMapper新增
java
public interface CouponInfoMapper {
@Insert("insert into coupon_info (coupon_code, rev_uid, country)\n" +
" values (#{couponCode}, #{recUid}, #{country})\n")
int addCouponCode(CouponInfo couponInfo);
@Insert({
"<script>",
"INSERT INTO coupon_info (coupon_code, rev_uid, country)",
"VALUES",
"<foreach collection='list' item='item' separator=','>",
"(#{item.couponCode}, #{item.recUid}, #{item.country})",
"</foreach>",
"</script>"
})
int batchAddCouponCode(@Param("list") List<CouponInfo> couponInfoList);
CouponController增加批量插入rest接口,以便postman调用
java
@RestController
@RequestMapping("/coupon")
public class CouponController {
private static final Logger LOGGER = LoggerFactory.getLogger(CouponController.class);
@Resource
private CouponInfoMapper couponInfoMapper;
@PostMapping(path = "/add")
public void addCouponCode(CouponInfo couponInfo) {
couponInfoMapper.addCouponCode(couponInfo);
}
@PostMapping(path = "batchAdd")
public void batchAddCouponCode(Integer start, Integer end) {
for (int i = start; i < end; i++) {
int ret = couponInfoMapper.batchAddCouponCode(batchGetCoupon(i));
LOGGER.info("batch insert ret={}", ret);
}
}
private List<CouponInfo> batchGetCoupon(int size) {
SecureRandom secureRandom = new SecureRandom();
List<CouponInfo> couponInfoList = new ArrayList<>();
for (int i = 0; i < size; i++) {
CouponInfo couponInfo = new CouponInfo();
int uid = secureRandom.nextInt(1000000000);
couponInfo.setRecUid(new BigInteger(String.valueOf(uid)));
couponInfo.setCouponCode("Rand" + uid);
couponInfo.setCountry("CN");
couponInfoList.add(couponInfo);
}
return couponInfoList;
}
其他不变,在服务器上部署后,postman执行
通过jstat命令可以发现,old区在一直变大
最终到95.85
最终达到99.26后也没减少多少,只减少到了84.88,同时fullgc已经非常频繁
最后打印下堆栈分配情况
问题分析
上述问题导出堆出来使用mat进行分析,分析结果后面再有问题单独说明。这里先说结果,sharding在执行时缓存了sql,具体在SQLStatementParserEngine
中
java
public final class SQLStatementParserEngine {
private final SQLStatementParserExecutor sqlStatementParserExecutor;
private final LoadingCache<String, SQLStatement> sqlStatementCache;
public SQLStatementParserEngine(final String databaseType, final CacheOption sqlStatementCacheOption, final CacheOption parseTreeCacheOption, final boolean isParseComment) {
sqlStatementParserExecutor = new SQLStatementParserExecutor(databaseType, parseTreeCacheOption, isParseComment);
sqlStatementCache = SQLStatementCacheBuilder.build(databaseType, sqlStatementCacheOption, parseTreeCacheOption, isParseComment);
}
/**
* Parse to SQL statement.
*
* @param sql SQL to be parsed
* @param useCache whether use cache
* @return SQL statement
*/
public SQLStatement parse(final String sql, final boolean useCache) {
return useCache ? sqlStatementCache.get(sql) : sqlStatementParserExecutor.parse(sql);
}
}
里面使用了Caffeine缓存
java
public static LoadingCache<String, SQLStatement> build(final String databaseType,
final CacheOption sqlStatementCacheOption, final CacheOption parseTreeCacheOption, final boolean isParseComment) {
return Caffeine.newBuilder().softValues().initialCapacity(sqlStatementCacheOption.getInitialCapacity()).maximumSize(sqlStatementCacheOption.getMaximumSize())
.build(new SQLStatementCacheLoader(databaseType, parseTreeCacheOption, isParseComment));
}
翻看源码,可以发现默认的配置比较大,具体在DefaultSQLParserRuleConfigurationBuilder
sqlstate缓存最大在65535,如果有一些大的sql,尤其是批量插入的,很容易导致本地存储的很大
java
public final class DefaultSQLParserRuleConfigurationBuilder implements DefaultGlobalRuleConfigurationBuilder<SQLParserRuleConfiguration, SQLParserRuleBuilder> {
public static final CacheOption PARSE_TREE_CACHE_OPTION = new CacheOption(128, 1024L);
public static final CacheOption SQL_STATEMENT_CACHE_OPTION = new CacheOption(2000, 65535L);
@Override
public SQLParserRuleConfiguration build() {
return new SQLParserRuleConfiguration(false, PARSE_TREE_CACHE_OPTION, SQL_STATEMENT_CACHE_OPTION);
}
@Override
public int getOrder() {
return SQLParserOrder.ORDER;
}
@Override
public Class<SQLParserRuleBuilder> getTypeClass() {
return SQLParserRuleBuilder.class;
}
}
解决思路
解决方案有两个
方案一:自定义SQL本地缓存,将配置改小一些
可以参考官网yaml配置:shardingsphere.apache.org/document/5....
yaml
rules:
- !SQL_PARSER
sqlCommentParseEnabled: true
sqlStatementCache:
initialCapacity: 64
maximumSize: 256
parseTreeCache:
initialCapacity: 128
maximumSize: 1024
第一种方案在重新执行结果如下: postman执行
最终堆运行情况
方案二:将因参数个数不同而导致的拼成 Sql 的不一致改成固定SQL
比如批量插入SQL,拆成固定集合插入的方式,比如可以改成 1 2 4 8 14 32 64 128 256 把一个大的插入SQL改成多个小批量SQL来解决
这样做能解决sql缓存的问题,但是会增加数据库执行次数
可以综合上述两种方案,按照实际的业务场景选择上述两种或者一种来解决
方案二参考这篇博客:blog.csdn.net/m0_73311735...
我在实际项目中选择的是方案一,改动简单些