周五晚上告警群突然收到了一条告警消息,点开一看,应用 fullGC 了。

于是赶紧联系运维下载堆内存快照,进行分析。

内存分析

使用 MemoryAnalyzer 打开堆文件

mat 下载地址:https://archive.eclipse.org/mat/1.8/rcp/MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip

下载下来后需要调大一下 MemoryAnalyzer.ini 配置文件里的-Xmx2048m

打开堆文件后如图:

发现有 809MB 的一个占用,应该问题就出在这块了。然后点击 Dominator Tree,看看有什么大的对象占用。

我们找大的对象,一级级往下点看看具体是谁在占用内存。点到下面发现是 sharding jdbc 里面的类,然后再继续往下发现了一个 localCache。

原来是一个本地缓存占了这么大的空间

为什么有这个 LocalCache 呢?

带着这个疑惑我们去代码里看看它是怎么使用的,根据堆内存分析上的提示,我直接打开了 SQLStatementParserEngine 类。

public final class SQLStatementParserEngine {
private final SQLStatementParserExecutor sqlStatementParserExecutor;
private final LoadingCache<String, SQLStatement> sqlStatementCache; public SQLStatementParserEngine(String databaseType, SQLParserRule sqlParserRule) {
this.sqlStatementParserExecutor = new SQLStatementParserExecutor(databaseType, sqlParserRule);
this.sqlStatementCache = SQLStatementCacheBuilder.build(sqlParserRule, databaseType);
} public SQLStatement parse(String sql, boolean useCache) {
return useCache ? (SQLStatement)this.sqlStatementCache.getUnchecked(sql) : this.sqlStatementParserExecutor.parse(sql);
}
}

他这个里面有个 LoadingCache 类型的 sqlStatementCache 对象,这个就是我们要找的缓存对象。

从 parse 方法可以看出,它这里是想用本地缓存做一个优化,优化通过 sql 解析 SQLStatement 的速度。

在普通的场景使用应该是没问题的,但是如果是进行批量操作场景的话就会有问题。

就像下面这个语句:

@Mapper
public interface OrderMapper { Integer batchInsertOrder(List<Order> orders);
}
<insert id="batchInsertOrder" parameterType="com.mmc.sharding.bean.Order" >
insert into t_order (id,code,amt,user_id,create_time)
values
<foreach collection="list" item="item" separator=",">
(#{item.id},#{item.code},#{item.amt},#{item.userId},#{item.createTime})
</foreach>
</insert>

1)我传入的 orders 的个数不一样,会拼出很多不同的 sql,生成不同的 SQLStatement,都会被放入到缓存中

2)因为批量操作的拼接,sql 本身长度也很大。如果我传入的 orders 的 size 是 1000,那么这个 sql 就很长,也比普通的 sql 更占用内存。

综上,就会导致大量的内存消耗,如果是请求速度很快的话,就就有可能导致频繁的 FullGC。

解决方案

因为是参数个数不同而导致的拼成 Sql 的不一致,所以我们解决参数个数就行了。

我们可以将传入的参数按我们指定的集合大小来拆分,即不管传入多大的集合,都拆为{300, 200, 100, 50, 25, 10, 5, 2, 1}这里面的个数的集合大小。如传入 220 大小的集合,就拆为[{200},{10},{10}],这样分三次去执行 sql,那么生成的 SQL 缓存数也就只有我们指定的固定数字的个数那么多了,基本不超过 10 个。

接下来我们实验一下,改造前和改造后的 gc 情况。

测试代码如下:

 @RequestMapping("/batchInsert")
public String batchInsert(){
for (int j = 0; j < 1000; j++) {
List<Order> orderList = new ArrayList<>();
int i1 = new Random().nextInt(1000) + 500;
for (int i = 0; i < i1; i++) {
Order order=new Order();
order.setCode("abc"+i);
order.setAmt(new BigDecimal(i));
order.setUserId(i);
order.setCreateTime(new Date());
orderList.add(order);
}
orderMapper.batchInsertOrder(orderList);
System.out.println(j);
} return "success";
}

GC 情况如图所示:

cache 里面存有元素:

修改代码后:

@RequestMapping("/batchInsert")
public String batchInsert(){
for (int j = 0; j < 1; j++) {
List<Order> orderList = new ArrayList<>();
int i1 = new Random().nextInt(1000) + 500;
for (int i = 0; i < i1; i++) {
Order order=new Order();
order.setCode("abc"+i);
order.setAmt(new BigDecimal(i));
order.setUserId(i);
order.setCreateTime(new Date());
orderList.add(order);
}
List<List<Order>> shard = ShardingUtils.shard(orderList);
shard.stream().forEach(
orders->{
orderMapper.batchInsertOrder(orders);
}
);
System.out.println(j);
} return "success";
}

GC 情况如下:

cache 里面存有元素:

可以看出 GC 次数有减少,本地缓存的条数由 600 多减到了 11 个,如果导出堆内存还能看出至少降低了几百 M 的本地内存占用。

另外,这个 cache 是有大小限制的,如果因为一个 sql 占了 600 多个位置,那么其他的 sql 的缓存就会被清理,导致其他 SQL 性能会受到影响,甚至如果机器本身内存不高,还会因为这个 cache 过大而导致频繁的 Full GC

大家以后在使用 Sharding JDBC 进行批量操作的时候就需要多注意了

另附上拆分为固定大小的数组的工具方法如下:

public class ShardingUtils {

    private static Integer[] nums = new Integer[]{800,500,300, 200, 100, 50, 25, 10, 5, 2, 1};

    public static <T> List<List<T>> shard(final List<T> originData) {
return shard(originData, new ArrayList<>());
} private static <T> List<List<T>> shard(final List<T> originData, List<List<T>> result) {
if (originData.isEmpty()) {
return result;
}
for (int i = 0; i < nums.length; i++) {
if (originData.size() >= nums[i]) {
List<T> ts = originData.subList(0, nums[i]);
result.add(ts);
List<T> ts2 = originData.subList(nums[i], originData.size());
if (ts2.isEmpty()) {
return result;
} else {
return shard(ts2, result);
}
}
}
return result;
}
}

记录因Sharding Jdbc批量操作引发的一次fullGC的更多相关文章

  1. Sharding jdbc 强制路由策略(HintShardingStrategy)使用记录

    背景 随着项目运行时间逐渐增加,数据库中的数据也越来越多,虽然加索引,优化查询,但是数据量太大,还是会影响查询效率,也给数据库增加了负载. 再加上冷数据基本不使用的场景,决定采用分表来处理数据,从而来 ...

  2. Spring boot项目集成Sharding Jdbc

    环境 jdk:1.8 framework: spring boot, sharding jdbc database: MySQL 搭建步骤 在pom 中加入sharding 依赖 <depend ...

  3. sharding jdbc(sphere) 3.1.0 spring boot配置

    sharding jdbc 2.x系列详解参见https://www.cnblogs.com/zhjh256/p/9221634.html. 最近将sharding jdbc的配置从xml切换到了sp ...

  4. Sharding JDBC整合SpringBoot 2.x 和 MyBatis Plus 进行分库分表

    Sharding JDBC整合SpringBoot 2.x 和 MyBatis Plus 进行分库分表 交易所流水表的单表数据量已经过亿,选用Sharding-JDBC进行分库分表.MyBatis-P ...

  5. spring boot:配置shardingsphere(sharding jdbc)使用druid数据源(druid 1.1.23 / sharding-jdbc 4.1.1 / mybatis / spring boot 2.3.3)

    一,为什么要使用druid数据源? 1,druid的优点 Druid是阿里巴巴开发的号称为监控而生的数据库连接池 它的优点包括: 可以监控数据库访问性能 SQL执行日志 SQL防火墙 但spring ...

  6. Spring学习记录(十四)---JDBC基本操作

    先看一些定义: 在Spring JDBC模块中,所有的类可以被分到四个单独的包:1.core即核心包,它包含了JDBC的核心功能.此包内有很多重要的类,包括:JdbcTemplate类.SimpleJ ...

  7. Spring JDBC批量操作

    以下示例将演示如何使用spring jdbc进行批量更新.我们将在单次批次操作中更新student表中的记录. student表的结果如下 - CREATE TABLE student( id INT ...

  8. 浅谈sharding jdbc

    定位为轻量级Java框架,在Java的JDBC层提供的额外服务. 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架. ...

  9. Sharding JDBC案例实战

    基础分库 以下实例基于shardingsphere 4.1.0 + SpringBoot 2.2.5.RELEASE版本 依赖导入: <properties> <project.bu ...

随机推荐

  1. Linux-shell笔记1

    一次执行很多命令,可以用:分割每个命令,依次运行所有命令.但是不是进程列表,要用()包围命令才是进程列表.它们有什么差别呢?进程列表是启动了一个子SHELL来执行的.用echo $BASH_SUBSH ...

  2. CCF NOI Online 2021 提高组 T2 积木小赛 (子序列自动机+后缀自动机,O(n^2))

    题面 Alice 和 Bob 最近热衷于玩一个游戏--积木小赛. Alice 和 Bob 初始时各有 n 块积木从左至右排成一排,每块积木都被标上了一个英文小写字母. Alice 可以从自己的积木中丢 ...

  3. 2-2 selenium IDE自动化实战

    Selenium IDE 自动化实战 任务1: 自动在百度搜索"我要自学网" 然后在搜索结果页面点击进入自学网主页 任务2 实现自学网自动登录个人账号 Test2017 12345 ...

  4. 第六篇:vue.js模板语法(,属性,指令,参数)

    Vue.js 的核心是一个允许你采用简洁的模板语法来声明式的将数据渲染进 DOM 的系统. 结合响应系统,在应用状态改变时, Vue 能够智能地计算出重新渲染组件的最小代价并应用到 DOM 操作上.( ...

  5. 函数索引引用的函数必须是immutable类型

    用户在使用中,可能会用到基于函数的索引,但是函数是非 immutable 类型的,导致函数索引无法创建.如: test=# create index ind_t1 on t1(to_char(crea ...

  6. KingbaseES 函数稳定性与SQL性能

    背景:客户现场的一次艰苦的调优过程(https://www.cnblogs.com/kingbase/p/16015834.html),让我觉得非常有必要让数据库用户了解函数的不同稳定性属性,及其对于 ...

  7. AOP实现系统告警

    工作群里的消息怕过于安静,又怕过于频繁 一.业务背景 在开发的过程中会遇到各种各样的开发问题,服务器宕机.网络抖动.代码本身的bug等等.针对代码的bug,我们可以提前预支,通过发送告警信息来警示我们 ...

  8. Python数据科学手册-Numpy数组的计算:比较、掩码和布尔逻辑,花哨的索引

    Numpy的通用函数可以用来替代循环, 快速实现数组的逐元素的 运算 同样,使用其他通用函数实现数组的逐元素的 比较 < > 这些运算结果 是一个布尔数据类型的数组. 有6种标准的比较操作 ...

  9. 第六章:Django 综合篇 - 13:发送邮件

    在Python中已经内置了一个smtp邮件发送模块,Django在此基础上进行了简单地封装,让我们在Django环境中可以更方便更灵活的发送邮件. 所有的功能都在django.core.mail中. ...

  10. 使用kubeoperator自带的nginx-ingress-controller设置服务的ingress规则进行访问

    情况说明 当使用kubeoperator安装k8s集群的时候,在组件设置部分选择的ingress 类型是nginx-ingress yaml文件 k8s集群安装后,可以在节点的master主机的这个目 ...