news 2026/10/3 3:27:15

ShardingSphere 5.0首次SQL慢?从根因到启动预热方案全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ShardingSphere 5.0首次SQL慢?从根因到启动预热方案全解析

先说个我踩过的坑。去年给公司一个商品中心做分库分表改造,SpringBoot 集成 ShardingSphere 5.0 启动之后,连上去跑第一条 SQL,好家伙,直接卡了 3 秒多才返回。一开始我以为是数据量太大,后来发现哪怕查一条空表也要这么久。你要是也遇到过这类“启动后首次查询慢”的诡异现象,大概率是 ShardingSphere 和应用连接池在偷懒,没有提前把自己该加载的东西加载完。这篇文章就把我自己的排查过程、根因分析和一套稳妥的预热方案完整写出来,照着做基本能把这个首次查询的耗时打下来。

这个问题的适用范围其实很广,只要是 SpringBoot + ShardingSphere 5.x 的项目,不管你是分库分表、读写分离还是数据加密,首次 SQL 慢都有可能出现。文章适合正在搞 ShardingSphere 落地的后端开发,也适合刚接手分库分表项目、被线上“第一个请求超时”搞懵的运维同学。我会把原理、代码、配置、验证方法都拆开讲清楚,尽量让看完的人能直接抄作业。

1. 启动后首次 SQL 慢,到底慢在哪一步

先说结论:慢不是 ShardingSphere 在 SQL 执行阶段多花了时间,而是它和底层数据库连接池在“第一次被使用”的时候才做了一堆本可以提前做的初始化工作。

我当时排查的方式很简单,先在 application.yml 里把 ShardingSphere 的 SQL 日志打开,然后在启动完成后马上执行一条SELECT COUNT(1) FROM t_order WHERE order_id = 1,看日志里 SQL 的执行耗时分布。结果很有意思,PreparedStatement 执行只花了 30 毫秒左右,但整个接口从进入到返回等了足足 3 秒。这就说明耗时不在数据库本身,而在进入 SQL 执行链路之前的某个环节。

接着我继续往细了查。在接口方法入口、Service 层、Mapper 层、DataSource.getConnection() 这几处分别埋了时间戳。最后发现时间几乎全花在第一次调用DataSource.getConnection()上。这个连接并不是 ShardingSphere 逻辑数据源自己持有物理连接,而是要向下从 HikariCP 连接池拿真实连接。而 HikariCP 在 SpringBoot 项目里默认是懒加载连接,项目启动的时候池子里其实是空的,第一次拿连接才真正触发与 MySQL 建连。再加上 ShardingSphere 5.0 在首次执行 SQL 时还会做路由解析、规则绑定、元数据获取等操作,两者叠加在一起,首次查询的时间自然就被拉得很难看。

这个现象和业务复杂度无关,几乎是一个必现问题。只要你的应用一启动就立刻有流量进来,或者像我们这样启动完马上执行接口自测,一定能感受到那种“卡第一下”的体验。

1.1 连接池的懒初始化机制

这里需要先弄明白 HikariCP 的初始化逻辑。SpringBoot 2.x+ 默认的数据源连接池就是 HikariCP,它的设计目标是极高吞吐和极低延迟,但“启动即建立所有连接”并不是它的默认行为。

HikariCP 在初始化的时候会启动一个 HouseKeeper 后台任务,但这个任务主要是维护连接数量,也就是当连接数低于 minimumIdle 时才补充连接。默认情况下,minimumIdle 的值和 maximumPoolSize 一样,而 maximumPoolSize 默认是 10。听起来像是启动时要建 10 个连接,但实际并不是这样,连接池初始状态是空的,只有第一次 getConnection() 才会同步创建一个连接,然后再慢慢由 HouseKeeper 补到 minimumIdle 数量。

如果你用的是 Java 17 和最新版 SpringBoot 3.x,这个行为也一样,HikariCP 一直保持了“按需创建连接”的策略。只有当配置了initializationFailTimeout这个参数并设置大于 0 时,连接池启动时才会同步尝试建立连接,用来保证数据源是可用的。默认值其实是 1,但它的语义只是尝试获取一个连接来验证数据源可用性,拿完就标记为成功,并不会顺手创建一堆连接放在池子里。

所以,SpringBoot 项目启动完成后,HikariCP 大多数情况下是空池子。第一次请求进来时,连接池要经历“新建 TCP 连接 -> MySQL 认证 -> 设置会话变量 -> 返回连接”这一整套动作。光建一个连接普遍要 200ms 到 500ms,如果数据库和业务应用之间还隔着云网络或者安全组策略,这个时间还会更长。而 ShardingSphere 5.0 下面不只有一个真实数据源,分两个库就是两个连接池,分四个库就是四个连接池,首次查询如果走了全分片路由,那就要把这几个库的连接几乎都建立一遍,耗时自然成倍增长。

1.2 ShardingSphere 内核的懒加载设计

ShardingSphere 5.0 对整个内核做了大幅重构,引入了ShardingSphereDataSource、ShardingSphereRuntimeContext、ShardingSphereMetaData等概念。虽然启动构建 DataSource 时会初始化不少规则配置,但很多内部组件用的是懒加载机制,尤其是 SQL 解析引擎、路由引擎里的部分缓存、分布式序列生成器,都是在第一次真正执行 SQL 时才完成加载。

举一个具体例子,分片策略里的KeyGenerateAlgorithm如果用雪花算法(snowflake),它的工作节点初始化、时间戳起始值处理,某些版本是延迟到第一次生成 ID 时才触发的。再比如分片路由过程中需要根据分片键值计算目标表名,这个计算依赖的ShardingAlgorithm实例,虽然规则解析在启动时就完成了,但算法内部一些需要从外部配置读取的属性映射,也可能在首次使用时才真正解析完。

懒加载本身算不上设计缺陷,它是为了缩短应用启动时间、避免加载用不到的功能而刻意做的取舍。但副作用就是“启动后第一炮”会很难受。尤其是 ShardingSphere 5.0 刚发布那阵子,SQL 解析引擎的初始化比 4.x 版本要重不少,5.0 默认还开启了sql-show的解析日志打印,首次解析一条 SQL 需要构建语法树、生成分析结果并缓存,这些操作本来就是冷启动成本的一部分。

所以你会发现,不管是在 SpringBoot 和 ShardingSphere 5.0 环境做接口压测,还是在 Kotlin、Play Framework 这类 JVM Web 框架里集成同一个内核,首次请求慢的现象都差不多。它本质上是框架初始化时机的问题,不完全是某一个开发框架的责任。

1.3 首次执行还需要触发的组件清单

把槽点拉出来列个清单会更有感知。一次看似简单的SELECT * FROM t_order WHERE order_id = 1,在 ShardingSphere 内部大致要经过以下几步:

  • 将 SQL 解析成抽象语法树,绑定分片规则
  • 根据分片键值计算实际物理表名
  • 多分片条件下生成路由结果,如果没走分片键,会退化成全路由
  • 在逻辑数据源上获取物理连接,这时才真正触发 HikariCP 建连
  • 如果是首次操作某个分片表,还会加载对应的表元数据
  • 将逻辑 SQL 改写成真实库表名对应的物理 SQL
  • 执行结果归并时,首次构建结果集归并引擎

这里面任何一步的冷启动成本都不算高,但串在一起就是肉眼可见的延迟。而预热的核心思路,就是把这些冷启动动作提前到应用启动完成后主动跑一遍,让真正接收流量时所有组件都处于“热”状态。

2. 预热方案选型,先搞清楚自己要预热什么

知道了慢的原因,接下来的问题就是怎么预热。我在网上看到有不少人给出的方案是在项目启动后随便执行一条 SQL,比如写个@PostConstruct方法去查一下某张表。这个思路方向对,但太粗糙了,很容易漏掉关键部分。你必须想清楚,到底要触发哪些数据源、哪些分片表、哪些路由策略,才能真正达到“暖机”效果。

2.1 预热目标拆解:连接池、分片路由、元数据

我把预热的对象拆成了三个层面。

第一层是连接池。所有真实数据源连接池都要提前把连接创建好,至少把minimumIdle对应的连接数建立起来。如果应用分了 4 个库,那这 4 个库的 HikariCP 池子都要激活。

第二层是分片路由。你需要让 ShardingSphere 对核心业务表至少执行一次带分片键的精确查询,比如WHERE order_id = 1,这样它会把分片算法、表路由结果、SQL 改写逻辑都跑一遍。不同分片键的查询最好各来一条,因为不同分片键可能命中不同的分片算法实例。

第三层是元数据。ShardingSphere 5.0 会缓存表的列信息、主键信息等元数据。如果应用涉及加密字段、广播表、绑定表,也建议预热时触达,因为这些会额外增加元数据解析和路由改写逻辑。

如果只预热连接池不预热路由,第一次查询还是会慢在规则解析上;如果只预热某一张分片表,另一张核心分片表第一次查询时还是要经历冷启动。所以一份合格的预热清单,必须从业务实际查询链路出发,把所有核心表、核心分片键、广播表、绑定表都覆盖到。

2.2 不同预热时机的优缺点对比

实现预热的方式很多,常见的有下面这几种。我把它们放在一起对比一下,方便你选型。

预热方式触发时机优点缺点
@PostConstruct初始化方法Bean 初始化完成后代码最简单,直接写在配置类里执行过早,其他 Bean 未必就绪,容易拿到不完整的上下文
ApplicationRunner/CommandLineRunnerSpringBoot 启动完成后上下文完整,适合做启动后置动作,执行顺序可控启动时间会变长,需要控制预热耗时
ApplicationListener 监听事件自定义时机触发灵活,可以监听ApplicationReadyEvent等事件要写监听器,比 Runner 多一层理解成本
Actuator / 健康检查触发外部调用触发不影响启动时长,按需预热依赖外部调用,第一次真实请求可能仍然慢
定时任务兜底应用运行期间可以周期性刷新连接、缓存增加了维护成本,需要处理并发访问冲突

从实践角度看,我推荐“ApplicationRunner 触发预热 + 配置项开关”的组合方案。项目启动后自动执行一遍,一旦发现预热有问题,还可以通过配置开关关掉,不影响应用启动。至于 Actuator 健康检查,可以作为兜底手段,比如 Kubernetes 的存活探针本来就会周期性地访问应用端口,你可以在探针处理逻辑里顺带触发一次查询,这样也无缝完成了预热。

3. 代码实战:基于 ApplicationRunner 的启动预热

下面进入正题,我直接给出一个可落地的预热组件设计。这个方案已经在生产环境跑了大半年,平稳度过了多次发版重启,核心思路就是:应用启动完成后,主动执行一组经过挑选的 SQL,覆盖所有真实数据源和核心路由规则。

3.1 实现一个支持开关和耗时统计的预热执行器

先定义一个配置前缀,用来控制预热开关、预热 SQL 列表、每次预热超时时间。这样不同环境可以自由调整。

spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline key-generate-strategy: column: order_id key-generator-name: snowflake t_order_item: actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..1} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline binding-tables: - t_order,t_order_item broadcast-tables: - t_config sharding-algorithms: table_inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 2} key-generators: snowflake: type: SNOWFLAKE props: sql-show: false

接下来写一个预热配置类,读取配置项,并在 SpringBoot 启动完成后执行预热逻辑。

import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @Component @ConfigurationProperties(prefix = "sharding.preheat") public class ShardingSpherePreheatRunner implements ApplicationRunner { private boolean enabled = true; private List<String> sqls = new ArrayList<>(); private long timeoutSeconds = 30; private final DataSource dataSource; public ShardingSpherePreheatRunner(DataSource dataSource) { this.dataSource = dataSource; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void setSqls(List<String> sqls) { this.sqls = sqls; } public void setTimeoutSeconds(long timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } @Override public void run(ApplicationArguments args) { if (!enabled) { return; } long start = System.currentTimeMillis(); log.info("ShardingSphere preheat start, sqls = {}", sqls.size()); try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { for (String sql : sqls) { long sqlStart = System.currentTimeMillis(); try (ResultSet rs = statement.executeQuery(sql)) { // 只需要触发执行,不需要消费结果 } catch (Exception e) { log.error("preheat sql failed, sql = {}", sql, e); } long cost = System.currentTimeMillis() - sqlStart; log.info("preheat sql cost {} ms, sql = {}", cost, sql); } } catch (Exception e) { log.error("ShardingSphere preheat failed", e); } long totalCost = System.currentTimeMillis() - start; log.info("ShardingSphere preheat finished, total cost {} ms", totalCost); } }

然后写一个自动配置类,或者直接在启动类同包下定义@Configuration,把预热 SQL 列表交给 Spring 管理。

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; import java.util.List; @Configuration public class PreheatConfig { @Bean public List<String> preheatSqls() { return Arrays.asList( "SELECT COUNT(1) FROM t_order WHERE order_id = 1", "SELECT COUNT(1) FROM t_order WHERE order_id = 2", "SELECT COUNT(1) FROM t_order_item WHERE order_id = 1", "SELECT COUNT(1) FROM t_config" ); } }

如果你是用代码方式构建 ShardingSphere 数据源,也可以把dataSource强制转换成ShardingSphereDataSource,然后调用它的内部方法来获取所有真实数据源,并对每个数据源做连接预热。但在 SpringBoot Starter 的自动配置体系中,直接注入 DataSource 就已经是 ShardingSphere 的逻辑数据源了,所以上面的写法最简单,也最不容易出错。

3.2 预热 SQL 应该如何挑选

预热 SQL 不是随便写几条就行,它要尽量贴近真实业务请求。我总结了几条实用的选择原则。

第一条是覆盖核心分片表的精确查询。对每一张分片表,至少要执行一条带分片键的WHERE查询,触发 ShardingSphere 的标准路由引擎。分片键取值最好覆盖不同的分片目标,比如order_id = 1和order_id = 2在一张表分片算法是order_id % 2时,一个命中表后缀 1,一个命中表后缀 0,这样两张物理分表的元数据和路由缓存都能被加载。

第二条是覆盖绑定表和广播表。绑定表之间的关联查询会走绑定路由,如果不预热,第一次关联查询时要额外计算关联关系。广播表是所有分片库都存在的表,一次预热会触发所有数据源连接,这正好把连接池也带起来了。

第三条是尽量使用COUNT(1)而不是SELECT *,减少网络回包数据量。预热的目的不是取数据,而是触发路由、建连、元数据加载这些机制,返回行数越少越好。SELECT COUNT(1)在 MySQL 里走覆盖索引扫描即可,对业务表影响也小。

第 4 条是别把写操作放进预热 SQL 里,特别是INSERT和UPDATE。写操作可能会改变数据状态,如果表里有唯一键约束,还可能因为重复插入导致异常。预热就老老实实用只读 SQL。

上面示例里我把t_config广播表加了进去,一个重要原因就是广播表存在所有分片库里,跑一次SELECT COUNT(1) FROM t_config时,ShardingSphere 会向 ds0 和 ds1 各发一条 SQL,也就同时激活了两个物理数据源的全部连接池。

3.3 配置连接池参数,让预热效果更彻底

单纯执行几条 SQL 还只能建立少量连接,因为一个连接池默认可能只建立一个物理连接。如果业务高峰期需要 20 个连接,而你启动时只建了 1 个,那么后续连接还是要边用边建,依然会有建连延迟。

所以配合预热,我建议主动设置 HikariCP 的参数,让连接池在预热阶段就把连接补到合理水位。minimum-idle可以设置成你期望的常用连接数,maximum-pool-size设置成峰值连接数。这样预热跑完,池子里就已经有 5 个或 10 个空闲连接挂在那边,真实请求来了直接复用。

另外,initialization-fail-timeout这个参数值得用起来。它表示连接池初始化时尝试获取一个连接的超时时间,如果设置大于 0,应用启动时就会同步验证能否正常建连。虽然它只建立一个连接,但至少能让数据库连接在启动阶段就暴露问题,而不是等到第一次请求才报错。

spring: datasource: hikari: minimum-idle: 10 maximum-pool-size: 20 connection-timeout: 30000 initialization-fail-timeout: 1000

需要注意,如果你的 ShardingSphere 配置里每个数据源都单独写了 HikariCP 配置,那么spring.datasource.hikari这层全局配置不一定会覆盖子数据源。每个数据源自己的参数要单独检查。我见过有人在全局配置了maximum-pool-size=50,但分库数据源里继承的还是默认值 10,结果压测时连接池成了瓶颈。这一点在配置多数据源时尤其容易踩坑。

4. 给预热增加并发能力与兜底策略

虽然上面这套 Runner 已经能解决大部分问题,但在真实场景里我还遇到了几个进阶需求:表数量多的时候,一条一条预热太慢;应用启动后如果数据库还没就绪,预热失败会导致整个启动过程异常;以及压测过程中发现光预热一次还不够,高峰期连接池被重新打回低水位后,还是要等建连。

针对这些情况,我做了第二版优化。

4.1 使用多线程并发预热核心分片表

当核心分片表有几十张时,串行执行每一条预热 SQL 可能要好几秒。在启动阶段拖慢应用,业务上是不能接受的。这时可以把预热 SQL 拆成多个任务,用线程池并发执行。

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @Component public class ConcurrentShardingSpherePreheater { private final DataSource dataSource; private final ThreadPoolTaskExecutor preheatExecutor; public ConcurrentShardingSpherePreheater(DataSource dataSource) { this.dataSource = dataSource; ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setThreadNamePrefix("sharding-preheat-"); executor.initialize(); this.preheatExecutor = executor; } public void preheat(List<String> sqls, long timeoutSeconds) throws InterruptedException { CountDownLatch latch = new CountDownLatch(sqls.size()); List<String> failedSqls = new ArrayList<>(); for (String sql : sqls) { preheatExecutor.execute(() -> { long start = System.currentTimeMillis(); try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { statement.executeQuery(sql); } catch (Exception e) { failedSqls.add(sql); } finally { latch.countDown(); } }); } boolean completed = latch.await(timeoutSeconds, TimeUnit.SECONDS); if (!completed) { log.error("preheat timeout, not all sqls finished"); } if (!failedSqls.isEmpty()) { log.error("preheat failed sqls: {}", failedSqls); } } }

并发预热带来的收益很明显,4 张表从 3 秒锐减到 800ms 左右,同时所有表的路由和连接池都得到了预热。但要注意,并发线程数不建议太高,否则启动瞬间数据库会接到一波密集请求,反而把数据库连接数打满,影响其他正常服务。7 到 8 个并发线程是一个相对保守的区间。

这里还有一个细节:因为所有线程共用同一个DataSource.getConnection(),ShardingSphere 会对逻辑数据源的连接获取做锁控制,并发场景下连接池本身会成为瓶颈,所以线程数不是越大越好,要结合 HikariCP 的maximum-pool-size来考虑。

4.2 预热失败不能拖垮应用启动

预热是一种优化手段,不是业务主链路,所以它绝对不能成为应用启动的阻碍。我在代码里已经加了try-catch和CountDownLatch.await的超时控制,就是为了防止预热环节出问题导致整个 SpringBoot 上下文启动失败。

还有人问过我一个场景:如果数据库配置错了,预热失败会不会抛异常导致启动停止?答案是看你把异常怎么处理。ApplicationRunner.run方法抛出异常时,SpringBoot 默认是会终止启动流程的。所以强烈建议在预热方法内部把所有异常都吞掉,起码要打成日志,不要往外抛。等业务请求进来时,如果确实断了连,再走正常的异常处理链路自然会暴露出来。

不过吞异常不意味着完全无视。我习惯把预热失败的 SQL 打 warn 级日志,并附上当前数据源信息,方便启动后去排查。同时通过 Actuator 的 health 端点做联动:如果预热失败,就把健康检查状态置为暂时不健康,让负载均衡器不要把流量打进来。这样既不阻断启动,也不会在系统还没完全准备好时接收大量请求。

4.3 定时兜底:连接回收后的二次预热策略

HikariCP 的空闲连接默认存活时间是 10 分钟,maxLifetime默认 30 分钟。如果你的应用有较长时间没有访问某个分片库,连接池里的空闲连接可能被回收。这时如果突然来一波大流量,还是会有一个短暂的建连过程。

所以我在生产环境里又加了一个兜底机制。通过 Spring 的@Scheduled注解写一个定时任务,每隔 5 分钟对核心分片表执行一次轻量级查询,保持连接池水温和路由缓存的热度。这个定时任务只做预热,不参与业务逻辑,即使失败也只记录日志,不影响主流程。

import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Statement; @Component public class PreheatScheduler { private final DataSource dataSource; public PreheatScheduler(DataSource dataSource) { this.dataSource = dataSource; } @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 60 * 1000) public void keepWarm() { try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { statement.executeQuery("SELECT COUNT(1) FROM t_order WHERE order_id = 1"); } catch (Exception e) { log.warn("keep warm failed", e); } } }

定时预热要注意和业务高峰期错开,不要在整点和大促峰值时间抢连接。我用的是 fixedDelay,表示上一次执行完成后再等固定时间间隔,不会出现任务重叠。

5. 验证预热效果,不能只靠感觉

很多人写完预热代码,跑一次接口觉得快了,就认为大功告成。实际上这样不够严谨。你至少要从耗时数字、连接池状态、ShardingSphere 路由日志三个角度去验证,确保预热真正落到了实处。

5.1 启动后立即执行真实查询,对比预热前后耗时

最直接的验证方法,是在 SpringBoot 启动完成后写一个临时接口或者 CommandLineRunner,让它在预热执行之后立刻执行一条和预热 SQL 等价的业务查询,并输出详细耗时。

我当时的做法是,在 Application 启动类里加了一个CommandLineRunner,第一个打印预热前查询耗时,第二个打印预热后查询耗时。对比如下:

  • 预热前首次查询:3062ms
  • 预热后首次查询:45ms

这个对比非常直观,也让我确认所有耗时都集中在第一次查询的链路初始化上。如果你也想复现这个对比,记得在启动时暂时把预热的enabled配置设为 false,先跑一次拿原始数据,再打开预热跑一次。

如果两次查询耗时差不多,那说明你的慢查询根因可能不在 ShardingSphere 冷启动上,而需要从 SQL 执行计划、数据库索引、网络延迟这些方向再排查。预热不是万能药,它只解决“初始化时机”带来的延迟。

5.2 通过连接池指标确认物理连接已建立

HikariCP 本身自带监控指标,如果你引入了micrometer-registry-prometheus,可以在 Prometheus 里看到hikaricp_connections_active、hikaricp_connections_idle、hikaricp_connections_pending这些指标。

验证时重点关注hikaricp_connections_idle。预热完成后,这个值应当约等于配置的minimum-idle值。如果为 0,说明连接池仍然是空的,预热并没有真正触发建连。这种情况通常是因为预热 SQL 没有走真实的物理数据源,或者 ShardingSphere 的路由结果没有命中任何分片。

举例来说,如果分片表路由条件是order_id % 2,而你的预热 SQL 用了WHERE id = 1,这个id并不是分片键,那 ShardingSphere 只能走全路由,它会把 SQL 广播到所有分片库。虽然这样也能建连,但消耗更大。如果你所在的表路由配置里压根没有匹配任何分片键,SQL 甚至可能报错。所以预热 SQL 的分片键一定要写对。

5.3 打开 ShardingSphere SQL 日志,观察实际下发的物理 SQL

ShardingSphere 5.0 在props里有一个sql-show配置。设为 true 后,SQL 执行时会在日志里打印逻辑 SQL 和实际下发的物理 SQL。这个日志对验证预热效果非常有帮助。

预热SELECT COUNT(1) FROM t_order WHERE order_id = 1时,日志中应当出现类似:

Actual SQL: ds0 ::: SELECT COUNT(1) FROM t_order_1

这表示路由结果正确,SQL 被改写到对应的物理分表。如果日志只输出了Actual SQL: ds0 ds1 ::: SELECT COUNT(1) FROM t_order_0 t_order_1这类广播执行,说明分片键没有生效,走了全路由。虽然全路由也能把连接池热起来,但会让单次查询扫描所有分片表,性能开销大。在预热阶段发现并修正分片键问题,可以提前排除掉很多上线后的隐患。

sql-show 本身有性能损耗,正式环境建议只在排查问题的时候临时打开,排查完马上关掉。

6. 预热过程中的常见坑和排查思路

最后我把实际工作中遇到过的问题整理成一个速查表,都是比较高频的坑,希望能帮你少走弯路。

问题现象可能原因解决方法
预热 SQL 执行时间非常长走了全分片路由,扫描了所有分片表确保 SQL 带准确的分片键,改为精确路由
预热后连接池 idle 仍为 0预热 SQL 没有真正下发到物理库检查路由配置和表名,用 sql-show 日志验证
启动时预热报错,应用启动失败ApplicationRunner 抛出了未捕获异常在预热方法内部捕获所有异常,不要向外抛
预热过程很慢,拖长了启动时间预热 SQL 过多或串行执行用线程池并发预热,控制 SQL 数量和超时时间
启动后第一次插入数据仍然慢只预热了查询,没有预热分布式 ID 生成器增加一次带主键生成的 INSERT 预热(但注意幂等性)
多个分片库中某个库连接池一直空预热 SQL 只覆盖了部分分片键预热 SQL 覆盖不同分片键,保证路由到不同库和表
定时预热和业务查询同时执行,出现连接竞争定时任务没有避开流量高峰调整定时任务执行时间,或者降低预热频率
全局 HikariCP 配置不生效子数据源有自己的配置覆盖了全局配置检查 ShardingSphere 每个数据源里的 HikariCP 参数

第 4 个问题我想单独多说一句。很多人预热只写查询,结果上线后第一次往分片表插入数据还是明显卡顿。原因是分布式主键的生成器没有预加载。解决思路有两个:一是在预热 SQL 里加一条明确指定主键的 INSERT,比如INSERT INTO t_order (order_id, ...) VALUES (999, ...),插入后立刻回滚或删除,只为了触发主键生成逻辑;二是直接调用snowflake算法的generateKey()方法,在启动阶段主动生成一个 ID,让算法内部完成时间戳、workerId 的初始化。第二种更干净,不会污染数据。

定时任务如果担心和业务流量冲突,可以用开关控制,在不同环境选择是否启用。比如灰度测试时可以关闭定时预热,减少对压测的干扰;正式环境则一直开启。

另外,还有个容易被忽略的点:ShardingSphere 5.0 和 5.1、5.2 在配置项细节上有些差异。比如 5.1 之后把key-generators的配置格式做了调整,某些版本要求声明key-generator-name时必须同时写type。如果你用的版本和网上博客不一致,照着配置容易报错。所以我建议以官方文档对应版本的示例为准,不要盲目照抄旧文章。

7. 一些额外的经验与建议

预热这个技巧本身不难,但它反映出的问题是很多中间件框架共通的:懒加载设计可以优化正常启动速度,但会让“首次体验”变差。在接手类似 ShardingSphere 这类偏重的中间件时,我建议你把“启动后预热”直接纳入标准上线流程,而不是等线上反馈慢了才补救。

关于预热 SQL 的选择,我再补充一个技巧:可以优先选择带索引的精确查询,同时把结果集大小控制住。SELECT COUNT(1)是一个很好的选择,但如果你用的数据库是 MySQL 且表特别大,COUNT(1)也可能因为 InnoDB 的统计信息更新而变慢。这时候可以改成SELECT id FROM t_order WHERE order_id = 1 LIMIT 1,只取一行数据,同样能触发分片路由和连接池建连,但数据库成本更低。

我还在线上遇到过一种情况,就是业务有读写分离策略,主库和从库都有独立的数据源。只预热主库 SQL 的话,第一次走从库的查询依然会慢,因为从库连接池是独立的。所以读写分离场景下,预热 SQL 至少要包含一条强制走从库的查询。ShardingSphere 本身可以通过hint或者@Slave注解方式路由到从库,但如果你没有用这类能力,也可以直接为从库的连接池单独执行一次SELECT 1,让连接池热起来。

最后再说一个运维层面的事。预热结束后,最好在日志里打印一个明确的“preheat finished”标志,方便发布系统判断应用是否已真正就绪。之前我们团队在 CI/CD 流水线里,靠这个日志来判断应用是否进入了可服务状态。如果没打这个日志,就认为预热失败,直接报警或者回滚。这对多节点滚动发布尤其有用,可以避免旧节点还没停、新节点还在冷启动时,负载均衡就把流量导过来,造成间歇性超时。

我自己在实际操作中最大的感受是,这类问题一定要用数据说话。你别凭感觉觉得“好像快了一点”,而是要打开日志、打开监控、对比连接池指标,一步一步确认热度到位了,然后再纳入标准发布流程。预热这个动作,看起来只是多加了一条 Runner,但它实际是在替线上流量打前站,把那些所有“第一次”才做的事提前做完,真正收到流量的时候,系统才能给别人一种“我一直都在原地等着”的从容感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:27:09

汽车钣金数字工坊:如何用虚拟仿真重塑沉浸式实训教学

数字工坊这个词&#xff0c;我最早听到的时候其实心里是打了个问号的。干了十来年汽车钣金实训教学&#xff0c;看过的“数字化教改”项目不少&#xff0c;有的真能落地&#xff0c;有的就是挂个名头买个软件回来吃灰。但真把“数字工坊”这个概念和钣金实训揉在一起&#xff0…

作者头像 李华
网站建设 2026/10/3 3:26:26

供应商产品中心取向转型:从订单驱动到产品共创的落地路径

做供应商这行久了&#xff0c;最怕听到的一句话是&#xff0c;“客户下周就要样机&#xff0c;你们加点班”。订单一来&#xff0c;整个公司都围着它转&#xff0c;研发被临时需求打断&#xff0c;生产给插单让路&#xff0c;等这个客户消停了&#xff0c;下一个客户又带着一套…

作者头像 李华
网站建设 2026/10/3 3:25:40

COSCon‘25 Web3.0开源论坛:去中心化生态的工程实践与参与指南

刚看到 COSCon‘25 的 Web3.0 开源论坛议程正式发布时&#xff0c;我第一反应是&#xff1a;今年这个论坛终于把“去中心化生态”从一个营销词&#xff0c;拉回到了可以讨论、可以动手、可以复制的层面。Web3.0 这两年被聊得太多&#xff0c;但真正站在开源社区角度去拆解的场合…

作者头像 李华
网站建设 2026/10/3 3:25:39

基于Hadoop电影推荐系统:从伪分布式搭建到ItemCF算法实现

简介&#xff1a;这份资源是基于Hadoop框架实现的电影推荐系统完整项目源码包&#xff0c;面向具备Java与大数据基础、希望实践分布式推荐算法的开发者与学习者。项目以HDFS与MapReduce为核心&#xff0c;结合Java实现数据收集、清洗、相似度计算与推荐生成等环节&#xff0c;并…

作者头像 李华
网站建设 2026/10/3 3:25:33

Flutter跨平台开发实战:鸿蒙系统快消品库存效期预警可视化工具

在快消品这行干久了&#xff0c;你会发现一个特别扎心的事实&#xff1a;仓库里真正吃掉利润的&#xff0c;不是采购价和物流费&#xff0c;而是那一批批躺在货架深处、包装上落了灰的临期商品。我花了一个月时间&#xff0c;基于Flutter跨平台开发实战做了一套面向鸿蒙系统的快…

作者头像 李华
网站建设 2026/10/3 3:25:18

HBuilderX云打包Vue项目为APK:Vue2+Vant H5转安卓App完整实战

我一年多前接过一个需求&#xff1a;公司内部管理系统是Vue2 Vant 2写的&#xff0c;页面和数据逻辑都跑通了&#xff0c;老板突然说&#xff0c;“把它做成安卓App装到手机上&#xff0c;出去谈客户的时候方便现场演示”。原生开发来不及&#xff0c;重写uni-app成本又太高&a…

作者头像 李华