8.3 连接健康检查

R2DBC 连接池健康监控完整方案:ConnectionPool 内置 metrics(acquired/idle/pending/max)暴露、Spring Boot Actuator ReactiveHealthIndicator 自定义、连接泄漏检测(maxAcquireTime/maxLifeTime)、后台驱逐与故障重试降级策略。

连接池 Metrics

R2DBC 连接池自带 metrics 快照,不需要自己包装 ConnectionFactory:

import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactory;

@Configuration
public class R2dbcMetricsConfig {

    @Bean
    public ConnectionPool connectionPool(ConnectionFactory factory) {
        ConnectionPoolConfiguration config = ConnectionPoolConfiguration.builder(factory)
            .initialSize(5)
            .maxSize(20)
            .maxIdleTime(Duration.ofMinutes(10))
            .build();

        return new ConnectionPool(config);
    }
}

这段代码假设底层 ConnectionFactory 由你手工提供、还没有池化。在标准 Spring Boot 应用里,配置了 spring.r2dbc.url 后 Boot 已经创建了一个 ConnectionPool 并作为 ConnectionFactory Bean 暴露;此时再声明 ConnectionPool Bean,Boot 的池化和非池化自动配置都会因为 @ConditionalOnMissingBean(ConnectionFactory.class) 退让,而你的 Bean 又在等一个 ConnectionFactory,结果是找不到可注入的工厂(或形成循环依赖)。Boot 应用的正确做法是配置 spring.r2dbc.pool.*;需要更细的自定义时用 ConnectionFactoryOptionsBuilderCustomizer(调整连接选项)或 ConnectionFactoryDecorator(在 Boot 建好的工厂外面做装饰),不要手工包第二层池。R2dbcCustomizer 在 Boot 3.5 已不存在,不要照旧版本示例引用它。

ConnectionPool 暴露关键指标:

ConnectionPool pool = ...;

int active = pool.getMetrics().map(m -> m.acquiredSize()).orElse(0);
int idle = pool.getMetrics().map(m -> m.idleSize()).orElse(0);
int pending = pool.getMetrics().map(m -> m.pendingAcquireSize()).orElse(0);
int max = pool.getMetrics().map(m -> m.getMaxAllocatedSize()).orElse(0);

System.out.println("Active: " + active + ", Idle: " + idle +
    ", Pending: " + pending + ", Max: " + max);
指标含义关注点
acquiredSize当前借出的连接数长时间贴着 maxAllocatedSize 说明池不够或连接没归还
idleSize池中空闲连接数长期为 0 且 pending > 0 才是压力信号
pendingAcquireSize等待连接的请求数持续大于 0 说明池满,请求在排队
maxAllocatedSize池允许的最大连接数固定配置,用作分母

自定义健康指标

Spring Boot Actuator 检测到 ConnectionFactory 后,已经通过 ConnectionFactoryHealthContributorAutoConfiguration 自动注册了一个名为 r2dbc 的健康贡献者,它会借一条连接做校验。下面的自定义实现用于覆盖它或补充池指标明细;注意不要让两个 Bean 同时注册成同名健康组件,必要时用 @ConditionalOnMissingBean 控制,或者只定制自动配置提供的 details。

通过 Spring Boot Actuator 暴露连接池健康状态:

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import io.r2dbc.pool.ConnectionPool;

@Component
public class R2dbcHealthIndicator implements ReactiveHealthIndicator {

    private final ConnectionPool pool;

    public R2dbcHealthIndicator(ConnectionPool pool) {
        this.pool = pool;
    }

    @Override
    public Mono<Health> health() {
        return Mono.usingWhen(
                Mono.from(pool.create()),
                conn -> Mono.just(conn.isOpen()),
                conn -> Mono.from(conn.close()))
            .map(valid -> {
                if (valid) {
                    int idle = pool.getMetrics().map(m -> m.idleSize()).orElse(0);
                    int pending = pool.getMetrics().map(m -> m.pendingAcquireSize()).orElse(0);
                    return Health.up()
                        .withDetail("pool.idle", idle)
                        .withDetail("pool.pending", pending)
                        .build();
                }
                return Health.down().withDetail("error", "Connection not open").build();
            })
            .onErrorResume(err ->
                Mono.just(Health.down()
                    .withDetail("error", err.getMessage())
                    .build()));
    }
}

访问 GET /actuator/health:

{
  "status": "UP",
  "components": {
    "r2dbc": {
      "status": "UP",
      "details": {
        "pool.idle": 3,
        "pool.pending": 0
      }
    }
  }
}

连接泄漏检测

连接借了没还(如异常导致 conn.close() 未被订阅)会造成泄漏。连接池没有专门的泄漏检测开关,只能靠超时和存活时间把问题变成可观测的失败:

ConnectionPoolConfiguration.builder(factory)
    .maxSize(20)
    .maxAcquireTime(Duration.ofSeconds(5))   // 获取连接最多等 5 秒
    .maxCreateConnectionTime(Duration.ofSeconds(3))  // 创建连接最多 3 秒
    .maxLifeTime(Duration.ofMinutes(30))     // 连接最大存活时间
    .maxIdleTime(Duration.ofMinutes(10))     // 空闲超时驱逐
    .validationQuery("SELECT 1")             // 取连接时校验可用性
    .build();

池满后,后续请求等待超过 maxAcquireTime 会抛 io.r2dbc.spi.R2dbcTimeoutException(不是 PoolAcquireTimeoutException,那个类不在 r2dbc-pool 里),而不是无限挂起。maxLifeTime 和 maxIdleTime 只能回收连接,不能证明它曾被泄漏;要定位泄漏,得在借出/归还处加日志或用 usingWhen 统一管理生命周期。

自动重连与故障恢复

数据库临时故障(网络抖动、重启)时,应用应自动恢复:

@Bean
public ConnectionPool resilientPool(ConnectionFactory factory) {
    return new ConnectionPool(ConnectionPoolConfiguration.builder(factory)
        .maxSize(20)
        .initialSize(5)
        .maxAcquireTime(Duration.ofSeconds(10))
        .backgroundEvictionInterval(Duration.ofMinutes(1))  // 后台执行空闲/过期驱逐
        .validationQuery("SELECT 1")
        .build());
}

backgroundEvictionInterval 只调度空闲和过期连接的驱逐,不做健康探测;判断连接是否真的可用要靠 validationQuery 或 validationDepth。如果启用 validationQuery,每次取连接都会先跑一次轻量查询,代价要算进延迟。

在应用层添加重试:

@Service
public class OrderService {
    public Mono<Order> findById(Long id) {
        return orderRepo.findById(id)
            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))
                .filter(err -> err instanceof R2dbcException));
    }
}

注意:重试读操作安全,重试写操作需要幂等性保障(第 2.3 节)。

故障降级

数据库彻底不可用时,应用不能无限挂起。配置降级策略:

@Service
public class OrderService {
    private final OrderRepository repo;
    private final Cache<String, Order> cache;  // Caffeine Async(第 9 章)

    public Mono<Order> findById(Long id) {
        return repo.findById(id)
            .retryWhen(Retry.max(2)
                .filter(err -> err instanceof R2dbcTimeoutException))
            .onErrorResume(err -> {
                // 从缓存读取(可能不是最新数据)
                Order cached = cache.getIfPresent(String.valueOf(id));
                if (cached != null) {
                    return Mono.just(cached);
                }
                return Mono.error(new ServiceUnavailableException("Database unavailable"));
            });
    }
}

降级层级:

  1. 重试:网络 transient 错误,2–3 次重试可能恢复
  2. 缓存:返回缓存数据(可能 stale,但可用)
  3. 错误:完全不可用,返回 503,让调用方处理

连接池调优参数速查

ConnectionPoolConfiguration.builder(factory)
    .initialSize(5)                    // 启动时创建 5 个连接
    .maxSize(20)                       // 最大 20 个连接
    .maxIdleTime(Duration.ofMinutes(10))     // 空闲 10 分钟驱逐
    .maxLifeTime(Duration.ofMinutes(30))     // 最长存活 30 分钟
    .maxAcquireTime(Duration.ofSeconds(5))   // 获取连接超时 5 秒
    .maxCreateConnectionTime(Duration.ofSeconds(3)) // 新建连接超时 3 秒
    .backgroundEvictionInterval(Duration.ofMinutes(1)) // 后台驱逐间隔
    .validationQuery("SELECT 1")       // 取连接时校验
    .maxValidationTime(Duration.ofSeconds(1))  // 校验超时
    .registerJmx(true)                 // 暴露 JMX metrics
    .build();

eventd v2 连接池监控

eventd v2 连接池配置:

spring:
  r2dbc:
    pool:
      initial-size: 5
      max-size: 20
      max-idle-time: 10m
      max-life-time: 30m
      max-acquire-time: 5s

配合 Prometheus + Grafana 监控时,不需要自己写 MeterBinder:Spring Boot Actuator 的 ConnectionPoolMetricsAutoConfiguration 在检测到 io.r2dbc.pool.ConnectionPool 后会自动注册 r2dbc.pool.acquired、r2dbc.pool.allocated、r2dbc.pool.idle、r2dbc.pool.pending、r2dbc.pool.max.allocated 和 r2dbc.pool.max.pending 这组 Gauge(Prometheus 下是 r2dbc_pool_*)。getMetrics() 的用途是自定义健康端点里取明细、或接 JMX,不是把指标再接一遍到 Micrometer。

小结

能力方案注意
池指标Boot 自动注册 r2dbc.pool.*;getMetrics() 取明细自定义端点/JMX 时才直接读快照
健康检查ReactiveHealthIndicatorpool.create() 验证连通性
泄漏兜底maxAcquireTime + maxLifeTime超时失败,暴露问题而非无限挂起
可用性校验validationQuery / validationDepth驱逐本身不做健康探测
自动恢复重试 + 缓存降级读操作安全,写需幂等
故障降级缓存 fallbackstale 数据优于无数据

第 8 章结束。下一章进入缓存:9.1 多数据源与读写分离 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南