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

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

相关推荐
站大爷IP2 分钟前
被 `@staticmethod` 和 `@classmethod` 坑惨了:继承链里它们真的会“变脸”
后端
orient5 分钟前
CompletableFuture 源码深度解析:6 大 API + 链式编排实战
后端
测试19985 分钟前
2026全新软件测试面试八股文(含答案+文档)
自动化测试·软件测试·python·测试工具·面试·职场和发展·测试用例
周杰伦fans11 分钟前
数列极限的定义
java·开发语言
Zane199415 分钟前
map 比推导式快"是真的吗?一文讲透 map、filter、reduce 与 lambda 的真实性能与设计取舍
后端·python
字节跳动数据库21 分钟前
火山引擎 RDS MySQL 向量索引:把高性能向量检索带到 MySQL 上
人工智能·后端·mysql
小白的后端世界23 分钟前
LangChain 模型初始化参数详解:从基础配置到企业级实践
java·人工智能·langchain
拖孩28 分钟前
用 AI 重解千年观音灵签,做了一个微信小程序,每天摇一摇,命运给你回应
前端·后端·微信小程序
Python私教29 分钟前
AI Agent 可观测性不只是日志:一套可回放的多步执行链
人工智能·后端·python
广州浮点FLOATLIC30 分钟前
Mentor Graphics浮动许可证管理为什么一到流片节点就失控
java·大数据·数据库·许可证管理·cad 许可证优化