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

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

相关推荐
用户9083246027321 分钟前
Spring Boot 缓存架构:一行配置切换 Caffeine 与 Redis,透明支持多租户隔离
后端
tyung34 分钟前
zhenyi-base 开源 | Go 高性能基础库:TCP 77万 QPS,无锁队列 16ns/op
后端·go
子兮曰41 分钟前
Humanizer-zh 实战:把 AI 初稿改成“能发布”的技术文章
前端·javascript·后端
桦说编程41 分钟前
你的函数什么颜色?—— 深入理解异步编程的本质问题(上)
后端·性能优化·编程语言
百度地图汽车版1 小时前
【AI地图 Tech说】第九期:让智能体拥有记忆——打造千人千面的小度想想
前端·后端
臣妾没空2 小时前
Elpis 全栈框架:从构建到发布的完整实践总结
前端·后端
H5开发新纪元2 小时前
Nginx 部署 Vue3 项目完整指南
前端·javascript·面试
喷火龙8号2 小时前
单 Token 认证方案的进阶优化:透明刷新机制
后端·架构
孟沐2 小时前
Java异常处理知识点整理(大白话版)
后端
ServBay2 小时前
告别面条代码,PSL 5.0 重构 PHP 性能与安全天花板
后端·php