Mysql 数据同步 ClickHouse 亿级数据10分钟内搞定
1、安装ClickHouse
docker run -d --name my-clickhouse \ -p 8123:8123 \ -p 9000:9000 \ -p 9009:9009 \ -v clickhouse_data:/var/lib/clickhouse \ -v clickhouse_log:/var/log/clickhouse-server \ -v clickhouse_config:/etc/clickhouse-server \ --ulimit nofile=262144:262144 \ -e CLICKHOUSE_USER=ckuser \ -e CLICKHOUSE_PASSWORD="ClickHouse123!" \ -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \ --restart unless-stopped \ clickhouse/clickhouse-server:latest
安装完成之后 :
1、浏览器访问http://localhost:8123/play
2、点击右上角Connection setting
3、输入用户名ckuser及密码ClickHouse123!
【就能执行sql了】
2、建表
CREATE TABLE company_info ( id Int64 COMMENT '企业ID', it_code String COMMENT '标识', company_name String COMMENT '公司名称', e_name String COMMENT '英文名称', prev_name String COMMENT '曾用名', group_name String COMMENT '集团名称', credit_code String COMMENT '统一社会信用代码', org_number String COMMENT '组织机构代码', reg_number String COMMENT '工商注册号', taxpayer_id String COMMENT '纳税人识别号', legal_person_name String COMMENT '法定代表人', reg_capital Decimal(20,2) COMMENT '注册资本', reg_capital_unit String COMMENT '注册资本单位', reg_capital_str String COMMENT '注册资本', registrar_code String COMMENT '登记机关代码', estiblish_time DateTime COMMENT '成立日期', issue_date DateTime COMMENT '核准日期', business_start String COMMENT '营业期限', organization String COMMENT '组织形式', company_org_type String COMMENT '公司类型', industry_mc_code String COMMENT '行业门类代码', industry_mc String COMMENT '行业门类', industry_sc_code String COMMENT '行业大类代码', industry_sc String COMMENT '行业大类', industry_tc_code String COMMENT '行业中类代码', industry_tc String COMMENT '行业中类', industry_fc_code String COMMENT '行业小类代码', industry_fc String COMMENT '行业小类', ind_category String COMMENT '所属类别', ind_sub_category String COMMENT '所属子类', reg_location String COMMENT '注册地址', province String COMMENT '省份', province_code String COMMENT '省份代码', city String COMMENT '城市', city_code String COMMENT '城市代码', region String COMMENT '区县', region_code String COMMENT '区县编码', reg_authority String COMMENT '登记机关', business_scope String COMMENT '经营范围', company_scale Int8 COMMENT '企业类型', status String COMMENT '经营状态', status_code String COMMENT '经营状态代码', website String COMMENT '公司官网', bank_top_name String, bank_type String COMMENT '金融机构类型', bank_type2 String COMMENT '银行细分类型', postal_code String COMMENT '邮政编码', contact_phone_number String COMMENT '联系电话', other_phone_number String COMMENT '其它联系电话', contact_address String COMMENT '联系地址', e_mail String COMMENT '电子邮箱', other_mail String COMMENT '其它邮箱', participate_ssn String COMMENT '参加社保人数', currency_code String COMMENT '货币代码', create_time DateTime COMMENT '创建时间', update_time DateTime COMMENT '更新时间', del_flag Int8 COMMENT '是否删除', remark String COMMENT '说明', lat Float64 COMMENT '维度', lng Float64 COMMENT '经度', location String COMMENT '位置信息', type_flag UInt64 COMMENT '分类编码(位掩码:最多64位)', _version UInt64 MATERIALIZED toUInt64(now()) ) ENGINE = ReplacingMergeTree(_version) PARTITION BY tuple() ORDER BY (id) PRIMARY KEY (id) COMMENT '企业信息表';
3、同步数据
依赖
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.clickhouse</groupId> <artifactId>clickhouse-jdbc</artifactId> <version>0.9.8</version> <classifier>all</classifier> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency>
同步代码
package com.faster.admin.bigData; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigDecimal; import java.sql.*; @SpringBootTest public class Mysql2ClickHouseTest { private static HikariDataSource mysqlDs; private static HikariDataSource chDs; private static final int BATCH_SIZE = 8000; // 表独立断点文件 private static final String BREAKPOINT_COMPANY = "breakpoint_company.txt"; // ========== 配置 ========== private static final String MYSQL_URL = "jdbc:mysql://192.168.10.124:3306/arfdata?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull"; private static final String MYSQL_USER = "root"; private static final String MYSQL_PWD = "root"; private static final String CK_URL = "jdbc:clickhouse:http://192.168.21.197:8123/default?use_time_zone=Asia/Shanghai"; private static final String CK_USER = "ckuser"; private static final String CK_PWD = "ClickHouse123!"; public static void initDataSource() { HikariConfig mysqlCfg = new HikariConfig(); mysqlCfg.setJdbcUrl(MYSQL_URL); mysqlCfg.setUsername(MYSQL_USER); mysqlCfg.setPassword(MYSQL_PWD); mysqlCfg.setMaximumPoolSize(4); mysqlDs = new HikariDataSource(mysqlCfg); HikariConfig chCfg = new HikariConfig(); chCfg.setJdbcUrl(CK_URL); chCfg.setUsername(CK_USER); chCfg.setPassword(CK_PWD); chCfg.setDriverClassName("com.clickhouse.jdbc.ClickHouseDriver"); chCfg.setMaximumPoolSize(2); chDs = new HikariDataSource(chCfg); } public static long readBreakpoint(String filePath) { File f = new File(filePath); if (!f.exists()) return 0L; try (FileInputStream fis = new FileInputStream(f)) { byte[] buf = new byte[64]; int len = fis.read(buf); if (len <= 0) return 0L; return Long.parseLong(new String(buf, 0, len).trim()); } catch (Exception e) { System.err.println("读取断点失败 file:" + filePath + ",从0开始"); return 0L; } } public static void saveBreakpoint(String filePath, long maxId) { try (FileOutputStream fos = new FileOutputStream(filePath)) { fos.write(String.valueOf(maxId).getBytes()); } catch (IOException e) { System.err.println("保存断点失败 file=" + filePath + " maxId=" + maxId + " err=" + e.getMessage()); } } /** * company_info * CK _version MATERIALIZED,插入不传 */ public static void syncArfCompanyInfo(long startId) throws SQLException { String mysqlSelect = "SELECT id,it_code,company_name,e_name,prev_name,group_name,credit_code,org_number,reg_number,taxpayer_id," + "legal_person_name,reg_capital,reg_capital_unit,reg_capital_str,registrar_code,estiblish_time,issue_date,business_start," + "organization,company_org_type,industry_mc_code,industry_mc,industry_sc_code,industry_sc,industry_tc_code,industry_tc," + "industry_fc_code,industry_fc,ind_category,ind_sub_category,reg_location,province,province_code,city,city_code,region," + "region_code,reg_authority,business_scope,company_scale,status,status_code,website,bank_top_name,bank_type,bank_type2," + "postal_code,contact_phone_number,other_phone_number,contact_address,e_mail,other_mail,participate_ssn,currency_code," + "create_time,update_time,del_flag,remark,lat,lng,location,type_flag " + "FROM company_info WHERE id > ? ORDER BY id"; String chInsert = "INSERT INTO company_info(" + "id,it_code,company_name,e_name,prev_name,group_name,credit_code,org_number,reg_number,taxpayer_id," + "legal_person_name,reg_capital,reg_capital_unit,reg_capital_str,registrar_code,estiblish_time,issue_date,business_start," + "organization,company_org_type,industry_mc_code,industry_mc,industry_sc_code,industry_sc,industry_tc_code,industry_tc," + "industry_fc_code,industry_fc,ind_category,ind_sub_category,reg_location,province,province_code,city,city_code,region," + "region_code,reg_authority,business_scope,company_scale,status,status_code,website,bank_top_name,bank_type,bank_type2," + "postal_code,contact_phone_number,other_phone_number,contact_address,e_mail,other_mail,participate_ssn,currency_code," + "create_time,update_time,del_flag,remark,lat,lng,location,type_flag) " + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; try (Connection mysqlConn = mysqlDs.getConnection(); PreparedStatement mysqlPs = mysqlConn.prepareStatement(mysqlSelect, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); Connection chConn = chDs.getConnection(); PreparedStatement chPs = chConn.prepareStatement(chInsert)) { mysqlPs.setFetchSize(Integer.MIN_VALUE); mysqlPs.setLong(1, startId); ResultSet rs = mysqlPs.executeQuery(); long maxProcessId = startId; int count = 0; while (rs.next()) { // 设置所有参数 chPs.setLong(1, rs.getLong("id")); chPs.setString(2, getStringOrDefault(rs.getString("it_code"))); chPs.setString(3, getStringOrDefault(rs.getString("company_name"))); chPs.setString(4, getStringOrDefault(rs.getString("e_name"))); chPs.setString(5, getStringOrDefault(rs.getString("prev_name"))); chPs.setString(6, getStringOrDefault(rs.getString("group_name"))); chPs.setString(7, getStringOrDefault(rs.getString("credit_code"))); chPs.setString(8, getStringOrDefault(rs.getString("org_number"))); chPs.setString(9, getStringOrDefault(rs.getString("reg_number"))); chPs.setString(10, getStringOrDefault(rs.getString("taxpayer_id"))); chPs.setString(11, getStringOrDefault(rs.getString("legal_person_name"))); // BigDecimal 处理 BigDecimal regCapital = rs.getBigDecimal("reg_capital"); chPs.setBigDecimal(12, regCapital == null ? BigDecimal.ZERO : regCapital); chPs.setString(13, getStringOrDefault(rs.getString("reg_capital_unit"))); chPs.setString(14, getStringOrDefault(rs.getString("reg_capital_str"))); chPs.setString(15, getStringOrDefault(rs.getString("registrar_code"))); // 时间字段处理:如果为 null 设置为 2000-01-01 00:00:00 chPs.setTimestamp(16, getTimestampOrDefault(rs.getTimestamp("estiblish_time"))); chPs.setTimestamp(17, getTimestampOrDefault(rs.getTimestamp("issue_date"))); chPs.setString(18, getStringOrDefault(rs.getString("business_start"))); chPs.setString(19, getStringOrDefault(rs.getString("organization"))); chPs.setString(20, getStringOrDefault(rs.getString("company_org_type"))); chPs.setString(21, getStringOrDefault(rs.getString("industry_mc_code"))); chPs.setString(22, getStringOrDefault(rs.getString("industry_mc"))); chPs.setString(23, getStringOrDefault(rs.getString("industry_sc_code"))); chPs.setString(24, getStringOrDefault(rs.getString("industry_sc"))); chPs.setString(25, getStringOrDefault(rs.getString("industry_tc_code"))); chPs.setString(26, getStringOrDefault(rs.getString("industry_tc"))); chPs.setString(27, getStringOrDefault(rs.getString("industry_fc_code"))); chPs.setString(28, getStringOrDefault(rs.getString("industry_fc"))); chPs.setString(29, getStringOrDefault(rs.getString("ind_category"))); chPs.setString(30, getStringOrDefault(rs.getString("ind_sub_category"))); chPs.setString(31, getStringOrDefault(rs.getString("reg_location"))); chPs.setString(32, getStringOrDefault(rs.getString("province"))); chPs.setString(33, getStringOrDefault(rs.getString("province_code"))); chPs.setString(34, getStringOrDefault(rs.getString("city"))); chPs.setString(35, getStringOrDefault(rs.getString("city_code"))); chPs.setString(36, getStringOrDefault(rs.getString("region"))); chPs.setString(37, getStringOrDefault(rs.getString("region_code"))); chPs.setString(38, getStringOrDefault(rs.getString("reg_authority"))); chPs.setString(39, getStringOrDefault(rs.getString("business_scope"))); chPs.setByte(40, getByteOrDefault(rs.getByte("company_scale"))); chPs.setString(41, getStringOrDefault(rs.getString("status"))); chPs.setString(42, getStringOrDefault(rs.getString("status_code"))); chPs.setString(43, getStringOrDefault(rs.getString("website"))); chPs.setString(44, getStringOrDefault(rs.getString("bank_top_name"))); chPs.setString(45, getStringOrDefault(rs.getString("bank_type"))); chPs.setString(46, getStringOrDefault(rs.getString("bank_type2"))); chPs.setString(47, getStringOrDefault(rs.getString("postal_code"))); chPs.setString(48, getStringOrDefault(rs.getString("contact_phone_number"))); chPs.setString(49, getStringOrDefault(rs.getString("other_phone_number"))); chPs.setString(50, getStringOrDefault(rs.getString("contact_address"))); chPs.setString(51, getStringOrDefault(rs.getString("e_mail"))); chPs.setString(52, getStringOrDefault(rs.getString("other_mail"))); chPs.setString(53, getStringOrDefault(rs.getString("participate_ssn"))); chPs.setString(54, getStringOrDefault(rs.getString("currency_code"))); // 时间字段处理 chPs.setTimestamp(55, getTimestampOrDefault(rs.getTimestamp("create_time"))); chPs.setTimestamp(56, getTimestampOrDefault(rs.getTimestamp("update_time"))); chPs.setByte(57, getByteOrDefault(rs.getByte("del_flag"))); chPs.setString(58, getStringOrDefault(rs.getString("remark"))); // 数值字段:如果为 null 设置为 0 chPs.setDouble(59, getDoubleOrDefault(rs.getDouble("lat"), rs.wasNull())); chPs.setDouble(60, getDoubleOrDefault(rs.getDouble("lng"), rs.wasNull())); chPs.setString(61, getStringOrDefault(rs.getString("location"))); // type_flag:如果为 null 设置为 0 long typeFlag = rs.getLong("type_flag"); if (rs.wasNull()) { chPs.setLong(62, 0L); } else { chPs.setLong(62, 0L); } chPs.addBatch(); maxProcessId = rs.getLong("id"); count++; if (count >= BATCH_SIZE) { chPs.executeBatch(); chPs.clearBatch(); saveBreakpoint(BREAKPOINT_COMPANY, maxProcessId); System.out.printf("company_info 已同步 id=%d%n", maxProcessId); count = 0; } } if (count > 0) { chPs.executeBatch(); chPs.clearBatch(); saveBreakpoint(BREAKPOINT_COMPANY, maxProcessId); System.out.printf("company_info 最后一批完成,maxId=%d%n", maxProcessId); } } System.out.println("==== company_info 全量同步完成 ===="); } // 辅助方法:将 null 转为空字符串 private static String getStringOrDefault(String value) { return value == null ? "" : value; } // 辅助方法:将 null 转为 0(double) private static double getDoubleOrDefault(double value, boolean wasNull) { return wasNull ? 0.0 : value; } // 辅助方法:将 null 转为 0(byte) private static byte getByteOrDefault(byte value) { // 注意:rs.getByte() 对于 NULL 返回 0,所以这里直接返回 return value; } // 辅助方法:将 null 时间转为 2000-01-01 00:00:00 private static Timestamp getTimestampOrDefault(Timestamp timestamp) { if (timestamp == null) { // 2000-01-01 00:00:00 return Timestamp.valueOf("2000-01-01 00:00:00"); } return timestamp; } @Test public void testSyncArfCompanyInfo() throws SQLException { initDataSource(); long startId = readBreakpoint(BREAKPOINT_COMPANY); System.out.println("company_info 从断点id=" + startId + "开始同步"); syncArfCompanyInfo(1); mysqlDs.close(); chDs.close(); } }

浙公网安备 33010602011771号