ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 集成 Druid 连接 SQL Server 的配置避坑指南

Spring Boot 集成 Druid 连接 SQL Server 的配置避坑指南

年初在做某个企业数据平台的基础访问层改造时,遇到了一个很典型的组合场景:业务库跑在 SQL Server 上,应用层用的是 Spring Boot,连接池选型被团队定成了 Druid。这套组合放在平时不算冷门,但真正动手配置的时候还是踩了不少坑,尤其是从一个 MySQL 惯性思维切到 SQL Server 的人,最容易在一堆连接参数上栽跟头。

这篇内容就是把当时从依赖引入、连接配置、参数调优到问题排查的完整过程整理出来,重点讲清每个关键配置到底为什么这么写,以及 SQL Server 和 Druid 组合之下那些容易忽略的细节。无论你是在维护老系统迁移连接池,还是新项目第一次让 Druid 去接管 SQL Server,都可以直接参照这份配置落地,避免重复踩坑。

1. 项目背景与需求拆解

1.1 为什么最终选择了 Druid

先说结论:连接池的选型本质上是在选“连接生命周期管理策略”。在 Java 生态里,可选的成熟方案不少,但 Druid 能被大量项目选中,主要是它在连接池基础能力之外,自带了一整套可观测和拦截扩展机制。

这个项目里我们最看重的几个点正好都是 Druid 的强项:

  • 内置了监控面板,可以实时查看活跃连接数、空闲连接数、SQL 执行频率和慢查询日志,这在排障阶段帮助非常大。
  • 支持连接泄漏检测,借出去超过指定时间没归还的连接会被强制回收并记录日志,对定位业务代码里忘记 close 的问题很有用。
  • 自带 SQL 防火墙和参数化语句缓存,虽然项目里没开启全部拦截规则,但对一些历史遗留的拼接 SQL 能起到兜底作用。
  • Druid 的配置参数体系完整,几乎每个连接池行为都有对应的配置项,自定义空间充足。

当然,选择它也要承担一些隐性成本。比如版本升级时部分参数会有行为调整,还有社区文档里有些示例是直接抄的,遇到 SQL Server 这个非默认数据库时,很多配置都不能无脑照搬。这也是为什么我会建议“理解参数后再配”,而不是把一段 yml 丢进项目就跑。

1.2 SQL Server 连接场景的特殊之处

SQL Server 和 MySQL 在 JDBC 连接层面有几个显著差异,直接影响 Druid 的配置方式。

第一,驱动坐标不同。SQL Server 官方驱动在 Maven 仓库里的 artifactId 叫mssql-jdbc,不再推荐使用老的sqljdbc4。老的驱动包在 Java 8 以上环境里会频繁抛模块访问警告,而且长期不更新,对新的 SQL Server 版本兼容性也差。

第二,URL 格式不同。SQL Server 的 JDBC URL 是jdbc:sqlserver://主机:端口;参数名=参数值的风格,参数之间用分号分隔,而不是用?和&。很多从 MySQL 转过来的人,下意识写成jdbc:sqlserver://localhost:1433?encrypt=true,这种写法部分驱动能容忍,但参数不一定生效,容易留下隐患。

第三,认证方式更复杂。SQL Server 支持 SQL 认证和 Windows 集成认证两种模式。集成认证需要额外的本地库支持,配置复杂度高,在 Linux 容器环境下尤其麻烦。项目里建议优先走 SQL 认证,也就是在连接串中显式指定用户名和密码。

第四,SQL Server 默认的端口是 1433,但如果是命名实例,端口可能是动态分配的,URL 里可能要写主机名\\实例名而不是端口号。这一点在测试环境特别常见,Druid 初始化时如果端口解析失败,报错信息会非常迷惑。

2. 核心配置与技术要点

2.1 Spring Boot 集成 Druid 的最小依赖

如果你的项目是基于 Spring Boot 的,使用 Druid 官方提供的druid-spring-boot-starter是最省事的路径。它不仅自动配置了数据源,还把 Druid 的监控 Servlet 和过滤器一并接入了 Spring 容器。

<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>9.4.1.jre8</version> <scope>runtime</scope> </dependency>

这里有一个关键点:druid-spring-boot-starter会自动把spring.datasource.druid.*前缀下的配置映射到 Druid 数据源上。如果同时存在spring.datasource.url这种通用配置,要注意两者的优先级关系。实际项目里我会统一使用spring.datasource.druid.*前缀写全所有配置,避免混用导致某些参数没生效。

驱动版本上,mssql-jdbc分jre8和jre11两个变体,根据项目运行的 JDK 版本选择。JDK 8 项目如果硬用jre11变体,启动时可能会报 UnsupportedClassVersionError。反过来,JDK 11 项目用jre8变体虽然能跑,但部分新特性(比如某些 TLS 算法支持)会受限。

2.2 Druid 关键参数逐个拆解:不是照着抄就行

连接池配置最忌讳的就是把一堆参数抄过来,却不知道它们各自控制什么。下面这几个参数是连 SQL Server 时必须明确含义的。

initialSize:连接池启动时创建的初始连接数。设置成 5 是比较合理的默认值,原因是 Druid 在启动时会同步创建这批连接,如果你项目启动阶段还有大量其他逻辑要执行,初始连接数设太大会拖慢启动速度。但如果设成 0,又会让第一次请求承担建连的延迟,对 SQL Server 这种建连开销比较大的数据库来说,初始化 5 条连接是兼顾启动速度和首请求延迟的选择。

minIdle 和 maxActive:这两个值决定连接池的收缩和扩容边界。minIdle 是保底空闲连接数,当并发下降时,连接池会逐渐把空闲连接关闭到 minIdle 数量。maxActive 是最大活跃连接数,也就是池子最多能同时借出的连接数。对于内部管理型系统,maxActive 设 20 左右足够;对于高并发业务系统,需要根据压测结果调整。有一个很容易踩的误区是 maxActive 设得越大越好,实际上连接数超过数据库能同时处理的量后,反而会增加数据库端的线程调度开销和锁等待。

spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000

maxWait:获取连接的超时时间。这里单位是毫秒。当池内没有空闲连接且活跃连接数已经达到 maxActive 时,调用方会等待 maxWait 毫秒。如果超时仍然拿不到连接,会抛出CannotGetJdbcConnectionException。设 60000 的意思是高峰期让业务最多等 1 分钟,超过这个时间说明连接池配置或数据库性能存在问题,不应该继续无限等下去。

validationQuery:连接有效性检查的 SQL。这是和数据库种类强相关的一个配置。MySQL 常用SELECT 1,Oracle 常用SELECT 1 FROM DUAL,SQL Server 同样用SELECT 1就够了。要注意的是,不要把复杂的查询写在 validationQuery 里,因为当连接池做空闲检查或借用检查时,会频繁执行这个查询,太重的 SQL 反而成为池子本身的性能瓶颈。

这里特别强调一下testWhileIdle和testOnBorrow的取舍。连接池在归还连接后不会物理关闭,而是在池里搁置,时间久了可能被数据库端断开。testWhileIdle是后台线程在空闲连接闲置一定时间后做一次验证,不通过就丢弃,这是推荐开启的。testOnBorrow是每次借出前都验证,虽然更保险,但代价是频繁执行 validationQuery,吞吐会下降。对于 SQL Server,只要网络稳定,开启testWhileIdle并且把空闲时间阈值设置合理,就足够保障连接可用性了。

2.3 SQL Server 连接串中的隐藏坑

SQL Server 的连接串参数远比 MySQL 复杂,而且有些参数在特定驱动版本下行为不一致,需要谨慎对待。

encrypt 和 trustServerCertificate:这两个是 SQL Server 驱动 8.4 以后的默认行为变化重灾区。较新的mssql-jdbc驱动默认 encrypt=true,也就是强制 TLS 加密连接。如果你的 SQL Server 没有配置有效证书,驱动会因证书校验失败而直接拒绝连接。处理方式有两种:一是给 SQL Server 配置受信任的证书,二是在连接串里加trustServerCertificate=true跳过证书校验。内网测试环境可以直接跳过校验,但生产环境强烈建议走正规证书。

loginTimeout 和 socketTimeout:SQL Server 驱动默认的登录超时可能长达 15 秒甚至更久,配合 Druid 的 maxWait 会出现一个矛盾:Druid 等待连接超时报错,但底层连接仍在尝试建立,导致池内出现悬挂的连接。建议在 URL 里显式设置loginTimeout=5,让整个建连行为在可控时间内完成。socketTimeout 则是查询阶段读取数据的超时,具体值根据业务查询耗时分布来定,避免一个慢 SQL 把线程无限拖住。

实例名连接:如果 SQL Server 是命名实例,典型的 URL 长这样:

jdbc:sqlserver://192.168.1.100\\SQLEXPRESS;encrypt=false;loginTimeout=5

在 YAML 配置文件里,这个反斜杠需要注意转义问题,建议把整个 URL 用单引号包起来,否则 YAML 解析可能吞掉反斜杠。这是一个非常隐蔽的配置坑,报错时会提示无法解析主机名,但实际上只是 YAML 转义搞砸了。

3. 实操过程与核心实现

3.1 一份可直接落地的完整配置

到这里我们把上面所有拆解过的点整合成一份经过项目验证的配置。场景是一个内部业务系统,并发峰值大约在 50 个同时请求左右,数据库是 SQL Server 2019 标准版,运行在专有网络内。

spring: datasource: druid: url: 'jdbc:sqlserver://192.168.10.20:1433;encrypt=false;loginTimeout=5;socketTimeout=60000' username: app_user password: 'your_password_here' driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 600000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false keep-alive: true pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,slf4j connection-properties: socketTimeout=60000;lockTimeout=5000

逐项解释一下关键选择。

keep-alive设为 true,是让 Druid 的守护线程在连接空闲时向数据库发送一个轻量请求,避免数据库端因为 wait_timeout 或 TCP keepalive 设置把空闲连接回收掉。这个参数在 MySQL 场景下很常见,但 SQL Server 同样适用,尤其当数据库端存在连接空闲回收策略时。

validation-query用SELECT 1,在 SQL Server 2008 到 2019 的所有版本上都可以稳定执行,且开销极低。这里不建议用SELECT GETDATE(),虽然也能执行,但没必要多一次函数调用。

filters里配置了stat(监控统计)、wall(SQL 防火墙)、slf4j(日志输出)。需要注意,如果项目里同时引入了druid-spring-boot-starter,filters 的配置会被读取并注册为底层拦截器。但wall过滤器在某些旧项目里会导致 Druid 解析 SQL 缓存占用内存持续增长,所以如果业务 SQL 复杂且不确定是否合规,可以先只保留stat和slf4j。

3.2 从连接到监控:验证配置是否真正生效

配置完成后,第一步不是启动整个应用,而是写一个独立的小测试类验证连接串和驱动情况。

public static void main(String[] args) throws SQLException { String url = "jdbc:sqlserver://192.168.10.20:1433;encrypt=false;loginTimeout=5"; try (Connection conn = DriverManager.getConnection(url, "app_user", "password")) { try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@VERSION")) { if (rs.next()) { System.out.println(rs.getString(1)); } } } }

这段代码的作用是确认驱动本身能正常连通,排除掉配置管理类的问题。如果这里报错,后面在 Spring Boot 里排查就复杂了。这一步特别适合在 Docker 容器里调试时使用,因为可以直接在容器内执行,确认容器到数据库的网络通畅。

测试通过后,就可以启动 Spring Boot 应用,然后访问 Druid 的监控面板来观察连接池的初始化情况。

spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: monitor login-password: monitor_123 web-stat-filter: enabled: true url-pattern: /* exclusions: '*.js,*.css,/druid/*'

访问/druid/index.html后,重点看两个指标:一是连接池页面的活跃连接数曲线,二是 SQL 监控页里是否存在大量SELECT 1检查语句。如果活跃连接数长期维持在 maxActive 水平且没有下降趋势,说明连接可能没正确归还,需要结合下一节的问题排查手段来定位。

3.3 压测环节的参数调优纪实

为了验证参数设置是否合理,我在测试环境做了一轮简单的并发压测。模拟出一个业务场景:类似订单信息查询的接口,内部会执行两次 SQL 查询,中间有少量非数据库处理逻辑。线程数从 1 逐步增加到 60,观察连接池的行为变化。

当线程数在 20 以内时,连接池的活跃连接曲线和线程数基本线性关联,最大活跃连接数在 8 到 10 之间。当线程数提高到 50 时,活跃连接数冲破 15,但依然在 maxActive 20 的范围内,请求平均响应时间从 18ms 涨到了 65ms,这个涨幅主要是 CPU 上下文切换和等待连接导致的。

继续把线程数加到 80,问题开始显现:部分请求开始出现CannotGetJdbcConnectionException,Druid 日志中明确记录了等待超时。这时候观察数据库端,发现 SQL Server 的锁等待和阻塞数也在同步攀升。这说明瓶颈已经不在连接池,而在数据库本身的并发处理能力上。此时继续调大 maxActive 已经没有意义,反而会让更多请求堆积在数据库端。

这个经验让我养成了一个习惯:连接池参数必须结合数据库端的监控指标一起看,不能只盯着应用侧。Druid 的 stat 过滤器采集的数据结合数据库端的sys.dm_exec_requests动态管理视图,能更快定位瓶颈到底在池子、网络、还是 SQL 本身。

4. 常见问题与排查技巧实录

4.1 高频问题速查表

把整个实施过程里遇到过的、以及社区里被反复提问的 SQL Server 连接池问题整理成了下表,每一条都是实际案例验证过的。

现象根本原因解决方案
启动报com.microsoft.sqlserver.jdbc.SQLServerException: 无法打开登录所请求的数据库用户没有目标库的权限,或默认数据库不存在检查登录名的映射关系和默认数据库设置
连接串加了encrypt=false仍然提示 SSL 错误驱动版本高于 9.2,部分版本忽略该参数或默认加密显式加trustServerCertificate=true,或确认驱动版本
连接池初始化成功,但首次请求耗时超过 2 秒连接池启动时未初始化连接,首请求才去建连设置 initialSize 大于 0,配合连接池预热
Druid 监控页没有数据filters未启用stat,或 WebStatFilter 未配置确认 filters 包含 stat,并配置 web-stat-filter
空闲一段时间后出现Connection reset by peer数据库端或防火墙回收了空闲连接开启testWhileIdle和keepAlive
URL 中反斜杠在 YAML 里被解析丢失YAML 转义问题,命名实例连接失败用单引号包裹整个 URL,写双反斜杠\\
报错驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接TLS 算法或证书兼容性问题检查 JDK 版本和 SQL Server 的 TLS 配置,必要时加trustServerCertificate=true
高并发下 Druid 获取连接超时,但数据库端连接数不高maxActive 设置过小,或连接被慢 SQL 长时间占用结合慢查询日志调大 maxActive,并优化 SQL

这张表里的前四类问题是 SQL Server 场景特有的,后四类在 MySQL 场景里同样会出现。如果排查时没有头绪,我建议先按“驱动、网络、认证、连接池参数”四个维度去排查,别一上来就怀疑连接池。

4.2 深度排查案例一:驱动版本引发的间歇性断连

项目里的一个服务升级 JDK 后,每天早上第一波请求总是偶发超时,过了十分钟后一切恢复正常。排查过程是这样的。

首先看 Druid 监控面板,发现失败请求集中在上午 9 点到 9 点 15 分之间,SQL 都是同一个查询。看 SQL Server 端的错误日志,发现夜间有大量来自应用服务器的登录失败记录,错误信息是该登录名来自不受信任的域,不能与 Windows 身份验证一起使用。

这个报错的出现是因为应用配置用的是 SQL 认证,但驱动版本较老,在 handshake 阶段默认尝试使用 NTLM 协议进行 Windows 认证,失败后才回退到 SQL 认证。这个回退过程会消耗大量时间,而 Druid 初始化连接时对登录超时比较敏感,导致超时失败。

处理方案是升级驱动到新版,并显式在 URL 中加入authenticationScheme=NativeAuthentication或者直接使用 SQL 认证专用参数,彻底跳过 NTLM 的探测过程。升级后,早晨第一波请求的失败率从 12% 降到了 0。

4.3 深度排查案例二:连接池耗尽但不释放

另一个项目遇到的状况更隐蔽:应用毫无异常,但夜晚低峰期 Druid 监控显示活跃连接数始终徘徊在 18 到 20,接近 maxActive。第二天早晨第一波请求一来,直接大面积报连接获取超时。

先怀疑业务代码忘关连接,但排查了一圈,所有操作都在 try-with-resources 里。继续看 Druid 的线程栈,发现连接被一批执行异步任务的线程借走没有归还。深入检查后,定位到一个定时任务框架的 bug:任务执行失败后线程进入阻塞状态,但持有的连接在异常分支里没有被释放。

这个问题的本质是业务代码对异常分支的把握不到位,代码 review 没发现。Druid 的removeAbandoned参数可以用于兜底,它能在连接被借出超过指定时间后强制回收。

spring: datasource: druid: remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: true

但这里要提醒一点:removeAbandoned是一把双刃剑。如果业务本身有合法的长事务或长时间占用连接的场景,强制回收会中断正在执行的 SQL,带来更严重的数据一致性问题。我把它定位为“事后的应急手段”,开启后必须有日志监控和告警,追着日志把泄漏点修掉,再考虑关闭这个参数。

4.4 排查思路方法论小结

如果你遇到的报错不在上面的表格里,我推荐按以下顺序排查。

  1. 先用 JDBC 直连测试,绕过 Spring、绕过 Druid,确认最基本的驱动连通性。
  2. 确认 SQL Server 版本和驱动匹配关系,查看 SQL Server 错误日志,往往有最直接的线索。
  3. 关闭 Druid 的部分高级拦截器(如 wall),排除 SQL 过滤引起的干扰。
  4. 开启 Druid 的日志输出,把获取连接、归还连接的细节输出到独立日志文件。
  5. 最后才调整参数。每次只改一个参数,观察变化,别同时改多个参数后无法定位问题成因。

这套排查顺序我在至少三个项目里都验证过,逻辑简单但非常有效。尤其是第一步,很多人跳过了直接进 Spring Boot 里调参,结果花了半天时间才发现是驱动版本或者网络问题,这个教训值得记下来。

5. 连上之后还需要做的几件事

连接池真正跑起来只是开始,后续的稳定性保障才是重点。Druid 的好处在于它把很多监控能力内建了,但你得主动启用并用好它们。

建议至少完成以下三个动作。

动作一:配置连接池告警。Druid 的监控页数据只解决“事后看”的问题,要解决“事前知”,需要对接外部监控系统。最直接的方式是定时读取 Druid 的监控统计接口,把活跃连接数、等待次数、超时次数等指标推送到告警平台。我在项目里用简单的定时任务每分钟拉一次 Druid 数据,设置活跃连接数超过 maxActive 90% 的告警阈值,效果非常明显。

动作二:关注 SQL Server 端的连接池关联指标。连接池的行为最终都会体现到数据库端。SQL Server 的动态管理视图sys.dm_exec_sessions可以看每个会话的状态,sys.dm_exec_requests可以看正在执行的请求和等待类型。当 Druid 侧显示连接获取超时,但数据库端活跃会话数并不高时,问题大概率在网络层或在连接归还逻辑上。这种跨端对比分析,比单看任何一端都要准确。

动作三:固定连接池参数版本并纳入变更管理。Druid 和 mssql-jdbc 都在持续迭代,升级时一定要看 changelog。曾经遇到过一次从 1.2.6 升级到 1.2.11 后,默认参数行为不同导致连接池启动失败的情况。所以只要连接池运行稳定,不是升级到有重大安全修复的版本,就不要轻易动它。如果必须升级,先在测试环境完整跑一轮压测,压测时重点观察连接的创建和销毁时长,以及是否存在新的 SSL 相关告警。

最后再分享一个小技巧:如果你所在团队已经统一使用了配置中心,建议把 Druid 连接池的这些参数放在配置中心统一管理,而不是直接写死在应用的 yml 里。因为连接池参数和数据库负载、实例规格是强相关的,同一套代码在不同环境的配置应该有所差异。通过配置中心能快速调整,而不需要重新发布应用。实际上我后面接手的一个项目就是这么操作的,在一次数据库扩容后,直接调整 maxActive 和 keepAlive 相关配置,全程没有重启服务,非常省心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进