连接超时、语句超时与事务超时如何配合——在线接口的超时治理实战
文章目录每日一句正能量前言1. 背景与问题1.1 连接超时1.2 语句超时1.3 事务超时2. 环境与数据3. 复现过程3.1 复现连接池等待超时3.2 复现 SQL 超时3.3 复现事务超时4. 方案实施4.1 先建立超时配置矩阵4.2 HikariCP连接池超时配置4.3 JDBC 驱动 connectTimeout4.4 JDBC单条语句超时4.5 MyBatis超时配置4.6 JPA / Hibernate 适配4.7 Spring 事务超时4.8 异常与事务边界不要吞超时异常4.9 记录哪一种超时真正发生4.10 时间线必须可观测5. 结果对比实施前实施后6. 风险与复盘6.1 超时不是越短越好6.2 超时值不能倒挂6.3 事务里不要夹远程调用6.4 SQL 超时不代表数据库立即停止6.5 重试必须配合幂等结语每日一句正能量风柔草木舒心静天地宽春赏花开成海冬观雪落如诗夏听蝉鸣入梦秋藏明月满怀。四季皆赠你温柔时光皆许你从容。前言很多在线接口的数据库故障并不是“没有配置超时”而是所有地方都配置了超时却彼此打架。一个常见现场是这样的HTTP 接口超时 3 秒数据库连接池超时 3 秒SQL 超时 3 秒事务超时也是 3 秒。表面上看每层都有保护实际上谁先触发完全取决于当时系统状态。连接池拥塞时请求可能 3 秒都耗在拿连接SQL 锁等待时接口先被上游取消但数据库语句还在执行事务里夹着远程调用时单条 SQL 都很快整个事务却拖到十几秒。因此连接超时、语句超时和事务超时不能独立设计。它们应该形成一套有内外层次、有异常边界、有回滚策略的治理矩阵。1. 背景与问题先明确三个概念。1.1 连接超时连接超时通常有两层。第一层是连接池等待超时例如 HikariCP 的spring:datasource:hikari:connection-timeout:300它控制的是业务线程最多等多久才能从连接池拿到一个连接。第二层是 JDBC 驱动建立网络连接的超时例如 MySQLconnectTimeout1000它控制的是驱动连接数据库地址时网络建连最多允许多久。这两个超时经常被混为一谈。实际上一个发生在“池里没连接可借”另一个发生在“驱动正在建新连接”。1.2 语句超时语句超时针对一条 SQL例如 JDBCstatement.setQueryTimeout(1);单位通常是秒。它的核心目标是限制SELECT / UPDATE / INSERT / DELETE单条语句的最大执行时间。这类超时尤其适合防止锁等待执行计划劣化全表扫描SQL 意外放大大结果集拖垮线程。1.3 事务超时事务超时约束的是一组操作而不是一条 SQL。例如 SpringTransactional(timeout2)publicvoidplaceOrder(){...}它保护的是完整事务生命周期BEGIN SQL-1 业务逻辑 SQL-2 远程调用 SQL-3 COMMIT / ROLLBACK因此单条 SQL 可能都只执行 100 ms但事务仍然可能超过 2 秒。工程上最重要的不是三个值分别是多少而是它们之间的层级关系。通常建议连接池等待超时 SQL 超时 事务超时 HTTP 接口总超时这不是绝对公式但它能让故障优先在更靠近根因的层级暴露。2. 环境与数据本文示例使用JDK 21 Spring Boot 3.3 MySQL 8.0 HikariCP Spring JDBC MyBatis 3.x Hibernate 6 / JPA测试表CREATETABLEinventory(idBIGINTPRIMARYKEY,skuVARCHAR(64)NOTNULL,availableINTNOTNULL,versionINTNOTNULLDEFAULT0,updated_atTIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP);INSERTINTOinventory(id,sku,available)VALUES(1,SKU-001,100),(2,SKU-002,100);再建立订单表CREATETABLEorders(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(64)NOTNULLUNIQUE,skuVARCHAR(64)NOTNULL,quantityINTNOTNULL,statusVARCHAR(32)NOTNULL,created_atTIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP);在线接口模拟POST /orders逻辑为扣库存 - 写订单 - 提交事务3. 复现过程3.1 复现连接池等待超时先把连接池调小spring:datasource:hikari:maximum-pool-size:2minimum-idle:2connection-timeout:300构造一个故意占用连接的接口RestControllerRequiredArgsConstructorpublicclassDebugController{privatefinalDataSourcedataSource;GetMapping(/debug/hold-connection)publicStringholdConnection()throwsException{try(ConnectionconnectiondataSource.getConnection()){Thread.sleep(5000);returnok;}}}并发调用三次。前两个请求各占一个连接第三个请求等待 300 ms 后失败。常见异常类似java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 300ms这里数据库可能完全健康只是连接池被占满。如果没有单独记录poolWaitMs开发人员很容易误以为是 SQL 慢。3.2 复现 SQL 超时MySQL 可以使用SELECTSLEEP(5);JDBC 测试GetMapping(/debug/query-timeout)publicStringqueryTimeout()throwsException{try(ConnectioncdataSource.getConnection();PreparedStatementpsc.prepareStatement(SELECT SLEEP(5))){ps.setQueryTimeout(1);try(ResultSetrsps.executeQuery()){returnunexpected;}}}理论上 1 秒左右即可触发查询超时。异常通常会表现为 JDBC 驱动抛出的超时异常例如java.sql.SQLTimeoutException或驱动自己的异常子类。重要的是语句失败并不自动等于整个业务事务一定已经回滚。如果这条 SQL 在事务中执行接下来该不该继续取决于异常是否向上抛出、事务管理器是否把事务标记为 rollback-only以及业务代码有没有错误地吞掉异常。3.3 复现事务超时代码ServiceRequiredArgsConstructorpublicclassOrderService{privatefinalJdbcTemplatejdbcTemplate;Transactional(timeout2)publicvoidcreateOrder(StringorderNo)throwsInterruptedException{jdbcTemplate.update( UPDATE inventory SET available available - 1 WHERE id 1 AND available 0 );Thread.sleep(2500);jdbcTemplate.update( INSERT INTO orders(order_no, sku, quantity, status) VALUES (?, SKU-001, 1, CREATED) ,orderNo);}}这里第一条 SQL 很快真正拖慢事务的是Thread.sleep(2500)。因此事务超时不是“SQL 超时的另一种写法”它负责识别事务生命周期过长而不是某条 SQL 执行过长4. 方案实施4.1 先建立超时配置矩阵一个在线接口可以先从下面的层次开始连接池等待超时300 ms 驱动 connectTimeout1 s SQL 超时800 ms 事务超时2 s HTTP 接口总超时3 s这些数值仅用于演示不应直接复制到生产。生产上至少要参考接口 P95 / P99 数据库 P95 / P99 连接池大小 平均事务 SQL 数量 锁等待特征 上游调用 SLA 峰值并发核心思想是越靠近底层资源超时越早暴露越靠近业务边界允许的预算越大。4.2 HikariCP连接池超时配置推荐spring:datasource:hikari:maximum-pool-size:20minimum-idle:5connection-timeout:300validation-timeout:500idle-timeout:600000max-lifetime:1800000这里最关键的是connection-timeout它不是 SQL 执行超时。当连接池耗尽时线程最多等待 300 ms。如果把这个值设成 5 秒甚至 30 秒会产生一种危险现象数据库已经拥塞 - 请求继续堆在连接池 - Web 线程被占满 - 上游继续重试 - 雪崩在线接口通常更适合快速失败。4.3 JDBC 驱动 connectTimeoutMySQL JDBC URL 可以配置spring:datasource:url:jdbc:mysql://127.0.0.1:3306/demo ?connectTimeout1000 socketTimeout2000注意两个参数含义不同。connectTimeout用于 TCP 建连阶段。socketTimeout通常约束驱动等待服务器网络响应的时间。不要把socketTimeout当作精确 SQL 超时的唯一手段因为它更靠近网络 I/O 层粒度没有Statement.setQueryTimeout()那么清晰。4.4 JDBC单条语句超时最直接的方式publicintupdateStock(longid)throwsSQLException{try(ConnectioncdataSource.getConnection();PreparedStatementpsc.prepareStatement( UPDATE inventory SET available available - 1 WHERE id ? AND available 0 )){ps.setLong(1,id);ps.setQueryTimeout(1);returnps.executeUpdate();}}如果项目使用JdbcTemplate可以配置BeanJdbcTemplatejdbcTemplate(DataSourcedataSource){JdbcTemplatetemplatenewJdbcTemplate(dataSource);template.setQueryTimeout(1);returntemplate;}这会影响通过该模板执行的 SQL。如果业务里有“普通 SQL”和“报表 SQL”两种 SLA更建议创建不同模板BeanJdbcTemplateonlineJdbcTemplate(DataSourceds){JdbcTemplatetnewJdbcTemplate(ds);t.setQueryTimeout(1);returnt;}BeanJdbcTemplatereportJdbcTemplate(DataSourceds){JdbcTemplatetnewJdbcTemplate(ds);t.setQueryTimeout(10);returnt;}不要为了一个慢报表把所有在线 SQL 的超时都拉长。4.5 MyBatis超时配置MyBatis XML 可以直接配置selectidfindInventoryresultTypecom.demo.Inventorytimeout1SELECT id, sku, available, version FROM inventory WHERE id #{id}/select也可以设置默认值mybatis:configuration:default-statement-timeout:1推荐原则默认值兜底 关键 SQL 单独覆盖例如后台报表可以设为timeout10而在线库存扣减仍维持 1 秒。4.6 JPA / Hibernate 适配JPA 查询可以设置 HintTypedQueryOrderEntityqueryentityManager.createQuery( select o from OrderEntity o where o.orderNo :orderNo ,OrderEntity.class);query.setParameter(orderNo,orderNo);query.setHint(jakarta.persistence.query.timeout,1000);这里常见单位为毫秒但不同 Provider 的具体行为要以实现和驱动为准。Hibernate 也可以query.unwrap(org.hibernate.query.Query.class).setTimeout(1);setTimeout(1)通常表示秒。项目里最忌讳的是JPA Hint 1000 Hibernate timeout 1000 JDBC queryTimeout 1000因为单位可能完全不同。必须在代码评审规范里明确写清哪个 API 的单位是秒 哪个 API 的单位是毫秒4.7 Spring 事务超时SpringTransactional(timeout2)publicvoidcreateOrder(...){...}也可以在事务模板中配置TransactionTemplatetemplatenewTransactionTemplate(transactionManager);template.setTimeout(2);template.execute(status-{// business logicreturnnull;});事务超时最大的价值是防止长事务特别是事务里调用 HTTP 事务里调用 RPC 事务里等待 MQ 事务里执行复杂计算这些操作本身可能与数据库无关但它们会延长连接与锁的占用时间。4.8 异常与事务边界不要吞超时异常这是线上最容易写错的地方。错误示例Transactional(timeout2)publicvoidcreateOrder(){try{jdbcTemplate.queryForObject(SELECT SLEEP(5),Integer.class);}catch(Exceptione){log.warn(sql timeout, ignore,e);}jdbcTemplate.update( INSERT INTO orders(order_no, sku, quantity, status) VALUES (O-1001, SKU-001, 1, CREATED) );}如果异常被吞掉事务语义会变得难以预测。更安全的做法是Transactional(timeout2)publicvoidcreateOrder(){try{executeBusinessSql();}catch(DataAccessExceptione){log.error(database operation failed,e);throwe;}}让事务管理器看到异常。如果业务确实需要捕获异常也应显式决定事务状态catch(DataAccessExceptione){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();thrownewOrderCreateException(订单创建失败,e);}4.9 记录哪一种超时真正发生结构化日志推荐{event:db_timeout,traceId:2c34c4b8b83f4f6e,txId:8a921f32,timeoutType:STATEMENT,poolWaitMs:8,sqlDurationMs:1001,transactionDurationMs:1240,sqlState:HY000,exception:java.sql.SQLTimeoutException}可以统一枚举publicenumTimeoutType{CONNECTION_POOL,CONNECT,SOCKET,STATEMENT,TRANSACTION,HTTP}并在异常处理器中分类publicTimeoutTypeclassify(Throwablee){if(einstanceofSQLTimeoutException){returnTimeoutType.STATEMENT;}if(einstanceofSQLTransientConnectionException){returnTimeoutType.CONNECTION_POOL;}if(einstanceofTransactionTimedOutException){returnTimeoutType.TRANSACTION;}returnnull;}具体项目还应结合驱动异常层次补充分类。4.10 时间线必须可观测一次接口如果能还原成0 ms 请求进入 80 ms 获取连接完成 200 ms SQL-1 完成 700 ms 远程调用结束 1600 ms SQL-2 超时 1650 ms 事务标记回滚 1700 ms HTTP 返回开发人员就能快速判断连接池不是瓶颈 SQL-1 正常 远程调用占用事务时间过长 SQL-2 触发语句超时 事务随后回滚5. 结果对比实施前日志ERROR createOrder failed Request processing failedAPM 显示接口耗时3004 ms问题是开发人员无法判断这 3 秒花在哪里。实施后结构化日志{traceId:97cb19c9,poolWaitMs:12,sqlDurationMs:801,transactionDurationMs:1102,timeoutType:STATEMENT,sqlTemplate:UPDATE inventory SET available available - 1 WHERE id ?,txId:3d91f14a}排障可以直接进入决策树接口超时 ├─ poolWaitMs 接近 connectionTimeout │ └─ 查连接池、长事务、连接泄漏 ├─ SQL 触发 statement timeout │ └─ 查执行计划、锁等待、索引 ├─ txDurationMs 接近 transaction timeout │ └─ 查事务内远程调用和过多 SQL └─ HTTP 先超时数据库还在执行 └─ 检查超时层级是否倒挂这比只看一个“请求 3 秒超时”有价值得多。6. 风险与复盘6.1 超时不是越短越好过短的超时会把正常的尾部延迟误判为故障。生产设置应至少观察P50 P95 P99 最大值 锁等待分布 连接池等待分布如果 SQL P99 是 420 ms把语句超时设成 300 ms等于主动制造失败。6.2 超时值不能倒挂危险配置HTTP1 s SQL5 s 事务10 s上游 1 秒就放弃请求但数据库可能继续执行 4 秒甚至更久。在高并发下这会形成客户端认为失败 - 自动重试 - 原 SQL 仍运行 - 新请求再次执行 - 数据库压力继续上升因此在线接口通常应该让内层超时先于外层超时触发。6.3 事务里不要夹远程调用典型坏味道Transactionalpublicvoidpay(){updateOrder();callRemotePayment();updatePaymentResult();}远程支付如果耗时 2 秒数据库连接和事务可能被白白占用 2 秒。更合理的是缩小事务事务 1落订单状态 远程调用事务外 事务 2更新支付结果必要时使用状态机、Outbox 或可靠消息保证最终一致性。6.4 SQL 超时不代表数据库立即停止即使客户端发出取消也不能假设数据库端一定瞬间释放所有资源。不同数据库和驱动对取消语义实现不同因此出现大量 SQL 超时时还应检查数据库活动会话 锁等待 正在运行的 SQL 连接状态6.5 重试必须配合幂等超时后最危险的做法是无脑重试。因为客户端看到超时并不总能证明数据库操作没有成功。例如INSERT 已提交 - 网络响应丢失 - 客户端超时 - 客户端重试如果没有唯一键或幂等号就可能重复下单。在线接口建议超时 重试必须同时设计幂等键 唯一约束 重试次数 退避策略 异常白名单结语连接超时、语句超时和事务超时不是三个孤立参数而是一套从资源层到业务层的保护链。可以把它们理解成连接超时我能不能及时拿到数据库资源 语句超时这一条 SQL 能不能及时完成 事务超时这一组业务操作能不能及时完成对于在线接口建议优先建立连接池等待超时 SQL 超时 事务超时 HTTP 总超时并把poolWaitMs、sqlDurationMs、transactionDurationMs、timeoutType、traceId、txId同时写入可观测日志。真正成熟的超时治理不是让系统“更容易超时”而是让故障在最接近根因的位置快速失败并且让异常能够准确地触发回滚、降级、告警和重试策略。转载自https://blog.csdn.net/u014727709/article/details/165241209欢迎 点赞✍评论⭐收藏欢迎指正