资讯动态

SpringBoot整合ClickHouse实战:MyBatis批量插入与分页查询

发布时间:2026/10/15 20:32:15 来源:尧图企业网站定制
1. 为什么要在SpringBoot里接ClickHouse1.1 从一次数据膨胀说起前两年接手一个日志分析类的内部系统最开始用的是MySQL单表存埋点数据。日增两三百万行的时候还能扛等到日增过千万、单表逼近两亿行一个带时间范围的聚合查询直接跑到超时加索引也只是把“慢”变成了“稍微没那么慢”。后来把明细数据迁到ClickHouse同样的聚合查询从十几秒降到几百毫秒这个差距是数量级上的不是调优能弥补的。ClickHouse是列式存储的OLAP数据库它的强项就是海量数据的写入和聚合分析。但它的生态和传统关系型数据库不太一样很多团队用惯了MyBatis那一套切过来会有点不适应。这篇就聊清楚一件事在SpringBoot项目里怎么用MyBatis把ClickHouse的插入和查询跑通并且跑得稳。适合谁看如果你手上有个SpringBoot项目数据量开始让MySQL吃力想引入ClickHouse做分析库但又不想把现有的MyBatis技术栈整个换掉那这篇就是给你写的。我会把依赖、配置、实体映射、批量插入、分页查询这些环节都拆开讲包括我踩过的坑。1.2 ClickHouse和MySQL的本质差异在动手之前得先明白这两者的定位不同否则配置的时候容易想当然。MySQL是行式存储一行数据的所有字段连续存在一起适合按行读写、事务处理。ClickHouse是列式存储同一列的数据连续存放做聚合统计时只需要读涉及的列I/O量大幅减少这是它快的根本原因。但这个特性也带来几个必须注意的点不适合频繁单行更新和删除。ClickHouse的更新删除是异步的、代价很高的操作它设计上就是为批量写入和批量查询服务的。不强制主键唯一。它的主键更像是排序索引用来加速范围查询不保证唯一性。事务支持弱。别指望它像MySQL那样做复杂事务。所以用MyBatis接ClickHouse心态要调整把它当成一个“写入用批量、查询用聚合”的分析引擎而不是又一个业务库。理解了这一点后面的配置选择就顺理成章了。2. 环境准备与依赖选型2.1 驱动选择官方JDBC还是HTTP接口ClickHouse对外提供两种主要访问方式HTTP接口默认8123端口和原生TCP协议默认9000端口。对应的JDBC驱动也有两套驱动类型依赖坐标端口特点官方JDBCcom.clickhouse:clickhouse-jdbc8123/9000官方维护支持HTTP和TCP推荐旧版驱动ru.yandex.clickhouse:clickhouse-jdbc8123已停止维护不建议新项目使用我实测下来官方驱动com.clickhouse:clickhouse-jdbc是首选它同时支持HTTP和TCP两种协议配置灵活而且对批量写入的支持更好。旧版ru.yandex那个包已经很久不更新了新项目别碰。在pom.xml里加上dependency groupIdcom.clickhouse/groupId artifactIdclickhouse-jdbc/artifactId version0.6.0/version classifierall/classifier /dependency注意classifier要选all它会把依赖的httpclient等一起打进来避免版本冲突。如果不加可能启动时报找不到某个类的错误。2.2 MyBatis与连接池的搭配MyBatis本身和数据库无关它只负责SQL映射所以接ClickHouse没有额外障碍。用mybatis-spring-boot-starter即可dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency连接池这块HikariCP是SpringBoot默认的性能也够用。但ClickHouse有个特点它的连接建立开销比MySQL大一些尤其是走TCP协议时。所以连接池的minimum-idle别设太小否则频繁建连会拖慢写入。我一般设minimum-idle5、maximum-pool-size20具体看并发量调整。2.3 一个容易忽略的坑驱动类名不同版本的驱动driver-class-name写法不一样。官方0.6.x版本推荐用com.clickhouse.jdbc.ClickHouseDriver而旧版是ru.yandex.clickhouse.ClickHouseDriver。写错了启动就报ClassNotFoundException。这个细节文档里不显眼但新手很容易卡在这。3. SpringBoot整合ClickHouse的完整配置3.1 application.yml的关键参数先看一份我实际在用的配置spring: datasource: clickhouse: driver-class-name: com.clickhouse.jdbc.ClickHouseDriver url: jdbc:clickhouse://127.0.0.1:8123/default?socket_timeout300000connection_timeout30000 username: default password: hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000这里有几个参数值得展开说socket_timeoutsocket读写超时单位毫秒。ClickHouse做大批量聚合时可能跑很久这个值设小了会中途断开。我设成300秒给复杂查询留足空间。connection_timeout建连超时30秒足够。max-lifetime连接最大存活时间设成30分钟。ClickHouse服务端有连接空闲回收客户端这边配合设置避免用到已被服务端关闭的死连接。提示如果走TCP协议URL改成jdbc:clickhouse://127.0.0.1:9000/default。HTTP协议在跨网络、穿代理的场景下更省心TCP协议在批量写入时性能略好。我一般内网用TCP跨机房用HTTP。3.2 多数据源配置让ClickHouse和MySQL共存实际项目里很少只用ClickHouse通常是MySQL存业务数据、ClickHouse存分析数据。这就需要配多数据源。核心思路是给ClickHouse单独建一个DataSource和SqlSessionFactory用Configuration类管理。Configuration MapperScan(basePackages com.example.mapper.ck, sqlSessionFactoryRef ckSqlSessionFactory) public class ClickHouseConfig { Bean(name ckDataSource) ConfigurationProperties(prefix spring.datasource.clickhouse) public DataSource ckDataSource() { return DataSourceBuilder.create().build(); } Bean(name ckSqlSessionFactory) public SqlSessionFactory ckSqlSessionFactory( Qualifier(ckDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/ck/*.xml)); return bean.getObject(); } }关键点在于MapperScan里的sqlSessionFactoryRef它把ClickHouse的Mapper绑定到专属的SqlSessionFactory上这样两套数据源互不干扰。Mapper的包路径也要分开比如MySQL的放mapper.mysqlClickHouse的放mapper.ck。注意多数据源场景下Transactional注解要慎用。ClickHouse不支持跨库事务如果方法上挂了事务注解可能因为数据源路由问题报错。ClickHouse的操作建议不加事务或者用Transactional(propagation Propagation.NOT_SUPPORTED)明确排除。3.3 建表语句的设计考量ClickHouse建表和MySQL思路不同重点在引擎和排序键。一个典型的日志表CREATE TABLE event_log ( event_date Date, event_time DateTime, user_id UInt64, event_type String, page_url String, duration UInt32 ) ENGINE MergeTree() PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, user_id) TTL event_date INTERVAL 90 DAY;几个设计要点ENGINE用MergeTree这是ClickHouse最常用的引擎适合大多数分析场景。PARTITION BY按月分区分区能大幅提升按时间范围查询的效率也方便过期数据清理。ORDER BY是排序键它决定了数据在磁盘上的物理顺序查询条件命中排序键前缀时最快。这里把event_date放前面因为大部分查询都带时间范围。TTL自动过期90天前的数据自动删除省去手动清理的麻烦。字段类型上能用数值类型就别用StringClickHouse对数值列的压缩率和查询效率都更好。UInt64、UInt32这些无符号整数类型在存ID、计数时很合适。4. 实体映射与插入实现4.1 实体类的字段对应ClickHouse的字段类型和Java类型对应关系比MySQL要稍微注意一下ClickHouse类型Java类型说明UInt8/16/32Integer/Long无符号注意别溢出UInt64Long/BigInteger大数值用BigIntegerInt32Integer有符号Float64Double浮点StringString通用字符串DateLocalDate日期DateTimeLocalDateTime日期时间DecimalBigDecimal精确小数实体类示例public class EventLog { private LocalDate eventDate; private LocalDateTime eventTime; private Long userId; private String eventType; private String pageUrl; private Long duration; // getter/setter 省略 }提示ClickHouse的DateTime默认精度到秒如果表里定义的是DateTime64(3)毫秒Java端用LocalDateTime也能接但要注意驱动版本对DateTime64的支持0.6.x版本已经支持得比较好了。4.2 单条插入与批量插入的取舍单条插入在ClickHouse里是性能杀手。因为每次插入都会在磁盘上生成一个新的数据partClickHouse后台再合并这些part。插入太频繁part数量暴涨合并跟不上查询就会变慢甚至报“too many parts”的错误。所以批量插入是必须的。MyBatis里用foreach拼批量insertinsert idbatchInsert INSERT INTO event_log (event_date, event_time, user_id, event_type, page_url, duration) VALUES foreach collectionlist itemitem separator, (#{item.eventDate}, #{item.eventTime}, #{item.userId}, #{item.eventType}, #{item.pageUrl}, #{item.duration}) /foreach /insert对应的Mapper方法void batchInsert(Param(list) ListEventLog list);批量大小怎么定我实测下来每批500到1000条比较合适。太小了插入次数多太大了单条SQL过长解析开销大而且一旦失败整批回滚。可以按每批1000条切分public void saveBatch(ListEventLog list) { int batchSize 1000; for (int i 0; i list.size(); i batchSize) { int end Math.min(i batchSize, list.size()); eventLogMapper.batchInsert(list.subList(i, end)); } }注意ClickHouse官方推荐用INSERT INTO ... VALUES一次带多条或者用JDBC的addBatch/executeBatch。MyBatis的foreach方式本质是拼一条大SQL对几千条以内没问题数据量特别大时建议改用JDBC原生批量接口。4.3 写入性能的几个调优点除了批量还有几个参数能明显影响写入速度max_insert_block_size单次插入的最大行数默认1048576。一般不用改。async_insert异步插入客户端提交后立即返回服务端攒批写入。适合高并发小批量场景但会牺牲一点实时性。写入并发ClickHouse单表并发写入能力有限多个线程同时写同一张表反而可能变慢。建议写入线程数控制在2到4个。我踩过的一个坑一开始用线程池并发写20个线程同时插结果part数量飙升查询直接卡住。后来改成单线程批量写反而稳定又快。ClickHouse的写入模型和MySQL完全不同别用MySQL的并发思维套。5. 查询实现与分页处理5.1 聚合查询的写法ClickHouse的查询语法和标准SQL大体一致但有一些自己的函数。比如按天统计事件量select idcountByDay resultTypecom.example.vo.DayCountVO SELECT toDate(event_time) AS day, count() AS cnt, uniq(user_id) AS userCnt FROM event_log WHERE event_date BETWEEN #{startDate} AND #{endDate} GROUP BY day ORDER BY day /select这里用了两个ClickHouse特色函数count()不带参数比count(*)更高效uniq()做去重计数比count(distinct)快很多代价是极小概率的误差对统计场景完全够用。如果要去重精确用uniqExact()但性能会差一些。选哪个看业务对精度的要求。5.2 分页查询的正确姿势ClickHouse的分页和MySQL不一样。MySQL的LIMIT offset, size在offset很大时会扫描并丢弃前面所有行越翻越慢。ClickHouse同样有这个问题而且更严重。几种分页方案对比方案写法适用场景缺点LIMIT offsetLIMIT 10000, 20小数据量深分页极慢游标分页WHERE id lastId LIMIT 20有序数据不能跳页预聚合物化视图分页固定维度统计灵活性差对于明细列表我推荐游标分页。用排序键的最后一个值作为游标SELECT * FROM event_log WHERE event_date #{date} AND user_id #{lastUserId} ORDER BY user_id LIMIT #{size}这样每次查询都从上次结束的位置继续不扫描已读数据翻到第几页都是同样的速度。如果业务必须支持跳页那就限制最大页数比如只允许翻到前100页再往后引导用户用筛选条件缩小范围。这是很多分析类产品的通用做法。5.3 结果映射的注意事项ClickHouse返回的字段名默认是大小写敏感的而且别名要用AS明确指定。MyBatis的resultType映射时如果字段名和Java属性名不一致要么在SQL里用别名对齐要么配resultMap。我习惯在SQL里直接对齐比如toDate(event_time) AS eventDate这样resultType自动映射就行省去写resultMap的麻烦。但要注意ClickHouse的别名如果和关键字冲突要用反引号包起来。另外ClickHouse的Nullable字段返回null时Java端如果是基本类型int、long会报错实体类里对应字段要用包装类型Integer、Long。6. 常见问题与排查技巧实录6.1 启动报驱动类找不到最常见的原因是依赖没引对。检查pom.xml里是不是引了com.clickhouse:clickhouse-jdbc并且带了allclassifier。如果同时引了旧版ru.yandex的包可能冲突把旧的排除掉。还有一种情况是版本太老0.4.x之前的版本驱动类名不一样。升级到0.6.x即可。6.2 批量插入报“Too many parts”这是ClickHouse最经典的错误。原因是短时间内插入了太多小批次每个批次生成一个part后台合并速度跟不上。解决办法增大每批的行数至少几百条起。降低写入频率用队列攒批。检查是不是多线程并发写同一张表改成单线程或少量线程。临时缓解可以调大parts_to_throw_insert但这是治标不治本根本还是要控制写入节奏。6.3 查询超时或内存溢出ClickHouse的聚合查询很吃内存尤其是GROUP BY高基数字段时。如果报内存超限几个方向加WHERE条件缩小数据范围尽量命中分区和排序键。用uniq替代count(distinct)。调大max_memory_usage默认单查询10GB但要评估服务器总内存。考虑用物化视图预聚合把计算提前到写入时。6.4 时间字段时区错乱ClickHouse的DateTime默认用服务器时区。如果Java端用LocalDateTime而服务器时区是UTC存进去和读出来可能差8小时。解决办法是在JDBC URL里指定时区比如serverTimezoneAsia/Shanghai或者建表时用DateTime(Asia/Shanghai)明确时区。我一般统一在URL里指定避免每张表都写。6.5 常见问题速查表现象可能原因排查方向ClassNotFoundException驱动依赖缺失或类名错检查pom和driver-class-nameToo many parts小批次高频写入增大批量、降低频率查询超时未命中分区/排序键检查WHERE条件、加分区内存超限高基数GROUP BY用uniq、缩小范围、物化视图时区偏差服务端与客户端时区不一致URL指定serverTimezone字段映射为null别名未对齐或Nullable检查SQL别名、用包装类型7. 一些实操心得配置这块我最大的体会是别把ClickHouse当MySQL用。它的强项是批量写入和聚合查询弱项是单行操作和事务。顺着它的特性设计事半功倍逆着来处处是坑。批量插入的批次大小我建议从500条起步根据实际压测调整。写入线程数控制在个位数别贪多。查询尽量带时间范围让分区和排序键发挥作用。分页优先用游标实在要跳页就限制页数。还有一点ClickHouse的版本迭代很快不同版本的行为可能有差异。生产环境升级前一定要在测试环境把核心的写入和查询链路跑一遍。我遇到过一次小版本升级后某个聚合函数的默认行为变了导致统计结果对不上排查了大半天。最后分享一个监控小技巧定期查system.parts表看每张表的part数量。如果某个表的part数持续增长不下降说明写入节奏有问题该调整批量策略了。SELECT table, count() AS partCount FROM system.parts WHERE active 1 GROUP BY table ORDER BY partCount DESC;这个查询能帮你提前发现写入隐患比等到报错再处理要从容得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑