Java项目中Clickhouse大数据量插入
文章目录
- Java项目中Clickhouse大数据量插入
- 背景
- 代码实现
- 导入依赖
- 定义Clickhouse配置信息
- 封装Clickhouse客户端
- 实现Clickhouse批量插入
- 总结
背景
最近,因为项目中使用到了Clickhouse数据库,本着MySQL数据库的思维,直接使用了Java + MP的实现方式,将大批量数据插入到Clickhouse中。由于MP的批量插入接口默认只支持1000的数据量,于是理所当然地将这批数据分页后再插入,便心安理得的交差了。
可是,后面项目测试后发现,只是单处理100w的数据就需要1h+的时间。一开始怀疑是程序内存不够导致频繁GC,所以就想着直接加大内存。后来内存加大后,性能并没有得到质的提升。
接着开始debug,这时才发现项目的瓶颈其实都在数据落库上。可是Clickhouse是专门应对大数据场景的数据库啊,不可能这么慢啊。于是开始查找Clickhouse相关大数据插入的资料,发现Clickhouse对大数据的导入性能支持得很好,于是便思考着能不能在项目上也应用上。最后终于在官方issue上找到最佳导入实践,通过测试后发现现在导入100w的数据直接缩短为了5m,这可是10倍性能的提升啊!!!话不多说,开始我们的实践吧。
代码实现
导入依赖
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-client</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
- 由于需要先将数据生成附件,再通过附件导入Clickhouse。所以这里直接使用easyexcel将数据生成csv文件;
- 其他两个依赖主要是针对Clickhouse提供的jdbc客户端。
定义Clickhouse配置信息
import lombok.Data;
import java.io.Serializable;
/**
* <p>
* ClickHouse 连接信息配置
* </p>
*/
@Data
public class ClickHouseNodeProperties implements Serializable {
/**
* ClickHouse 连接信息 <br/>
* eg: http://192.168.1.1:8123/test
*/
private String uri;
/**
* ClickHouse 用户名
*/
private String username = "";
/**
* ClickHouse 密码
*/
private String password = "";
/**
* Maximum query execution time in seconds, 0 means no limit.
*/
private Integer maxExecutionTime = 0;
/**
* Socket timeout in seconds.
*/
private Integer socketTimeout = 3600;
/**
* Session timeout in seconds. 0 or negative number means same as server default.
*/
private Integer sessionTimeout = 3600;
/**
* Connection timeout in seconds. It's also used for waiting a connection being closed.
*/
private Integer connectionTimeout = 120;
}
封装Clickhouse客户端
import cn.hutool.core.util.StrUtil;
import com.clickhouse.client.ClickHouseCredentials;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.config.ClickHouseClientOption;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* <p>
* ClickHouse连接信息配置
* </p>
*/
@Slf4j
@ConditionalOnProperty(prefix = "clickhouse", name = "uri")
public class ClickHouseNodeConfig {
private static final String CLICK_HOUSE_NODE_PROPERTIES_BEAN = "clickHouseNodeProperties";
@Bean(name = CLICK_HOUSE_NODE_PROPERTIES_BEAN)
@ConfigurationProperties(prefix = "clickhouse")
public ClickHouseNodeProperties clickHouseNodeProperties() {
return new ClickHouseNodeProperties();
}
/**
* ClickHouse 连接信息
*
* @return xxlJobExecutor
*/
@Bean
@ConditionalOnMissingBean
public ClickHouseNode clickHouseNode(@Qualifier(CLICK_HOUSE_NODE_PROPERTIES_BEAN) ClickHouseNodeProperties clickHouseNodeProperties) {
log.info(">>>>>>>>>>> clickHouseNode config init.");
ClickHouseNode.Builder builder = ClickHouseNode.builder(ClickHouseNode.of(clickHouseNodeProperties.getUri()))
.addOption(ClickHouseClientOption.MAX_EXECUTION_TIME.getKey(), String.valueOf(clickHouseNodeProperties.getMaxExecutionTime()))
.addOption(ClickHouseClientOption.SOCKET_TIMEOUT.getKey(), String.valueOf(clickHouseNodeProperties.getSocketTimeout() * 1000))
.addOption(ClickHouseClientOption.SESSION_TIMEOUT.getKey(), String.valueOf(clickHouseNodeProperties.getSessionTimeout()))
.addOption(ClickHouseClientOption.CONNECTION_TIMEOUT.getKey(), String.valueOf(clickHouseNodeProperties.getConnectionTimeout()));
// 认证信息处理
if (StrUtil.isNotBlank(clickHouseNodeProperties.getUsername()) && StrUtil.isNotBlank(clickHouseNodeProperties.getPassword())) {
ClickHouseCredentials credentials = ClickHouseCredentials.fromUserAndPassword(clickHouseNodeProperties.getUsername(), clickHouseNodeProperties.getPassword());
builder.credentials(credentials);
}
return builder.build();
}
}
实现Clickhouse批量插入
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.excel.EasyExcel;
import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.data.ClickHouseCompression;
import com.clickhouse.data.ClickHouseFile;
import com.clickhouse.data.ClickHouseFormat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import javax.annotation.Resource;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* <p>
* ClickHouse操作业务接口
* </p>
*/
@Slf4j
@ConditionalOnProperty(prefix = "clickhouse", name = "uri")
public class ClickHouseService {
@Resource
private ClickHouseNode clickHouseNode;
@Resource
private CommonProperties commonProperties;
/**
* 将数据导入 ClickHouse(自定义字段插入顺序)
*
* @param table 数据导入目标表
* @param dataList 待导入数据集
* @param fieldOrderList 字段导入顺序
*/
public void batchImport(String table, List dataList, List<String> fieldOrderList) throws Exception {
Objects.requireNonNull(dataList, "dataList");
// 排序字段为空时处理
if (CollectionUtil.isEmpty(fieldOrderList)) {
batchImport(table, dataList);
return;
}
// 对列表字段进行排序
List<List<Object>> orderDataList = new ArrayList<>(dataList.size());
for (Object data : dataList) {
List<Object> orderData = new ArrayList<>(fieldOrderList.size());
orderDataList.add(orderData);
// 对字段排序
Class<?> dataClass = data.getClass();
for (String fieldName : fieldOrderList) {
Field field = ClassUtil.getDeclaredField(dataClass, fieldName);
Object fieldValue = null;
if (Objects.nonNull(field)) {
field.setAccessible(true);
fieldValue = field.get(data);
}
orderData.add(fieldValue);
}
}
// 数据导入
batchImport(table, orderDataList);
}
/**
* 将数据导入 ClickHouse(默认按类中字段的定义顺序导入数据)
*
* @param table 数据导入目标表
* @param dataList 待导入数据集
*/
public void batchImport(String table, List dataList) throws Exception {
Objects.requireNonNull(table, "table");
Objects.requireNonNull(dataList, "dataList");
// 1. 创建临时文件
String fileName = table + "_" + System.currentTimeMillis() + "_" + RandomUtil.randomString(5);
File file = File.createTempFile(fileName, ".csv");
try {
// 2. 使用EasyExcel将数据写入临时文件
log.info("------------------>> 数据写入临时文件开始, filePath: {}", file.getAbsolutePath());
EasyExcel.write(file)
.sheet()
.doWrite(dataList);
log.info("------------------>> 数据写入临时文件成功, filePath: {}", file.getAbsolutePath());
// 3. 将文件导入ClickHouse
batchImport(table, file);
} finally {
try {
// 4. 删除临时文件
file.delete();
} catch (Exception e) {
log.error("删除临时文件失败 --> ", e);
}
}
}
/**
* 将csv文件导入 ClickHouse
*
* @param table 数据导入目标表
* @param csvFile 待导入数据集csv文件
*/
public void batchImport(String table, File csvFile) throws Exception {
batchImport(table, csvFile, ClickHouseFormat.CSV);
}
/**
* 将指定文件导入 ClickHouse
*
* @param table 数据导入目标表
* @param file 待导入数据文件
* @param format 待导入数据文件格式
*/
public void batchImport(String table, File file, ClickHouseFormat format) throws Exception {
Objects.requireNonNull(table, "table");
Objects.requireNonNull(file, "file");
// 将文件导入ClickHouse
ClickHouseClient.load(
clickHouseNode,
table,
// will parse file name later so that you don't have to specify compression and format
ClickHouseFile.of(file, ClickHouseCompression.NONE, 0, format))
.get();
log.info("------------------>> clickHouse import data success.");
}
}
总结
以上就是Java项目中Clickhouse大数据插入的实践,本文仅仅简单介绍了Clickhouse的csv形式导入,具体可以根据实际项目需要,封装更多方式的数据导入方法。Clickhouse本身是一款优秀的列式存储数据库,数据批量插入的效率提升后,使用Clickhouse进行后续的数据分析都将不再是问题啦。。。。