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

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

相关推荐
码熔burning4 分钟前
JVM 面试精选 20 题(续)
jvm·面试·职场和发展
刘一说5 分钟前
CentOS 系统 Java 开发测试环境搭建手册
java·linux·运维·服务器·centos
Victor35610 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor35610 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学11 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bingbingyihao2 小时前
多数据源 Demo
java·springboot
在努力的前端小白7 小时前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
bobz9659 小时前
小语言模型是真正的未来
后端
一叶飘零_sweeeet9 小时前
从繁琐到优雅:Java Lambda 表达式全解析与实战指南
java·lambda·java8
DevYK9 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent