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...

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

相关推荐
垚垚学技术_聚焦云原生2 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide7 分钟前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
Java成神之路-10 分钟前
深入理解三大高频队列:ArrayDeque、LinkedList、PriorityQueue 方法、特性与场景选择
java
不一样的少年_10 分钟前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
roman_日积跬步-终至千里19 分钟前
【资源控制】自助查询的智能路由
java·大数据·数据库
水深火乐29 分钟前
安全地生成验证码、密码和 Token
后端
小强198841 分钟前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐43 分钟前
interface和any
后端
步行cgn44 分钟前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端