线程池+策略模式+反射实现动态批量操作clickhouse
- 目录
- 前言
- 安装ck
- 整体设计
- 策略模式设计
- 线程池管理ck
- 码上有戏
- 反射核心
- 测试
- 简单说明
- 源码地址
目录
前言
之前实际项目中遇到过几千万的数据库的单表查询,并且字段多达数百个。即使命中索引仍然很慢,由于数据基本无更新操作,后来研究一番,将该表数据存储到ck中使得查询速度提升了很多倍。
当然,ck本身是一个用于OLAP的列式数据库管理系统,它是按列进行存储,相当于每一个列都是一个索引,并且它的优势是适用于 大数据量级 的单张大宽表的 聚合查询分析
安装ck
安装clickhouse需要安装docker,可自行百度安装docker
下面是简单通过挂载方式安装ck
1 创建挂载目录
mkdir -p /data/clickhouse
2 拉取镜像
docker pull yandex/clickhouse-server
docker pull yandex/clickhouse-client
3 启动
docker run -d --name ck-server --ulimit nofile=262144:262144 -p 9000:9000 -p 8123:8123 --volume=/data/clickhouse:/var/lib/clickhouse yandex/clickhouse-server
其中 – name 指定名称 --volume 后接挂载的目录:容器映射目录
这里只需要默认的配置就好,即用户名为default,密码为空
通过 docker ps -a 查看容器是否正常启动
4 客户端通过DBeaver可视化连接ck(可自行百度安装)
5 创建测试表如api_leesin并初始化一些数据
DROP TABLE IF EXISTS `api_leesin`;
CREATE TABLE `api_leesin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`api_test1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`api_test2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
6 数据从mysql导入ck
这里为了简单和测试直接自动创建表,通过mysql函数初始化
CREATE TABLE ck数据表
ENGINE = MergeTree
ORDER BY id AS
select * from mysql('ip:3306', 'mysql数据库名称', 'mysql表名', 'mysql用户', 'mysql密码');
如下面例子
CREATE TABLE api_leesin
ENGINE = MergeTree
ORDER BY id AS
select * from mysql('ip:3306', 'test', 'api_leesin', 'root', '123456');
结果如下图所示,并同步了数据
注意,为了测试方便和实体相对应,这里手动改api_test1为apiTest1,api_test2为apiTest2
整体设计
此处demo的需求是通过配置文件实现调用mysql处理还是ck处理表数据策略,通过线程池管理并提供工具类操作ck
策略模式设计
以上是简化策略模式uml图,事实上我们需要在系统初始化的时候,通过参数配置去自动注入是ck策略还是mysql策略
1 参数配置初始化
@ConfigurationProperties(
prefix = "system.param"
)
@Configuration
public class SystemParamConfig {
private SystemParamConfig.DataStorageType dataStorageType;
public SystemParamConfig() {
this.dataStorageType = DataStorageType.mysql;
}
public DataStorageType getDataStorageType() {
return dataStorageType;
}
public void setDataStorageType(SystemParamConfig.DataStorageType dataStorageType) {
this.dataStorageType = dataStorageType;
}
public static enum DataStorageType {
mysql,
clickhouse;
private DataStorageType() {
}
}
}
默认策略是mysql,可在yaml中配置策略
system:
param:
data-storage-type: mysql
2 注入bean
@Configuration
public class StrategyManagerConfig {
private final SystemParamConfig paramConfig;
private final IApiLeesinService apiLeesinService;
@Bean
@ConditionalOnMissingBean({DataStrategyService.class})
public DataStrategyService dataStrategyService() {
SystemParamConfig.DataStorageType dataStorageType = this.paramConfig.getDataStorageType();
Object dataStrategyService;
switch (dataStorageType) {
case mysql:
dataStrategyService = new MysqlDataStrategyServiceImpl(apiLeesinService);
break;
case clickhouse:
dataStrategyService = new CkDataStrategyServiceImpl();
break;
default:
throw new IllegalStateException("Unexpected value: " + dataStorageType);
}
return (DataStrategyService) dataStrategyService;
}
public StrategyManagerConfig(final SystemParamConfig paramConfig, final IApiLeesinService apiLeesinService) {
this.paramConfig = paramConfig;
this.apiLeesinService = apiLeesinService;
}
}
这里有两个核心,第一个是通过switch (dataStorageType) 去取出策略配置,从而生成不同策略的对象,
第二个就是注入bean,通过final 属性+构造器去初始化bean,而其中AbstractDataStrategyService作为顶层策略抽象,主要用来扩展。最后通过操作DataStrategyService接口去使用默认的策略
线程池管理ck
我们知道使用线程池可以减少在创建和销毁线程上所花的时间以及系统资源的开销,而这里线程池使用org.apache.commons.commons-pool2
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
1 待管理的对象ClickHouse
这里为了方便,只贴出重要方法
public class ClickHouse {
private static Connection conn;
private static String clickhouseAddress = "jdbc:clickhouse://ip:8123";
private static String clickhouseUsername = "default";
private static String clickhousePassword = "";
private static String clickhouseDB = "default";
private static Integer clickhouseSocketTimeout = 600000;
public ClickHouse() {
ClickHouseProperties properties = new ClickHouseProperties();
properties.setDatabase(clickhouseDB);
properties.setUser(clickhouseUsername);
properties.setPassword(clickhousePassword);
properties.setSocketTimeout(clickhouseSocketTimeout);
ClickHouseDataSource clickHouseDataSource = new ClickHouseDataSource(clickhouseAddress, properties);
try {
conn = clickHouseDataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
public List<Map<String, String>> exeSql(String sql) {
}
public void insertBatch(List<?> datas, String tableName, Class cls) {
}
public void updateBatch(List<?> datas, String tableName) {
}
public void close() throws SQLException {
conn.close();
}
}
2 创建工厂ClickHouseFactory
public class ClickHouseFactory extends BasePooledObjectFactory<ClickHouse> {
@Override
public ClickHouse create() throws Exception {
return new ClickHouse();
}
@Override
public PooledObject<ClickHouse> wrap(ClickHouse arg0) {
return new DefaultPooledObject<ClickHouse>(arg0);
}
@Override
public void destroyObject(PooledObject<ClickHouse> p) throws Exception {
p.getObject().close();
super.destroyObject(p);
}
}
3 工具类ClickHouseUtil
public class ClickHouseUtil {
private static GenericObjectPool<ClickHouse> pool;
static {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(10);
config.setMaxIdle(10);
config.setMinIdle(0);
config.setMaxWaitMillis(-1);
config.setBlockWhenExhausted(true);
config.setTestOnBorrow(false);
config.setTestOnReturn(false);
config.setTestWhileIdle(true);
config.setMinEvictableIdleTimeMillis(10 * 60000L);
config.setTestWhileIdle(true);
pool = new GenericObjectPool<ClickHouse>(new ClickHouseFactory(), config);
}
public static List<Map<String, String>> exeSql(String sql) {
ClickHouse ck = null;
try {
ck = pool.borrowObject();
return ck.exeSql(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.returnObject(ck);
}
return new ArrayList<>();
}
public static void insertBatch(List<?> datas, String tableName, Class cls) {
ClickHouse ck = null;
try {
ck = pool.borrowObject();
ck.insertBatch(datas, tableName, cls);
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.returnObject(ck);
}
}
public static void updateBatch(List<?> datas, String tableName) {
ClickHouse ck = null;
try {
ck = pool.borrowObject();
ck.updateBatch(datas, tableName);
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.returnObject(ck);
}
}
}
最后我们只要调用ClickHouseUtil去调用clickhouse相应功能,不过值得一提的是很多时候可以通过封装去实现util里的方式,从而达到复用(如void insertBatch(List<?> datas, String tableName, Class cls))
码上有戏
如图,为项目代码结构
反射核心
如果ck里表的字段特别多,如果模块里的类特别多的话。如果写死的api去实现操作,很显然不合理。本着能动态实现就动态实现的原则,可以先抽象出工具类的接口api,再在clickhouse具体去实现
1 抽象出批量插入api
void insertBatch(List<?> datas, String tableName, Class cls)
其中 datas表示任意类型的对象的集合数据,tableName为ck中待操作的表,cls为插入的类,显然有了这三个条件就可以简单实现批量操作了
2 反射拼接占位符
// 拼接插入占位符sql
String sqlBegin = "insert into " + tableName + "(";
Field[] fields = cls.getDeclaredFields();
String params = "";
String quesion = "?";
for (int i = 0; i < fields.length; i++) {
if (i == 0) {
params = fields[0].getName();
quesion = "?";
} else {
params = Joiner.on(",").join(params, fields[i].getName());
quesion = Joiner.on(",").join(quesion, "?");
}
}
String result = Joiner.on("").join(sqlBegin, params, ") VALUES(", quesion, ")");
最后得到的效果即使insert into table(A,B,C) VALUES(?,?,?),最后我们只需要反射获取对应属性的值,去填充这些占位符即可
3 反射取值填充占位符
for (int i = 0; i < datas.size(); i++) {
// 反射获取相应字段的值
Object object = datas.get(i);
Class<?> targetCls = object.getClass();
Field[] objectField = targetCls.getDeclaredFields();
for (int j = 0; j < objectField.length; j++) {
Method m = targetCls.getMethod("get" + getMethodName(objectField[j].getName()));
Object value = m.invoke(object);
value = ensureSafe(value, objectField[j]);
statement.setObject(j + 1, value);
}
statement.addBatch();
}
这其中会有两个注意的地方,需要将每个属性名称首字母转为大写,保证get方法取到值,另一方面,在获取值后,需要保证每个值是合法的,即value = ensureSafe(value, objectField[j]),因为实际中由于空值null或其他值都会导入插值失败,最后批量提交
测试
通过配置切换ck或mysql策略,可分别测试数据获取与插入
浏览器输入 http://localhost:8080/apiLeesin/listTest(数据获取)
结果显示
[ApiLeesin(id=1, apiTest1=11, apiTest2=12), ApiLeesin(id=2, apiTest1=21, apiTest2=22)]
数据插入 http://localhost:8080/apiLeesin/saveTest
简单说明
当然这里只是一个demo,实际中我们可以扩展许多功能,比如可以扩展其它的策略如es,redis等,同时可以将系统中抽象的功能如token的存储等封装成jar,通过配置去加载策略等等
源码地址