先抛个结论:数据库性能出问题,大约只有三成是数据库本身扛不住,剩下七成得从程序那边找原因。同样的SQL,有人写出来能打到每秒几千次,有人写出来不到一百次就把连接池拖垮,差别就在“操作方式”上。
这是数据库性能优化系列的第三篇,前两篇聊了SQL改写和配置层面的调优手段,这一篇专门针对程序侧的优化。适合谁看?后端开发、接口负责人、维护老系统的同学,以及那些“SQL看着没问题但服务就是慢”的疑难杂症排查者。核心解决这些问题:连接数被打满、每秒钟大量重复请求、事务越开越长、结果集越查越大、代码里循环访问数据库。这些不是调数据库能解决的,必须改代码。
1. 程序操作优化到底在优化什么
先说思路。程序操作数据库的本质,就是“客户端发出一堆指令,数据库执行并返回结果”。这个链路里,真正消耗时间的其实不只是SQL执行本身,还包括:网络往返、驱动处理、连接获取、并发排队、锁等待、GC停顿,甚至代码里一条无关紧要的日志打印。
所以程序操作优化的目标,不是把某一条SQL改得飞快,而是把“程序与数据库之间的交互模式”调整到最优,把数据库的资源和时间花在真正需要的地方。
1.1 最常见的程序访问数据库坏味道
列出我这些年排查线上问题时,在代码里反复看到的几类典型问题,占了数据库性能故障的大头:
- 获取连接后不放回,或者未使用连接池而每次新建连接。
- 在for循环里逐条执行SQL,一条业务数据就把数据库往返次数拉到几千次。
- 查出来的数据量动辄几万行,代码层不做分页、不限制条数。
- 事务内混入大量非数据库操作,比如调外部接口、等待锁、文件读写。
- SQL逻辑本身没问题,但同一个查询在短时间内被重复执行几百遍,且没有缓存。
这些问题有一个共同特点:单看代码,每一行好像都没问题,但叠加到数据库上就是灾难。程序侧的优化,就是去解决这些交互层面的“放大效应”。
1.2 用资源预算思维看待程序与数据库的交互
举个例子,一个接口拉取数据后,每行调用一个字段翻译服务,最坏情况导致每条主数据额外产生几十次子查询,这在资源消耗上的放大是乘数级的。数据库资源有限,连接数、IO、CPU、内存就那么多,程序每次无谓的交互,都是在抢占其他请求的资源,而不是“多花一点时间”这么简单。
你可以把数据库连接想象成一座大桥的车道。车流量正常时每条车道都好走;一旦某几辆车走得很慢又占着车道不放,后面所有车都得排队。程序操作优化的本质就是:减少占道车辆、缩短占用时间、让车流更有序。后面聊的内容都会围绕降低连接占用、减少访问次数、缩短事务与锁持有时间、降低无效压力这四条展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接管理:从连接池参数到连接泄漏排查
连接是数据库操作的第一步,也是最容易被忽视的环节。很多线上故障表现为“连接池耗尽”“Too many connections”,但根因往往不是数据库连接数配置太小,而是程序侧获取连接后没有释放、连接池参数严重不合理、或者单个请求持有多余连接。
2.1 程序不应绕过连接池
我曾经接手过一个老项目,测试环境一切正常,一到生产就间歇性报连接失败。翻代码后发现,有个定时任务为了“提升性能”自己写了个连接管理器,每次任务新建物理连接,结束后又没真正关闭,最终把数据库连接数打满。
数据库物理连接的创建成本远比想象中高。一次TCP三次握手走完,数据库要完成线程创建、认证、权限检查、环境变量初始化等步骤。在MySQL 8.0和Oracle里,创建连接的开销通常能达到几十毫秒到上百毫秒。高并发下大量新建短连接,不仅响应变慢,还会让数据库CPU和上下文切换急剧上升。
所以第一原则:程序必须走连接池,如HikariCP、Druid、Tomcat JDBC Pool,并且确保从池中获取的连接最终都在finally或try-with-resources里释放。
java复制// 正确用法:try-with-resources 保证连接关闭
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// 处理逻辑
}
}
} // 连接自动归还连接池
有些框架封装的DAO基类里容易漏掉这个细节,排查时可以先确认各数据访问层的连接是否都遵循了这个模式。
2.2 连接池参数不能拍脑袋
连接池大小并非越大越好。很多刚入行的同学以为把maximumPoolSize调大就是性能优化,实际上如果设置过大,数据库要维护大量空闲连接,反而占用内存和线程资源。连接池应尽量设置小而足够的数量。
HikariCP官方给过一个经验结论:连接数 = TPS × 单连接处理耗时(秒)。比如目标TPS是100,每条SQL平均耗时20ms,那么连接数约等于100×0.02=2,再留一定余量设置成5-10就够了。很多系统的真实瓶颈根本不是连接不够,而是单条SQL太慢,或者业务在循环里发SQL,导致连接被长时间占用。
推荐的HikariCP配置:
yaml复制spring:
datasource:
hikari:
