SpringBoot + 数据库连接池监控 + 动态扩容:连接不足时自动扩容,避免请求排队
前言
数据库连接池是应用与数据库之间的桥梁,它的健康状况直接影响着整个系统的性能。当连接池中的连接被耗尽时,新的请求只能排队等待,响应时间从毫秒级飙升到秒级甚至超时,这就是 dreaded 的"连接池耗尽"问题。
传统的连接池配置是静态的:在应用启动时设置好最大连接数,之后就一成不变。但在实际生产环境中,流量是动态变化的:
- 早高峰:用户登录、查看数据,连接需求激增
- 大促期间:订单量暴增,数据库压力陡增
- 夜间批处理:定时任务集中执行,连接竞争激烈
本文将介绍一套完整的数据库连接池监控与动态扩容方案,实现:
- 实时监控:全面掌握连接池运行状态
- 智能扩容:连接不足时自动增加连接数
- 缩容回收:空闲时自动释放多余连接
- 告警通知:异常情况及时通知运维
一、连接池问题分析
1. 连接池耗尽的场景
┌─────────────────────────────────────────────────────────────┐
│ 连接池耗尽场景示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正常情况 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 连接池:10个连接 │ │
│ │ 活跃连接:3个 │ │
│ │ 空闲连接:7个 │ │
│ │ 等待队列:0个请求 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 流量激增 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 连接池:10个连接 │ │
│ │ 活跃连接:10个(全部占满) │ │
│ │ 空闲连接:0个 │ │
│ │ 等待队列:50个请求(排队中) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 后果: │
│ - 请求响应时间从 10ms → 5000ms+ │
│ - 大量请求超时失败 │
│ - 用户体验极差,系统几乎不可用 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 连接池关键指标
| 指标 | 说明 | 健康阈值 | 危险阈值 |
|---|---|---|---|
| 活跃连接数 | 正在使用的连接 | < 70% | > 90% |
| 空闲连接数 | 可用的连接 | > 20% | < 10% |
| 等待队列长度 | 等待连接的请求 | 0 | > 10 |
| 连接等待时间 | 获取连接的平均时间 | < 100ms | > 500ms |
| 连接使用率 | 活跃连接 / 最大连接 | < 70% | > 90% |
3. 静态配置的痛点
┌─────────────────────────────────────────────────────────────┐
│ 静态配置 vs 动态配置 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 静态配置(传统方式) │
│ ─────────────────── │
│ 最大连接数 = 50(固定) │
│ │
│ 低峰期:使用 5 个连接 ──► 45 个连接闲置,浪费资源 │
│ 高峰期:需要 80 个连接 ──► 30 个请求排队,性能下降 │
│ │
│ 动态配置(本文方案) │
│ ─────────────────── │
│ 初始连接数 = 10 │
│ 最小连接数 = 5 │
│ 最大连接数 = 100 │
│ │
│ 低峰期:自动缩容到 5 个 ──► 节省资源 │
│ 高峰期:自动扩容到 80 个 ──► 满足需求,无排队 │
│ │
└─────────────────────────────────────────────────────────────┘
二、整体架构设计
1. 系统架构
┌─────────────────────────────────────────────────────────────┐
│ 连接池监控与动态扩容系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 监控采集层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ HikariCP │ │ 连接池指标 │ │ │
│ │ │ MXBean 监控 │ │ 采集器 │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │
│ │ └────────┬────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ 指标数据流 │ │ │
│ │ └──────┬───────┘ │ │
│ └────────────────┼──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 分析决策层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 负载分析器 │ │ 扩容决策器 │ │ │
│ │ │ (趋势预测) │ │ (策略引擎) │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │
│ │ └────────┬────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ 扩容/缩容指令│ │ │
│ │ └──────┬───────┘ │ │
│ └────────────────┼──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 执行控制层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 连接池配置 │ │ 平滑扩缩容 │ │ │
│ │ │ 动态修改器 │ │ 控制器 │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 告警通知层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 邮件告警 │ │ 钉钉通知 │ │ 日志记录 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2. 核心组件
| 组件 | 职责 | 技术选型 |
|---|---|---|
| 连接池监控器 | 实时采集连接池指标 | HikariCP MXBean |
| 负载分析器 | 分析连接使用趋势 | 滑动窗口算法 |
| 扩容决策器 | 根据策略决定扩缩容 | 规则引擎 |
| 配置修改器 | 动态修改连接池配置 | HikariConfig |
| 告警服务 | 异常时发送通知 | 邮件/钉钉/企业微信 |
3. 扩容策略
┌─────────────────────────────────────────────────────────────┐
│ 扩容决策流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 采集指标(每 10 秒) │
│ │ │
│ ▼ │
│ 2. 计算连接使用率 │
│ 使用率 = 活跃连接数 / 最大连接数 │
│ │ │
│ ▼ │
│ 3. 判断是否需要扩容 │
│ │ │
│ ├── 使用率 > 80% 且 等待队列 > 0 ──► 需要扩容 │
│ │ │
│ ├── 使用率 < 30% 持续 5 分钟 ──► 需要缩容 │
│ │ │
│ └── 其他情况 ──► 保持现状 │
│ │ │
│ ▼ │
│ 4. 计算目标连接数 │
│ 目标数 = min(当前最大连接数 + 增量, 绝对上限) │
│ │ │
│ ▼ │
│ 5. 执行扩容/缩容 │
│ │ │
│ ▼ │
│ 6. 记录日志并通知 │
│ │
└─────────────────────────────────────────────────────────────┘
三、代码实现
1. 项目结构
SpringBoot-ConnectionPool-Demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── connectionpool/
│ │ ├── ConnectionPoolApplication.java
│ │ ├── config/
│ │ │ ├── DataSourceConfig.java
│ │ │ └── DynamicPoolConfig.java
│ │ ├── monitor/
│ │ │ ├── ConnectionPoolMonitor.java
│ │ │ ├── ConnectionPoolMetrics.java
│ │ │ └── PoolStatistics.java
│ │ ├── scaler/
│ │ │ ├── PoolScaler.java
│ │ │ ├── ScaleStrategy.java
│ │ │ └── ScaleDecision.java
│ │ ├── controller/
│ │ │ ├── PoolController.java
│ │ │ └── UserController.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── SimulationService.java
│ │ ├── entity/
│ │ │ └── User.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ └── notification/
│ │ └── NotificationService.java
│ └── resources/
│ └── application.yml
├── pom.xml
└── README.md
2. 动态连接池配置
# application.yml
spring:
application:
name: connection-pool-demo
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
hikari:
# 初始配置
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: DynamicHikariPool
# 动态连接池配置
dynamic:
pool:
enabled: true
# 扩容配置
scale-up:
enabled: true
threshold-percentage: 80 # 使用率超过80%触发扩容
queue-length-threshold: 5 # 等待队列超过5触发扩容
increment-size: 10 # 每次增加10个连接
max-pool-size: 100 # 最大连接数上限
cooldown-seconds: 60 # 扩容冷却时间(秒)
# 缩容配置
scale-down:
enabled: true
threshold-percentage: 30 # 使用率低于30%触发缩容
min-pool-size: 5 # 最小连接数下限
decrement-size: 5 # 每次减少5个连接
idle-duration-minutes: 5 # 持续空闲5分钟才缩容
# 监控配置
monitor:
enabled: true
interval-seconds: 10 # 监控间隔
metrics-retention-minutes: 60 # 指标保留时间
# 告警配置
alert:
enabled: true
high-usage-threshold: 90 # 使用率超过90%告警
queue-wait-threshold: 1000 # 等待时间超过1000ms告警
channels: email,dingtalk
logging:
level:
com.example.connectionpool: DEBUG
com.zaxxer.hikari: DEBUG
3. 连接池监控器
@Component
@Slf4j
public class ConnectionPoolMonitor {
@Autowired
private HikariDataSource dataSource;
@Autowired
private DynamicPoolConfig config;
@Autowired
private PoolScaler poolScaler;
@Autowired
private NotificationService notificationService;
private final Queue<PoolStatistics> metricsHistory = new ConcurrentLinkedQueue<>();
@Scheduled(fixedRateString = "${dynamic.pool.monitor.interval-seconds:10}000")
public void monitor() {
if (!config.isEnabled()) {
return;
}
try {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
if (poolMXBean == null) {
return;
}
PoolStatistics stats = collectMetrics(poolMXBean, configMXBean);
metricsHistory.offer(stats);
// 清理过期数据
cleanupOldMetrics();
// 记录日志
log.debug("连接池状态: 活跃={}, 空闲={}, 等待={}, 总数={}",
stats.getActiveConnections(),
stats.getIdleConnections(),
stats.getPendingThreads(),
stats.getTotalConnections());
// 检查是否需要扩缩容
checkAndScale(stats);
// 检查告警条件
checkAlerts(stats);
} catch (Exception e) {
log.error("连接池监控失败", e);
}
}
private PoolStatistics collectMetrics(HikariPoolMXBean poolMXBean,
HikariConfigMXBean configMXBean) {
return PoolStatistics.builder()
.timestamp(System.currentTimeMillis())
.activeConnections(poolMXBean.getActiveConnections())
.idleConnections(poolMXBean.getIdleConnections())
.totalConnections(poolMXBean.getTotalConnections())
.pendingThreads(poolMXBean.getThreadsAwaitingConnection())
.maxPoolSize(configMXBean.getMaximumPoolSize())
.minIdleConnections(configMXBean.getMinimumIdle())
.usagePercentage(calculateUsagePercentage(poolMXBean, configMXBean))
.build();
}
private double calculateUsagePercentage(HikariPoolMXBean poolMXBean,
HikariConfigMXBean configMXBean) {
int maxSize = configMXBean.getMaximumPoolSize();
if (maxSize == 0) {
return 0;
}
return (double) poolMXBean.getActiveConnections() / maxSize * 100;
}
private void cleanupOldMetrics() {
long cutoff = System.currentTimeMillis() -
TimeUnit.MINUTES.toMillis(config.getMonitor().getMetricsRetentionMinutes());
while (!metricsHistory.isEmpty() &&
metricsHistory.peek().getTimestamp() < cutoff) {
metricsHistory.poll();
}
}
private void checkAndScale(PoolStatistics stats) {
ScaleDecision decision = poolScaler.evaluate(stats, getRecentMetrics());
if (decision.isShouldScale()) {
log.info("触发连接池{}: 当前={}, 目标={}, 原因={}",
decision.getScaleType() == ScaleDecision.ScaleType.UP ? "扩容" : "缩容",
stats.getMaxPoolSize(),
decision.getTargetPoolSize(),
decision.getReason());
boolean success = poolScaler.executeScale(decision);
if (success) {
notificationService.notifyScale(decision, stats);
}
}
}
private void checkAlerts(PoolStatistics stats) {
// 高使用率告警
if (stats.getUsagePercentage() > config.getAlert().getHighUsageThreshold()) {
notificationService.notifyHighUsage(stats);
}
// 高等待队列告警
if (stats.getPendingThreads() > 0) {
notificationService.notifyQueueWait(stats);
}
}
public List<PoolStatistics> getRecentMetrics() {
return new ArrayList<>(metricsHistory);
}
public PoolStatistics getCurrentMetrics() {
List<PoolStatistics> metrics = getRecentMetrics();
return metrics.isEmpty() ? null : metrics.get(metrics.size() - 1);
}
}
4. 连接池扩缩容器
@Component
@Slf4j
public class PoolScaler {
@Autowired
private HikariDataSource dataSource;
@Autowired
private DynamicPoolConfig config;
private final AtomicLong lastScaleTime = new AtomicLong(0);
private final AtomicInteger consecutiveLowUsage = new AtomicInteger(0);
public ScaleDecision evaluate(PoolStatistics current, List<PoolStatistics> history) {
// 检查冷却时间
long cooldownMillis = TimeUnit.SECONDS.toMillis(
config.getScaleUp().getCooldownSeconds());
if (System.currentTimeMillis() - lastScaleTime.get() < cooldownMillis) {
return ScaleDecision.noScale("冷却期内,跳过评估");
}
// 评估扩容
ScaleDecision scaleUpDecision = evaluateScaleUp(current, history);
if (scaleUpDecision.isShouldScale()) {
return scaleUpDecision;
}
// 评估缩容
return evaluateScaleDown(current, history);
}
private ScaleDecision evaluateScaleUp(PoolStatistics current, List<PoolStatistics> history) {
if (!config.getScaleUp().isEnabled()) {
return ScaleDecision.noScale("扩容已禁用");
}
int currentMax = current.getMaxPoolSize();
int absoluteMax = config.getScaleUp().getMaxPoolSize();
// 已达到上限
if (currentMax >= absoluteMax) {
return ScaleDecision.noScale("已达到最大连接数上限: " + absoluteMax);
}
// 使用率超过阈值
boolean highUsage = current.getUsagePercentage() >
config.getScaleUp().getThresholdPercentage();
// 有等待线程
boolean hasQueue = current.getPendingThreads() >=
config.getScaleUp().getQueueLengthThreshold();
if (highUsage || hasQueue) {
int increment = config.getScaleUp().getIncrementSize();
int targetSize = Math.min(currentMax + increment, absoluteMax);
String reason = String.format("使用率%.1f%% %s 等待队列%d",
current.getUsagePercentage(),
highUsage ? "超过阈值" : "正常",
current.getPendingThreads());
return ScaleDecision.scaleUp(targetSize, reason);
}
return ScaleDecision.noScale("未达到扩容条件");
}
private ScaleDecision evaluateScaleDown(PoolStatistics current, List<PoolStatistics> history) {
if (!config.getScaleDown().isEnabled()) {
return ScaleDecision.noScale("缩容已禁用");
}
int currentMax = current.getMaxPoolSize();
int absoluteMin = config.getScaleDown().getMinPoolSize();
// 已达到下限
if (currentMax <= absoluteMin) {
consecutiveLowUsage.set(0);
return ScaleDecision.noScale("已达到最小连接数下限: " + absoluteMin);
}
// 使用率低于阈值
if (current.getUsagePercentage() < config.getScaleDown().getThresholdPercentage()) {
int count = consecutiveLowUsage.incrementAndGet();
int requiredCount = config.getScaleDown().getIdleDurationMinutes() * 6; // 每10秒检查一次
if (count >= requiredCount) {
int decrement = config.getScaleDown().getDecrementSize();
int targetSize = Math.max(currentMax - decrement, absoluteMin);
consecutiveLowUsage.set(0);
return ScaleDecision.scaleDown(targetSize,
String.format("使用率持续%.1f%%低于阈值%d%%",
current.getUsagePercentage(),
config.getScaleDown().getThresholdPercentage()));
}
} else {
consecutiveLowUsage.set(0);
}
return ScaleDecision.noScale("未达到缩容条件");
}
public boolean executeScale(ScaleDecision decision) {
try {
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
int currentSize = configMXBean.getMaximumPoolSize();
int targetSize = decision.getTargetPoolSize();
if (currentSize == targetSize) {
return false;
}
// 修改最大连接数
configMXBean.setMaximumPoolSize(targetSize);
// 同时调整最小空闲连接
if (decision.getScaleType() == ScaleDecision.ScaleType.UP) {
int newMinIdle = Math.min(targetSize, configMXBean.getMinimumIdle() + 5);
configMXBean.setMinimumIdle(newMinIdle);
} else {
int newMinIdle = Math.max(config.getScaleDown().getMinPoolSize(),
configMXBean.getMinimumIdle() - 5);
configMXBean.setMinimumIdle(newMinIdle);
}
lastScaleTime.set(System.currentTimeMillis());
log.info("连接池{}完成: {} -> {}",
decision.getScaleType() == ScaleDecision.ScaleType.UP ? "扩容" : "缩容",
currentSize, targetSize);
return true;
} catch (Exception e) {
log.error("执行连接池扩缩容失败", e);
return false;
}
}
}
5. 扩缩容决策类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ScaleDecision {
private boolean shouldScale;
private ScaleType scaleType;
private int targetPoolSize;
private String reason;
public enum ScaleType {
UP, DOWN
}
public static ScaleDecision noScale(String reason) {
return ScaleDecision.builder()
.shouldScale(false)
.reason(reason)
.build();
}
public static ScaleDecision scaleUp(int targetSize, String reason) {
return ScaleDecision.builder()
.shouldScale(true)
.scaleType(ScaleType.UP)
.targetPoolSize(targetSize)
.reason(reason)
.build();
}
public static ScaleDecision scaleDown(int targetSize, String reason) {
return ScaleDecision.builder()
.shouldScale(true)
.scaleType(ScaleType.DOWN)
.targetPoolSize(targetSize)
.reason(reason)
.build();
}
}
6. 连接池指标统计
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PoolStatistics {
private long timestamp;
private int activeConnections;
private int idleConnections;
private int totalConnections;
private int pendingThreads;
private int maxPoolSize;
private int minIdleConnections;
private double usagePercentage;
public int getQueueLength() {
return pendingThreads;
}
public boolean isHealthy() {
return usagePercentage < 80 && pendingThreads == 0;
}
public boolean isCritical() {
return usagePercentage > 90 || pendingThreads > 10;
}
}
7. 连接池管理接口
@RestController
@RequestMapping("/api/pool")
@Slf4j
public class PoolController {
@Autowired
private ConnectionPoolMonitor poolMonitor;
@Autowired
private HikariDataSource dataSource;
@Autowired
private PoolScaler poolScaler;
@GetMapping("/status")
public ApiResponse<Map<String, Object>> getStatus() {
Map<String, Object> status = new HashMap<>();
PoolStatistics current = poolMonitor.getCurrentMetrics();
if (current != null) {
status.put("current", current);
}
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
status.put("maxPoolSize", configMXBean.getMaximumPoolSize());
status.put("minIdle", configMXBean.getMinimumIdle());
return ApiResponse.success(status);
}
@GetMapping("/metrics/history")
public ApiResponse<List<PoolStatistics>> getMetricsHistory() {
return ApiResponse.success(poolMonitor.getRecentMetrics());
}
@PostMapping("/scale")
public ApiResponse<String> manualScale(@RequestParam int targetSize) {
try {
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
int currentSize = configMXBean.getMaximumPoolSize();
configMXBean.setMaximumPoolSize(targetSize);
return ApiResponse.success(String.format("连接池已调整: %d -> %d",
currentSize, targetSize));
} catch (Exception e) {
log.error("手动调整连接池失败", e);
return ApiResponse.error("调整失败: " + e.getMessage());
}
}
@GetMapping("/config")
public ApiResponse<Map<String, Object>> getConfig() {
Map<String, Object> config = new HashMap<>();
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
config.put("maximumPoolSize", configMXBean.getMaximumPoolSize());
config.put("minimumIdle", configMXBean.getMinimumIdle());
config.put("connectionTimeout", configMXBean.getConnectionTimeout());
config.put("idleTimeout", configMXBean.getIdleTimeout());
config.put("maxLifetime", configMXBean.getMaxLifetime());
return ApiResponse.success(config);
}
}
8. 通知服务
@Service
@Slf4j
public class NotificationService {
@Autowired
private DynamicPoolConfig config;
public void notifyScale(ScaleDecision decision, PoolStatistics stats) {
if (!config.getAlert().isEnabled()) {
return;
}
String message = String.format(
"【连接池%s通知】\n时间: %s\n类型: %s\n目标大小: %d\n当前状态: 活跃=%d, 空闲=%d, 等待=%d\n原因: %s",
decision.getScaleType() == ScaleDecision.ScaleType.UP ? "扩容" : "缩容",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
decision.getScaleType(),
decision.getTargetPoolSize(),
stats.getActiveConnections(),
stats.getIdleConnections(),
stats.getPendingThreads(),
decision.getReason()
);
log.info(message);
// 这里可以集成邮件、钉钉等通知渠道
sendNotification(message);
}
public void notifyHighUsage(PoolStatistics stats) {
String message = String.format(
"【连接池高使用率告警】\n使用率: %.1f%%\n活跃连接: %d\n最大连接: %d\n等待队列: %d",
stats.getUsagePercentage(),
stats.getActiveConnections(),
stats.getMaxPoolSize(),
stats.getPendingThreads()
);
log.warn(message);
sendNotification(message);
}
public void notifyQueueWait(PoolStatistics stats) {
String message = String.format(
"【连接池等待队列告警】\n等待线程: %d\n活跃连接: %d\n空闲连接: %d",
stats.getPendingThreads(),
stats.getActiveConnections(),
stats.getIdleConnections()
);
log.warn(message);
sendNotification(message);
}
private void sendNotification(String message) {
// 实现邮件、钉钉等通知逻辑
// TODO: 集成实际的通知渠道
}
}
9. 数据源配置
@Configuration
@Slf4j
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setPoolName("DynamicHikariPool");
// 注册 MBean 以便监控
dataSource.setRegisterMbeans(true);
log.info("初始化 HikariCP 连接池");
return dataSource;
}
}
四、高级功能
1. 连接池性能测试
@Service
@Slf4j
public class SimulationService {
@Autowired
private UserService userService;
@Autowired
private ExecutorService executorService;
public void simulateHighLoad(int concurrentRequests, int durationSeconds) {
log.info("开始模拟高并发场景: {} 并发, {} 秒", concurrentRequests, durationSeconds);
CountDownLatch latch = new CountDownLatch(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
long endTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(durationSeconds);
for (int i = 0; i < concurrentRequests; i++) {
executorService.submit(() -> {
try {
while (System.currentTimeMillis() < endTime) {
try {
userService.getAllUsers();
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
}
// 模拟随机间隔
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("模拟完成: 成功={}, 失败={}", successCount.get(), failCount.get());
}
}
2. 连接池指标导出
@Component
@Slf4j
public class ConnectionPoolMetrics {
@Autowired
private HikariDataSource dataSource;
@Autowired
private MeterRegistry meterRegistry;
@PostConstruct
public void init() {
// 活跃连接数
Gauge.builder("hikaricp.connections.active",
() -> getPoolMXBean().getActiveConnections())
.description("活跃连接数")
.register(meterRegistry);
// 空闲连接数
Gauge.builder("hikaricp.connections.idle",
() -> getPoolMXBean().getIdleConnections())
.description("空闲连接数")
.register(meterRegistry);
// 等待线程数
Gauge.builder("hikaricp.connections.pending",
() -> getPoolMXBean().getThreadsAwaitingConnection())
.description("等待连接的线程数")
.register(meterRegistry);
// 连接使用率
Gauge.builder("hikaricp.connections.usage", this::getUsagePercentage)
.description("连接使用率")
.register(meterRegistry);
// 最大连接数
Gauge.builder("hikaricp.connections.max",
() -> getConfigMXBean().getMaximumPoolSize())
.description("最大连接数")
.register(meterRegistry);
}
private HikariPoolMXBean getPoolMXBean() {
return dataSource.getHikariPoolMXBean();
}
private HikariConfigMXBean getConfigMXBean() {
return dataSource.getHikariConfigMXBean();
}
private double getUsagePercentage() {
HikariPoolMXBean poolMXBean = getPoolMXBean();
HikariConfigMXBean configMXBean = getConfigMXBean();
int maxSize = configMXBean.getMaximumPoolSize();
if (maxSize == 0) {
return 0;
}
return (double) poolMXBean.getActiveConnections() / maxSize * 100;
}
}
五、最佳实践
1. 连接池参数设置建议
| 参数 | 开发环境 | 测试环境 | 生产环境 | 说明 |
|---|---|---|---|---|
| minimum-idle | 5 | 10 | 10-20 | 根据业务低峰期连接需求 |
| maximum-pool-size | 20 | 50 | 50-100 | 根据数据库承载能力 |
| connection-timeout | 30000 | 30000 | 10000-30000 | 获取连接等待时间 |
| idle-timeout | 600000 | 600000 | 300000-600000 | 空闲连接回收时间 |
| max-lifetime | 1800000 | 1800000 | 1800000 | 连接最大生命周期 |
2. 扩容策略建议
# 保守策略(适合稳定业务)
scale-up:
threshold-percentage: 85
increment-size: 5
max-pool-size: 50
# 激进策略(适合大促场景)
scale-up:
threshold-percentage: 70
increment-size: 20
max-pool-size: 200
3. 监控告警阈值
| 告警类型 | 阈值 | 级别 |
|---|---|---|
| 高使用率 | > 90% | 严重 |
| 使用率偏高 | > 80% | 警告 |
| 等待队列 | > 10 | 严重 |
| 扩容频繁 | 5分钟内 > 3次 | 警告 |
六、常见问题
Q1: 动态修改连接池配置会影响正在执行的请求吗?
A:
- 增加最大连接数:不会影响现有请求,新请求可以使用增加的连接
- 减少最大连接数:不会强制关闭活跃连接,但空闲连接会被回收
- 建议:缩容时逐步减少,避免对业务造成影响
Q2: 连接池扩到很大会有什么问题?
A:
- 数据库端连接数限制
- 内存占用增加
- 上下文切换开销增大
- 建议:根据数据库实际承载能力设置上限
Q3: 如何确定连接池的合理大小?
A:
- 公式:连接数 = ((核心数 × 2) + 有效磁盘数)
- 压测:逐步增加连接数,观察吞吐量变化
- 监控:根据实际使用率动态调整
更多技术文章,欢迎关注公众号"服务端技术精选",及时获取最新动态。
标题:SpringBoot + 数据库连接池监控 + 动态扩容:连接不足时自动扩容,避免请求排队
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/29/1774590750902.html
公众号:服务端技术精选
- 前言
- 一、连接池问题分析
- 1. 连接池耗尽的场景
- 2. 连接池关键指标
- 3. 静态配置的痛点
- 二、整体架构设计
- 1. 系统架构
- 2. 核心组件
- 3. 扩容策略
- 三、代码实现
- 1. 项目结构
- 2. 动态连接池配置
- 3. 连接池监控器
- 4. 连接池扩缩容器
- 5. 扩缩容决策类
- 6. 连接池指标统计
- 7. 连接池管理接口
- 8. 通知服务
- 9. 数据源配置
- 四、高级功能
- 1. 连接池性能测试
- 2. 连接池指标导出
- 五、最佳实践
- 1. 连接池参数设置建议
- 2. 扩容策略建议
- 3. 监控告警阈值
- 六、常见问题
- Q1: 动态修改连接池配置会影响正在执行的请求吗?
- Q2: 连接池扩到很大会有什么问题?
- Q3: 如何确定连接池的合理大小?
评论
0 评论