一、数据库连接池的核心本质与通用设计模型
1.1 为什么必须用数据库连接池
数据库连接的创建与销毁是典型的重量级操作:一次完整的JDBC连接创建,需要经历TCP三次握手、MySQL服务端身份认证、会话上下文初始化、权限校验等多个环节,单次耗时通常在几十到几百毫秒。而在高并发场景下,每秒数千次的连接创建销毁,会直接耗尽系统CPU与网络资源,导致数据库响应延迟飙升甚至服务不可用。
连接池的核心价值,是通过池化思想实现连接的复用,将连接的生命周期交由池容器统一管理:应用启动时预先创建一定数量的连接,业务请求直接从池中获取空闲连接,使用完毕后归还给池而非直接销毁,从根本上消除频繁创建销毁连接的开销,同时通过连接数上限控制数据库的并发访问压力。
1.2 连接池的核心设计组件
所有成熟的数据库连接池,都围绕以下核心组件构建底层能力:
- 连接池核心容器:存储所有数据库连接的内存结构,是并发控制的核心载体,直接决定连接获取与释放的性能
- 连接生命周期管理:负责连接的创建、校验、回收、销毁,保证连接的有效性与生命周期可控
- 空闲连接回收机制:定时清理长时间空闲的无效连接,平衡资源占用与请求响应速度
- 连接泄露检测:识别并回收长时间被占用未归还的连接,避免连接池耗尽
- 并发控制机制:保证多线程环境下连接获取与释放的线程安全,同时最大化降低锁竞争带来的性能损耗
- 监控与扩展体系:采集连接池运行状态、SQL执行指标,提供可扩展的功能插件体系
1.3 连接池的核心性能瓶颈
连接池的性能天花板,本质上由两个核心因素决定:
- 连接获取与释放的锁竞争强度:传统连接池大多使用阻塞队列存储空闲连接,多线程并发获取连接时,会产生激烈的锁竞争,导致线程阻塞与上下文切换,这是最核心的性能损耗点
- 连接有效性校验的开销:无效连接的处理逻辑,直接决定业务请求的成功率与性能,不合理的校验机制会带来额外的数据库交互开销
二、HikariCP底层原理:极致性能的核心密码
2.1 HikariCP的核心定位与优势
HikariCP是一款专注于极致性能与稳定性的轻量级数据库连接池,自SpringBoot 2.0起成为官方默认连接池,凭借极简的设计与极致的优化,在各类性能基准测试中常年稳居榜首。它的设计理念是「专注于连接池的核心职责,剔除所有非必要的功能损耗」,核心优势集中在极致的性能、极低的资源占用与极高的稳定性。
2.2 核心数据结构:ConcurrentBag无锁设计
HikariCP性能远超同类产品的核心,是自研的ConcurrentBag无锁并发容器,彻底替代了传统连接池使用的BlockingQueue,从根本上解决了多线程场景下的锁竞争问题。
ConcurrentBag的核心设计围绕「线程局部优先、CAS无锁修改、弱一致性」三大原则构建,核心结构包括:
- ThreadLocal threadList:每个线程私有的本地缓存,存储当前线程使用过的连接,线程获取连接时优先从本地缓存中读取,完全无锁
- CopyOnWriteArrayList sharedList:存储连接池内所有的连接对象,全局共享,保证数据的最终一致性
- 同步队列:用于存储等待连接的线程,当池内无可用连接时,线程进入队列阻塞等待
- 连接状态机:每个PoolEntry连接对象维护一个volatile修饰的state状态字段,通过CAS操作实现状态的无锁修改,状态包括:
- STATE_NOT_IN_USE:空闲可用
- STATE_IN_USE:已被占用
- STATE_RESERVED:已被预留
- STATE_REMOVED:已被移除
ConcurrentBag的核心优化逻辑:
- 绝大多数场景下,线程会从自己的ThreadLocal中获取到连接,全程无锁、无阻塞,性能达到极致
- 当本地缓存无可用连接时,才会遍历全局共享列表,通过CAS操作抢占空闲连接,仅在CAS失败时才会重试,避免了锁竞争
- 采用弱一致性设计,允许短暂的连接状态不一致,换取更高的并发性能,最终通过状态机保证数据正确性
2.3 极致性能的其他核心优化
- FastList替代ArrayListHikariCP内部使用自研的FastList替代JDK的ArrayList存储连接的Statement对象,核心优化点:
- 去掉了ArrayList的get方法范围校验,因为连接池内部完全可控,不会出现数组越界
- 重写了remove方法,从数组尾部向前遍历查找元素,因为Statement的关闭顺序与创建顺序相反,尾部查找命中率更高,时间复杂度从O(n)降低到接近O(1)
- 字节码动态代理优化HikariCP使用Javassist动态生成Connection、Statement等JDBC接口的代理类,替代JDK原生的动态代理。相比JDK动态代理的反射调用,动态生成的字节码代理类直接实现了接口方法,消除了反射带来的性能损耗,同时精简了代理逻辑,只保留连接池必要的拦截逻辑。
- 精简的代码体量HikariCP的核心代码量极小,剔除了所有非必要的功能与冗余逻辑,不仅降低了CPU的指令缓存失效概率,也极大减少了潜在的BUG风险,保证了极高的稳定性。
2.4 连接生命周期管理机制
HikariCP对连接的生命周期做了精细化的管理,核心逻辑:
- 连接创建后,会被加入sharedList,同时记录创建时间,用于计算最大生命周期
- 连接被获取时,通过CAS修改状态为IN_USE,记录获取时间,用于连接泄露检测
- 连接释放时,通过CAS修改状态为NOT_IN_USE,归还到池中,等待下次复用
- 后台定时线程会定期扫描所有连接,销毁超过最大生命周期的连接,同时补充空闲连接到最小空闲数
- 连接有效性校验默认使用JDBC4规范的
Connection.isValid()方法,该方法由数据库驱动原生实现,通过TCP ping包校验连接有效性,无需执行SQL语句,性能远高于传统的SELECT 1校验方式
三、Druid底层原理:企业级全功能连接池的架构设计
3.1 Druid的核心定位与优势
Druid是阿里开源的企业级数据库连接池,是国内使用最广泛的连接池产品,核心设计理念是「一站式企业级数据库访问解决方案」,在保证连接池核心能力的同时,内置了完善的监控体系、SQL防火墙、扩展插件等企业级功能,深度适配国内的业务场景与运维需求。
3.2 核心架构与连接池底层实现
Druid的核心架构围绕可扩展的责任链模式构建,底层连接池通过锁与条件队列实现并发控制,核心结构如下:
Druid连接池的底层核心实现:
- DruidConnectionHolder数组:存储连接池内所有的连接对象,包括活跃连接与空闲连接,是连接池的核心存储结构
- ReentrantLock可重入锁:全局唯一的锁对象,保证连接获取、释放、创建、销毁等操作的线程安全,支持公平锁与非公平锁两种模式
- Condition条件队列:包含notEmpty与notFull两个条件对象,实现线程的等待与唤醒机制:
- 当池内无空闲连接时,获取连接的线程进入notEmpty队列阻塞等待
- 当池内连接数达到上限时,创建连接的线程进入notFull队列阻塞等待
- 活跃连接集合:维护当前被业务线程占用的连接对象,用于连接泄露检测与监控统计
- 后台销毁线程:定时运行,清理超过空闲时间的连接、超过最大生命周期的无效连接,回收泄露的连接
3.3 灵魂设计:Filter-Chain扩展机制
Filter-Chain责任链模式是Druid最核心的设计,也是它能够实现丰富功能的核心支撑。Druid将数据库连接的所有操作(连接创建、连接关闭、SQL执行、事务提交/回滚等)都抽象为责任链上的节点,所有的附加功能都通过Filter插件实现,无需修改核心代码,具备极强的扩展性。
Filter-Chain的核心执行逻辑:
- 业务线程的每一次JDBC操作,都会先经过Filter链的前置处理
- 依次执行链上所有Filter的拦截逻辑,最终调用原生JDBC的方法执行操作
- 操作执行完成后,反向执行Filter链的后置处理,完成指标统计、日志记录、结果校验等操作
这种设计的优势在于,功能模块完全解耦,用户可以根据业务需求自由组合Filter,比如只开启监控功能,就只加载StatFilter;需要SQL防火墙,就加载WallFilter;需要自定义日志逻辑,就实现自定义Filter加入链中,灵活性极高。
3.4 企业级核心特性详解
- 全维度监控体系Druid内置了业界最完善的监控能力,覆盖连接池运行状态、SQL执行全链路、事务执行、Web请求等多个维度:
- 连接池监控:实时展示当前活跃连接数、空闲连接数、等待连接的线程数、连接创建销毁次数等核心指标
- SQL执行监控:统计所有SQL的执行次数、执行时间、最大耗时、最小耗时、出错次数,支持按耗时排序定位慢SQL
- 慢SQL记录:可配置慢SQL阈值,自动记录执行超时的SQL语句、执行参数、堆栈信息,方便定位性能问题
- 事务监控:统计事务的提交、回滚次数,识别长事务与异常事务
- 内置Web监控页面:无需额外开发,通过简单配置即可访问可视化监控界面,查看所有监控指标
- SQL防火墙(WallFilter)WallFilter是Druid内置的SQL安全防护组件,基于SQL语法解析实现,核心能力包括:
- SQL注入防护:识别并拦截恶意的SQL注入语句,比如永真条件、注释绕过、多语句注入等攻击方式
- 语法校验:拦截不符合SQL规范的语句,避免执行报错
- 权限控制:禁止执行DROP、ALTER、TRUNCATE等高危DDL语句,禁止无WHERE条件的UPDATE、DELETE语句
- 性能防护:禁止执行全表扫描的大查询,避免数据库性能被拖垮
- 连接泄露检测与回收Druid提供了完善的连接泄露检测能力,可配置开启泄露检测,当连接被业务线程获取后,超过指定时间未归还,会被强制回收,同时打印连接的堆栈信息,帮助开发人员快速定位泄露的代码位置。
- 数据库兼容性Druid完美适配MySQL、Oracle、SQL Server、PostgreSQL等所有主流数据库,针对不同数据库的特性做了专门的优化与适配,无需额外修改配置即可无缝切换。
四、HikariCP与Druid核心维度对比
| 对比维度 | HikariCP | Druid |
| 核心定位 | 极致性能的轻量级连接池 | 一站式企业级数据库访问解决方案 |
| 性能表现 | 极高,无锁设计带来的性能优势明显 | 良好,因Filter链与监控逻辑有轻微性能损耗 |
| 功能丰富度 | 极简,仅保留连接池核心功能 | 极丰富,内置监控、SQL防火墙、扩展插件等企业级功能 |
| 资源占用 | 极低,代码量小,内存占用低 | 中等,功能丰富带来一定的内存与CPU占用 |
| 监控能力 | 基础,仅提供核心JMX指标,需额外集成监控系统 | 完善,内置全维度监控与可视化Web界面,开箱即用 |
| 扩展能力 | 弱,几乎无扩展点 | 极强,基于Filter-Chain可实现任意自定义扩展 |
| 适用场景 | 追求极致性能的微服务、高并发接口、云原生场景 | 企业级业务系统、需要全链路监控与安全防护的场景、传统IT系统 |
| 社区支持 | 全球范围活跃,SpringBoot官方默认,持续迭代 | 国内社区活跃,阿里内部大规模使用,稳定版持续维护 |
五、高并发场景核心调优策略
5.1 连接池调优的第一性原理
连接池调优最核心的误区,是认为「最大连接数设置越大,性能越高」。实际上,数据库的处理能力是有限的,当连接数超过数据库的最优处理阈值后,性能会急剧下降。
数据库的最优连接数,核心由两个硬件因素决定:CPU核心数与磁盘IO能力。业界公认的最优连接数计算公式来自Oracle官方性能优化白皮书:
最优连接数 = (CPU核心数 * 2) + 有效磁盘数
举个例子,一台8核CPU、单SSD磁盘的MySQL服务器,最优连接数约为 8*2 +1 =17。即使是16核的服务器,最优连接数也不会超过40。
从业务视角补充计算逻辑:如果单条SQL的平均执行时间是10ms,单个连接每秒可以处理100个请求,那么10个连接就可以支撑1000QPS的业务流量。绝大多数业务场景下,连接池的最大连接数不需要超过50,只有存在大量长事务的场景,才需要适当上调。
5.2 通用核心参数调优指南
核心连接数参数
- 最大连接数:控制连接池能创建的连接上限,需根据压测结果设置,通常不超过50,必须小于MySQL的
max_connections配置(默认151),预留足够的连接给管理员与其他应用 - 最小空闲连接数:控制连接池长期保持的空闲连接数,高并发场景下,推荐设置与最大连接数相等,将连接池变为固定大小,避免高并发下动态创建连接带来的响应延迟
- 初始化连接数:应用启动时预先创建的连接数,推荐设置与最小空闲连接数相等,避免首次请求时需要创建连接带来的延迟
连接生命周期参数
- 连接最大生命周期:连接从创建到被强制销毁的最大时间,必须比数据库的
wait_timeout(MySQL默认8小时)小30秒以上,避免拿到被数据库服务端主动断开的无效连接 - 空闲连接超时时间:连接空闲超过该时间会被回收,仅当最小空闲连接数小于最大连接数时生效,必须小于连接最大生命周期
- 连接获取超时时间:线程从池中获取连接的最大等待时间,超过该时间未获取到连接则抛出异常,高并发场景下推荐设置为3000ms,避免线程长时间阻塞,实现快速失败,保护系统
连接有效性校验参数
- 获取连接时校验:每次获取连接时都执行校验,性能损耗极大,高并发场景下必须关闭
- 空闲时校验:连接空闲超过指定时间后,在下次获取时执行校验,性能损耗极小,推荐开启,配合后台回收线程使用
- 校验语句:JDBC4及以上驱动,优先使用驱动原生的
isValid()方法,无需设置校验语句;仅老旧驱动需要设置SELECT 1作为校验语句
5.3 HikariCP专属调优最佳实践
| 参数名 | 核心含义 | 默认值 | 高并发场景最佳实践 |
| maximum-pool-size | 连接池最大连接数 | 10 | 根据压测结果设置,通常10-50,不超过数据库最优连接数 |
| minimum-idle | 最小空闲连接数 | 10 | 设置与maximum-pool-size相等,固定连接池大小 |
| max-lifetime | 连接最大生命周期 | 1800000ms(30分钟) | 设置为1500000ms(25分钟),小于MySQL的wait_timeout |
| idle-timeout | 空闲连接超时时间 | 600000ms(10分钟) | 当minimum-idle与max相等时,该参数不生效,无需调整 |
| connection-timeout | 连接获取超时时间 | 30000ms(30秒) | 设置为3000ms,高并发下快速失败 |
| leak-detection-threshold | 连接泄露检测阈值 | 0(关闭) | 设置为60000ms(60秒),大于业务最长事务执行时间,开启泄露检测 |
| validation-timeout | 连接校验超时时间 | 5000ms | 设置为1000ms,避免校验超时阻塞业务 |
5.4 Druid专属调优最佳实践
| 参数名 | 核心含义 | 默认值 | 高并发场景最佳实践 |
| max-active | 最大活跃连接数 | 8 | 根据压测结果设置,通常10-50,不超过数据库最优连接数 |
| min-idle | 最小空闲连接数 | 0 | 设置与max-active相等,固定连接池大小 |
| initial-size | 初始化连接数 | 0 | 设置与min-idle相等,启动时预创建连接 |
| max-wait | 连接获取最大等待时间 | -1(无限等待) | 设置为3000ms,高并发下快速失败 |
| time-between-eviction-runs-millis | 后台回收线程运行间隔 | 60000ms(60秒) | 设置为30000ms(30秒),更频繁的清理无效连接 |
| min-evictable-idle-time-millis | 连接最小空闲时间 | 1800000ms(30分钟) | 设置为1500000ms(25分钟),小于MySQL的wait_timeout |
| test-on-borrow | 获取连接时校验 | false | 必须保持关闭,避免性能损耗 |
| test-while-idle | 空闲时校验 | true | 保持开启,配合回收线程实现低开销校验 |
| remove-abandoned | 开启连接泄露回收 | false | 高并发场景下开启,防止连接泄露耗尽池资源 |
| remove-abandoned-timeout | 泄露连接回收超时时间 | 300秒 | 设置为60秒,大于业务最长事务执行时间 |
| log-abandoned | 泄露连接日志打印 | false | 开启,打印泄露连接的堆栈信息,方便定位问题 |
| filters | 加载的Filter链 | 无 | 基础场景配置stat,wall,slf4j,高并发下无需加载非必要Filter |
5.5 高并发调优落地步骤
- 基准压测:通过压测工具获取业务的核心指标,包括平均QPS、单请求SQL执行时间、最长事务执行时间、数据库服务器的CPU与IO负载
- 初始参数设置:根据最优连接数公式,设置初始的最大连接数,将最小空闲连接数与最大连接数设置为相等,配置合理的连接生命周期与超时参数
- 梯度压测调整:逐步提升压测流量,观察连接池的等待线程数、活跃连接数、数据库的CPU负载与响应时间,微调最大连接数,找到性能最优的阈值
- 开启防护配置:开启连接泄露检测、慢SQL记录,配置合理的超时时间,避免异常场景下连接池耗尽
- 持续监控优化:通过监控系统采集连接池的核心指标,关注等待连接的线程数、连接泄露告警、慢SQL记录,持续优化业务代码与连接池配置
六、代码示例
6.1 项目环境与依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>demo-datasource-pool</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-datasource-pool</name>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<druid.version>1.2.23</druid.version>
<fastjson2.version>2.0.49</fastjson2.version>
<guava.version>32.1.3-jre</guava.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
6.2 数据库表结构
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(64) NOT NULL COMMENT '用户名',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
6.3 HikariCP整合SpringBoot3+MyBatisPlus实战
application.yml配置
spring:
application:
name: demo-datasource-pool
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
hikari:
maximum-pool-size: 20
minimum-idle: 20
max-lifetime: 1500000
connection-timeout: 3000
leak-detection-threshold: 60000
validation-timeout: 1000
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.jam.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
api-docs:
path: /v3/api-docs
packages-to-scan: com.jam.demo.controller
6.4 Druid整合SpringBoot3+MyBatisPlus实战
application.yml配置
spring:
application:
name: demo-datasource-pool
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
druid:
max-active: 20
min-idle: 20
initial-size: 20
max-wait: 3000
time-between-eviction-runs-millis: 30000
min-evictable-idle-time-millis: 1500000
test-on-borrow: false
test-while-idle: true
remove-abandoned: true
remove-abandoned-timeout: 60
log-abandoned: true
filters: stat,wall,slf4j
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin123
allow: 127.0.0.1
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.jam.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
api-docs:
path: /v3/api-docs
packages-to-scan: com.jam.demo.controller
6.5 核心代码实现
实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("t_user")
@Schema(description = "用户实体")
public class User {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "jam")
private String username;
@Schema(description = "年龄", example = "25")
private Integer age;
@Schema(description = "邮箱", example = "jam@example.com")
private String email;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
Service接口
package com.jam.demo.service;
import com.jam.demo.entity.User;
import java.util.List;
/**
* 用户服务接口
* @author ken
*/
public interface UserService {
Long addUser(User user);
User getUserById(Long id);
List<User> getUserList();
Boolean updateUser(User user);
Boolean deleteUserById(Long id);
}
Service实现类
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 用户服务实现类
* @author ken
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final TransactionTemplate transactionTemplate;
public UserServiceImpl(UserMapper userMapper, TransactionTemplate transactionTemplate) {
this.userMapper = userMapper;
this.transactionTemplate = transactionTemplate;
}
/**
* 新增用户
* @param user 用户实体
* @return 新增成功的用户ID
*/
@Override
public Long addUser(User user) {
if (ObjectUtils.isEmpty(user)) {
throw new IllegalArgumentException("用户对象不能为空");
}
if (!StringUtils.hasText(user.getUsername())) {
throw new IllegalArgumentException("用户名不能为空");
}
return transactionTemplate.execute(status -> {
try {
int result = userMapper.insert(user);
if (result <= 0) {
status.setRollbackOnly();
throw new RuntimeException("新增用户失败");
}
log.info("新增用户成功,用户ID:{}", user.getId());
return user.getId();
} catch (Exception e) {
status.setRollbackOnly();
log.error("新增用户异常", e);
throw e;
}
});
}
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户实体
*/
@Override
public User getUserById(Long id) {
if (ObjectUtils.isEmpty(id)) {
throw new IllegalArgumentException("用户ID不能为空");
}
return userMapper.selectById(id);
}
/**
* 查询用户列表
* @return 用户列表
*/
@Override
public List<User> getUserList() {
List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>()
.orderByDesc(User::getCreateTime));
if (CollectionUtils.isEmpty(userList)) {
return Lists.newArrayList();
}
return userList;
}
/**
* 更新用户信息
* @param user 用户实体
* @return 更新结果
*/
@Override
public Boolean updateUser(User user) {
if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {
throw new IllegalArgumentException("用户信息或ID不能为空");
}
return transactionTemplate.execute(status -> {
try {
int result = userMapper.updateById(user);
if (result <= 0) {
status.setRollbackOnly();
log.warn("更新用户失败,用户ID:{}", user.getId());
return Boolean.FALSE;
}
log.info("更新用户成功,用户ID:{}", user.getId());
return Boolean.TRUE;
} catch (Exception e) {
status.setRollbackOnly();
log.error("更新用户异常", e);
throw e;
}
});
}
/**
* 根据ID删除用户
* @param id 用户ID
* @return 删除结果
*/
@Override
public Boolean deleteUserById(Long id) {
if (ObjectUtils.isEmpty(id)) {
throw new IllegalArgumentException("用户ID不能为空");
}
return transactionTemplate.execute(status -> {
try {
int result = userMapper.deleteById(id);
if (result <= 0) {
status.setRollbackOnly();
log.warn("删除用户失败,用户ID:{}", id);
return Boolean.FALSE;
}
log.info("删除用户成功,用户ID:{}", id);
return Boolean.TRUE;
} catch (Exception e) {
status.setRollbackOnly();
log.error("删除用户异常", e);
throw e;
}
});
}
}
Controller层
package com.jam.demo.controller;
import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户控制器
* @author ken
*/
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户信息增删改查接口")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/add")
@Operation(summary = "新增用户", description = "创建新的用户信息")
public Long addUser(@RequestBody User user) {
return userService.addUser(user);
}
@GetMapping("/{id}")
@Operation(summary = "查询用户", description = "根据用户ID查询用户详情")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/list")
@Operation(summary = "用户列表", description = "查询所有用户信息列表")
public List<User> getUserList() {
return userService.getUserList();
}
@PutMapping("/update")
@Operation(summary = "更新用户", description = "更新用户的信息")
public Boolean updateUser(@RequestBody User user) {
return userService.updateUser(user);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "根据用户ID删除用户信息")
public Boolean deleteUserById(@PathVariable Long id) {
return userService.deleteUserById(id);
}
}
启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class DemoDatasourcePoolApplication {
public static void main(String[] args) {
SpringApplication.run(DemoDatasourcePoolApplication.class, args);
}
}
6.6 自定义Druid Filter扩展示例
package com.jam.demo.filter;
import com.alibaba.druid.filter.FilterEventAdapter;
import com.alibaba.druid.proxy.jdbc.ConnectionProxy;
import com.alibaba.druid.proxy.jdbc.PreparedStatementProxy;
import lombok.extern.slf4j.Slf4j;
import java.sql.SQLException;
/**
* 自定义SQL执行日志Filter
* @author ken
*/
@Slf4j
public class SqlExecuteLogFilter extends FilterEventAdapter {
@Override
protected void preparedStatement_executeAfter(PreparedStatementProxy statement, boolean firstResult, SQLException error) {
super.preparedStatement_executeAfter(statement, firstResult, error);
String sql = statement.getSql();
long executeTime = statement.getLastExecuteTimeNano() / 1000000;
if (error != null) {
log.error("SQL执行异常,SQL:{},异常信息:{}", sql, error.getMessage());
return;
}
log.info("SQL执行完成,执行耗时:{}ms,SQL:{}", executeTime, sql);
}
@Override
public void connection_closeAfter(ConnectionProxy connection, SQLException error) {
super.connection_closeAfter(connection, error);
log.debug("连接归还到连接池,连接ID:{}", connection.getId());
}
}
七、高并发场景常见踩坑与避坑指南
- 连接数设置过大导致数据库性能崩溃这是最常见的踩坑场景,很多开发人员为了应对高并发,将最大连接数设置为几百甚至上千,导致数据库线程数过多,CPU大部分时间消耗在线程上下文切换上,SQL执行效率急剧下降,最终数据库响应超时,服务雪崩。 避坑方案:严格按照最优连接数公式设置初始值,通过压测微调,绝大多数场景下最大连接数不超过50,同时保证应用连接池的最大连接数总和小于MySQL的
max_connections配置。 - 连接最大生命周期超过数据库超时时间数据库会对长时间空闲的连接主动断开,比如MySQL的
wait_timeout默认是8小时,如果连接池的连接最大生命周期设置超过这个值,会导致应用拿到已经被数据库断开的无效连接,执行SQL时抛出No operations allowed after connection closed异常。 避坑方案:将连接池的最大生命周期设置为比数据库的wait_timeout小30秒以上,MySQL场景下推荐设置为25分钟。 - 长事务导致连接池耗尽长事务会长期占用数据库连接,期间连接无法被其他请求复用,当并发量上来后,连接池内的连接会被快速耗尽,所有新的请求都会阻塞等待连接,最终导致服务响应超时。 避坑方案:严格控制事务的执行时间,避免在事务中执行远程调用、文件IO等耗时操作,将大事务拆分为多个小事务,同时开启连接泄露检测,识别长事务与未释放的连接。
- 开启testOnBorrow导致性能急剧下降
testOnBorrow参数会在每次获取连接时都执行一次校验SQL,相当于每次业务请求都多了一次数据库交互,在高并发场景下会带来极大的性能损耗,响应时间成倍增加。 避坑方案:高并发场景下必须关闭testOnBorrow,使用testWhileIdle配合后台回收线程做连接有效性校验,JDBC4及以上驱动使用原生的isValid()方法做校验,无需设置校验SQL。 - 连接泄露导致连接池被打满连接泄露是指应用从连接池获取连接后,使用完毕没有归还给池,比如事务没有提交/回滚、没有在finally中关闭连接、异常场景下未释放连接等,最终导致连接池内的空闲连接耗尽,服务不可用。 避坑方案:开启连接池的连接泄露检测功能,设置合理的超时时间,打印泄露连接的堆栈信息,快速定位问题代码;使用try-with-resources语法管理连接,确保连接一定会被释放;使用编程式事务,保证异常场景下事务一定会回滚,连接被释放。
- Druid的WallFilter配置不当导致业务异常WallFilter的SQL防火墙功能如果配置过严,会拦截正常的业务SQL,比如禁止了多表联查、子查询等业务需要的语法,导致业务执行报错;如果配置过松,又起不到安全防护的作用。 避坑方案:根据业务场景配置WallFilter的规则,先开启日志模式,不直接拦截,观察所有业务SQL的执行情况,调整规则后再开启拦截模式,避免影响正常业务。
- 最小空闲连接数设置过小导致高并发下响应延迟如果最小空闲连接数设置远小于最大连接数,当流量突增时,连接池需要动态创建新的连接,而连接的创建是重量级操作,会导致请求响应延迟增加,甚至超时。 避坑方案:高并发场景下,将最小空闲连接数与最大连接数设置为相等,将连接池变为固定大小,应用启动时就预创建所有连接,避免高并发下动态创建连接的开销。
八、总结
HikariCP与Druid两款连接池,分别代表了连接池产品的两个极致方向:HikariCP专注于极致的性能与稳定性,凭借无锁设计与深度优化,成为了SpringBoot官方默认的连接池,完美适配追求极致性能的微服务与云原生场景;Druid则聚焦于企业级一站式解决方案,凭借完善的监控体系、SQL防火墙与灵活的扩展能力,成为了国内企业级业务场景的首选。
连接池调优的核心,从来不是盲目调大参数,而是回归本质:理解连接池的底层原理,结合业务的实际场景与压测数据,设置合理的参数,同时优化业务代码,避免长事务、慢SQL、连接泄露等问题,才能充分发挥连接池的性能,保证系统在高并发场景下的稳定运行。