Troubleshooting系列-shardingjdbc频繁fullgc问题分析及解决

最近项目上新上的应用服务使用了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...

我在实际项目中选择的是方案一,改动简单些

相关推荐
子兮曰8 小时前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
小羊没烦恼!8 小时前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
子兮曰8 小时前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
俊昭喜喜里8 小时前
java中的继承和多态的区别
java
小羊没烦恼!8 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
爱勇宝9 小时前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
譕痕9 小时前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码9 小时前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖9 小时前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时9 小时前
01-06-A-JVM排查实战详解
java·jvm