<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>JDBC on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/jdbc/</link>
        <description>Recent content in JDBC on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Mon, 27 Jul 2026 07:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/jdbc/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>一次数据库连接池耗尽导致接口大面积超时的排查记录</title>
            <link>https://blog.5772447.xyz/posts/82574309/</link>
            <pubDate>Mon, 27 Jul 2026 07:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/82574309/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;我们有一个基于 Spring Boot 的订单服务，后端接的是一套单实例 MySQL 8.0，应用层用 HikariCP 做连接池，&lt;code&gt;maximumPoolSize&lt;/code&gt; 沿用了脚手架里的默认值 10。这个服务平时 QPS 不高，几百上下，跑了一年多都相安无事。&lt;/p&gt;&#xA;&lt;p&gt;变化发生在一次营销活动上线之后：产品在下单成功页新增了一个「实时积分计算」的功能，这个功能会在下单事务里同步调用一个外部积分中心的 HTTP 接口。功能测试、压测都过了，谁也没多想。活动当天晚高峰，订单服务开始大面积超时,网关侧 5xx 陡增,监控群瞬间炸了。这篇记录一下这次从「接口超时」一路挖到「连接池被长事务攥死」的完整排查过程。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;现象非常典型,但一开始很容易把人带偏:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关侧订单相关接口 P99 从平时的 80ms 飙到 30s 直接超时,错误率一度超过 40%。&lt;/li&gt;&#xA;&lt;li&gt;应用日志里刷满同一条异常:&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;request timed out after 30000ms (total=10, active=10, idle=0, waiting=27)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;关键点:&lt;code&gt;total=10, active=10, idle=0, waiting=27&lt;/code&gt;——连接池 10 个连接全被占着(active),没有一个空闲(idle),还有 27 个线程在排队等连接。&lt;/li&gt;&#xA;&lt;li&gt;登录 MySQL 看,&lt;code&gt;show processlist&lt;/code&gt; 里的连接数并不多,CPU、内存、磁盘 IO 都不高,数据库本身「看起来很闲」。&lt;/li&gt;&#xA;&lt;li&gt;重启应用后能好几分钟,流量一上来又迅速复现,治标不治本。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最迷惑人的地方就在这里:数据库明明很闲,应用却一直喊「拿不到连接」。第一反应是不是连接池配小了,或者数据库连接被打满了,但 MySQL 侧的连接数据并不支持这个猜测。&lt;/p&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;h3 id=&#34;第一步确认瓶颈在连接池不在数据库&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%80%e6%ad%a5%e7%a1%ae%e8%ae%a4%e7%93%b6%e9%a2%88%e5%9c%a8%e8%bf%9e%e6%8e%a5%e6%b1%a0%e4%b8%8d%e5%9c%a8%e6%95%b0%e6%8d%ae%e5%ba%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第一步:确认瓶颈在连接池,不在数据库&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;先把方向定死。既然异常是 HikariCP 抛的 &lt;code&gt;Connection is not available&lt;/code&gt;,说明问题出在「应用向连接池借连接」这一步,而不是「连接池向 MySQL 建连接」。&lt;/p&gt;&#xA;&lt;p&gt;对照 MySQL 侧:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;show&lt;/span&gt; status &lt;span style=&#34;color:#66d9ef&#34;&gt;like&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Threads_connected&amp;#39;&lt;/span&gt;;   &lt;span style=&#34;color:#75715e&#34;&gt;-- 当前连接数,稳定在 12 左右,远没到 max_connections(500)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;show&lt;/span&gt; processlist;                        &lt;span style=&#34;color:#75715e&#34;&gt;-- 大部分连接是 Sleep,个别在跑,没有长时间 Query&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;数据库连接数很低、也没有堆积的慢 SQL,基本可以排除「数据库被打满」和「慢查询拖垮数据库」这两个方向。瓶颈就在应用这端的连接池:&lt;strong&gt;连接被借走后长时间不归还&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第二步开启-hikaricp-泄漏检测&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e5%bc%80%e5%90%af-hikaricp-%e6%b3%84%e6%bc%8f%e6%a3%80%e6%b5%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第二步:开启 HikariCP 泄漏检测&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;HikariCP 有个专门定位连接不归还的利器 &lt;code&gt;leakDetectionThreshold&lt;/code&gt;。临时把它调小到 5 秒,重启观察:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-properties&#34; data-lang=&#34;properties&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;spring.datasource.hikari.maximum-pool-size&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;10&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;spring.datasource.hikari.leak-detection-threshold&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;5000&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;流量一上来,日志立刻出现大量告警,并且直接打印出「借了连接却迟迟不还」的那根线程的完整堆栈:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;4&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;5&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;6&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@3f1a,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;stack trace follows&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  at o.a.o.service.OrderService.createOrder(OrderService.java:88)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  at o.a.o.client.PointClient.calcPoint(PointClient.java:42)   &amp;lt;-- 卡在这里&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  at okhttp3.RealCall.execute(...)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;堆栈非常清楚:连接是在 &lt;code&gt;createOrder&lt;/code&gt; 里借出去的,但线程当前卡在 &lt;code&gt;PointClient.calcPoint&lt;/code&gt; 的一个 HTTP 调用上。也就是说——&lt;strong&gt;数据库连接被借出后,线程跑去做一个耗时的外部 HTTP 调用,在整个 HTTP 往返期间,这个数据库连接一直被攥在手里空转,谁也用不了。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;第三步定位到长事务里嵌外部调用&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e4%b8%89%e6%ad%a5%e5%ae%9a%e4%bd%8d%e5%88%b0%e9%95%bf%e4%ba%8b%e5%8a%a1%e9%87%8c%e5%b5%8c%e5%a4%96%e9%83%a8%e8%b0%83%e7%94%a8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第三步:定位到长事务里嵌外部调用&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;回到代码,&lt;code&gt;createOrder&lt;/code&gt; 方法上有 &lt;code&gt;@Transactional&lt;/code&gt; 注解。方法体的逻辑简化后是这样:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;4&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;5&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;6&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;7&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Transactional&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;createOrder&lt;/span&gt;(OrderReq req) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    orderMapper.&lt;span style=&#34;color:#a6e22e&#34;&gt;insert&lt;/span&gt;(order);              &lt;span style=&#34;color:#75715e&#34;&gt;// 1. 写订单,借用连接、开启事务&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    stockMapper.&lt;span style=&#34;color:#a6e22e&#34;&gt;deduct&lt;/span&gt;(req.&lt;span style=&#34;color:#a6e22e&#34;&gt;getSkuId&lt;/span&gt;());     &lt;span style=&#34;color:#75715e&#34;&gt;// 2. 扣库存,复用同一连接&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; point &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pointClient.&lt;span style=&#34;color:#a6e22e&#34;&gt;calcPoint&lt;/span&gt;(req); &lt;span style=&#34;color:#75715e&#34;&gt;// 3. 同步 HTTP 调外部积分中心(新加的!)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    orderMapper.&lt;span style=&#34;color:#a6e22e&#34;&gt;updatePoint&lt;/span&gt;(order, point);  &lt;span style=&#34;color:#75715e&#34;&gt;// 4. 回写积分&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}                                           &lt;span style=&#34;color:#75715e&#34;&gt;//    事务提交,连接归还&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;问题一目了然:第 3 步的外部 HTTP 调用被放进了 &lt;code&gt;@Transactional&lt;/code&gt; 的事务边界里。&lt;strong&gt;只要事务没提交,这个连接就不会归还给连接池。&lt;/strong&gt; 而积分中心那个接口平时响应 50ms,活动当天自己也扛不住,响应劣化到 3~8 秒,极端时触发到调用方的 10 秒读超时。&lt;/p&gt;&#xA;&lt;p&gt;于是一笔下单事务从原来的几毫秒,被这个外部调用拖长到好几秒。晚高峰订单并发一上来:每笔下单都要占着一个连接等好几秒,10 个连接的池子瞬间被占满,后面的请求全部在 &lt;code&gt;waiting&lt;/code&gt; 里排队,直到 30 秒借连接超时,抛出 &lt;code&gt;Connection is not available&lt;/code&gt;。数据库侧因为这些连接大多在「等 HTTP 返回」而不是在跑 SQL,所以看起来一片安静——这正是它迷惑人的原因。&lt;/p&gt;&#xA;&lt;h3 id=&#34;第四步量化验证&#34;&gt;&lt;a href=&#34;#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e9%87%8f%e5%8c%96%e9%aa%8c%e8%af%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;第四步:量化验证&#xD;&#xA;&lt;/h3&gt;&lt;p&gt;用一个简单的估算印证判断。连接池能支撑的下单吞吐 ≈ &lt;code&gt;池大小 / 单笔事务持有连接的时长&lt;/code&gt;。正常时单笔事务约 5ms,10 个连接理论上能撑 &lt;code&gt;10 / 0.005 = 2000&lt;/code&gt; 笔/秒;而事务被 HTTP 拖到 5s 后,变成 &lt;code&gt;10 / 5 = 2&lt;/code&gt; 笔/秒——吞吐直接塌了三个数量级。晚高峰几百的下单并发,2 笔/秒的处理能力当然全线排队超时。数字对得上,根因坐实。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;分止血和根治两步走。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;止血(立刻恢复):&lt;/strong&gt; 把外部积分调用的超时从 10s 收紧到 800ms 并加熔断,让连接不至于被攥住太久;同时把 &lt;code&gt;maximumPoolSize&lt;/code&gt; 临时从 10 调到 30 争取缓冲。改完发布,超时率立刻从 40% 降到个位数,先把火压住。注意扩连接池只是缓冲,不解决根本问题——连接持有时长不降下来,池子再大也有被占满的一天。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;根治(把外部调用移出事务):&lt;/strong&gt; 核心原则是&lt;strong&gt;事务里只做数据库操作,绝不嵌入网络 IO / 外部调用等不可控的耗时操作&lt;/strong&gt;。把积分计算拆出事务边界:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&#xA;&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;10&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;11&#xA;&lt;/span&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;12&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;createOrder&lt;/span&gt;(OrderReq req) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 1. 事务只包住数据库操作,快进快出,连接毫秒级归还&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    orderService.&lt;span style=&#34;color:#a6e22e&#34;&gt;saveOrderInTx&lt;/span&gt;(req);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 2. 积分计算走事务之外,失败也不影响下单主流程&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    eventPublisher.&lt;span style=&#34;color:#a6e22e&#34;&gt;publish&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; OrderCreatedEvent(order.&lt;span style=&#34;color:#a6e22e&#34;&gt;getId&lt;/span&gt;()));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Transactional&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;saveOrderInTx&lt;/span&gt;(OrderReq req) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    orderMapper.&lt;span style=&#34;color:#a6e22e&#34;&gt;insert&lt;/span&gt;(order);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    stockMapper.&lt;span style=&#34;color:#a6e22e&#34;&gt;deduct&lt;/span&gt;(req.&lt;span style=&#34;color:#a6e22e&#34;&gt;getSkuId&lt;/span&gt;());&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;积分改为下单成功后通过异步事件(消息队列 / &lt;code&gt;@TransactionalEventListener&lt;/code&gt; after-commit)去算,即便积分中心慢或挂,也只是积分延迟到账,下单主链路不再被它拖累。改造后事务持有连接时长回落到毫秒级,连接池又恢复了平时几百 QPS 都用不满 10 个连接的从容状态。&lt;/p&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;根因是&lt;strong&gt;在数据库事务边界内嵌入了不可控的外部 HTTP 调用,导致数据库连接被长时间占用而非归还&lt;/strong&gt;,再叠加连接池偏小(10)缺乏缓冲,共同把「一个外部依赖变慢」放大成了「订单服务整体不可用」。&lt;/p&gt;&#xA;&lt;p&gt;更深一层看,这是一个「持有时长」而非「资源总量」的问题:连接池耗尽的表面是「连接不够用」,本质却是「每个连接被占用的时间太长」。凡是把网络 IO、外部 RPC、甚至 &lt;code&gt;Thread.sleep&lt;/code&gt; 放进事务里的写法,都会以类似方式放大故障——平时无感,一旦被嵌的依赖抖动,就会顺着连接池瞬间击穿整个服务。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;事务边界最小化&lt;/strong&gt;:&lt;code&gt;@Transactional&lt;/code&gt; 方法里只允许数据库操作,严禁嵌入 HTTP/RPC 调用、消息发送、大文件读写等耗时/不可控操作。代码评审把这一条列为红线。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;连接池配套监控与泄漏检测&lt;/strong&gt;:生产常态开启 &lt;code&gt;leak-detection-threshold&lt;/code&gt;(如 30s 只告警不影响功能),并把 HikariCP 的 &lt;code&gt;active/idle/pending&lt;/code&gt;、连接获取耗时接入监控与告警,连接排队时第一时间可见。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;连接池容量按公式估算&lt;/strong&gt;:参考 &lt;code&gt;池大小 ≈ 峰值 QPS × 单事务平均持有时长&lt;/code&gt;,结合数据库 &lt;code&gt;max_connections&lt;/code&gt; 反推,不要沿用脚手架默认值拍脑袋。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;外部依赖强制超时 + 熔断&lt;/strong&gt;:所有对外调用必须设置合理的连接/读超时并配熔断降级,杜绝「一个下游慢,拖垮全站」。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;压测覆盖依赖劣化场景&lt;/strong&gt;:压测不能只测正常路径,要专门注入「外部接口变慢/超时」的故障演练,提前暴露这类被放大的隐患。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;这次故障的表象是「数据库连接池耗尽、接口大面积超时」,但数据库本身全程很闲,真正的元凶是一次被塞进事务里的同步外部 HTTP 调用,把每笔下单事务从毫秒拖成数秒,让连接被长期占用、池子被瞬间占满。&lt;/p&gt;&#xA;&lt;p&gt;排查的关键转折点有三个:一是通过异常里的 &lt;code&gt;active=10, idle=0, waiting=27&lt;/code&gt; 判定瓶颈在连接池而非数据库;二是用 &lt;code&gt;leakDetectionThreshold&lt;/code&gt; 直接打印出「攥着连接不放」的线程堆栈;三是顺着堆栈定位到事务里的外部调用。记住一条朴素的经验:&lt;strong&gt;数据库事务是很贵的资源,连接必须快借快还;任何把网络 IO 关进事务的写法,都是在给自己埋雷。&lt;/strong&gt;&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
