慢Sql不一定是数据库或者Sql本身的问题,有可能是网络链路的问题

本文主要分享一次由网络链路引起的慢Sql问题,慢Sql除了索引和Sql语句本身的问题,也有可能是由网络链路引起的。

1. 问题描述

  1. 近期生产服务器系统频繁报Sql超时和死锁的错误Lock wait timeout exceeded; try restarting transaction
  2. 根据错误日志定位,定位到一个关于用户金额更新的方法。
  3. 这个方法主要做了两件事情,开启事务,更新用户金额然后往记录表插入一条数据。
  4. 查询慢Sql日志,确认了是更新用户金额的update语句超时了。然后懵逼的就是这个Sql过滤条件是主键,explain这个update语句type是range级别的,没道理会超时。
  5. 再次查看日志,是多个事务并发,单次事务执行时间要好几秒,前面的行锁持有时间过长,导致后续的事务没获得锁超时了。

那么问题来了,一张数据量不算太大的表(20w以内),update语句在过滤条件为表主键的情况下,为什么还会超时?

2. 一次远程Sql执行的流程

2.1. 网络链路可能导致慢 SQL 的各个环节

1
2
3
应用服务器 → 网络链路 → 数据库服务器
↓           ↓           ↓
应用层       网络层       数据库层

2.2. 远程调用SQL的完整流程

sequenceDiagram
    participant Client as 客户端/浏览器
    participant App as 应用服务器
    participant Network as 网络链路
    participant DB as 数据库服务器

    title UPDATE语句的网络链路耗时分析

    %% 第一部分:请求进入
    Note over Client,DB: 阶段一:请求接收与事务开始
    Client->>App: HTTP POST/PUT请求
    App->>App: 业务验证与处理(15ms)
    App->>Network: 开始事务 BEGIN
    
    %% 第二部分:查询数据(网络往返1)
    Note over Client,DB: 阶段二:查询要更新的数据
    App->>Network: SELECT查询(查数据)
    Network->>DB: 查询请求(30ms延迟)
    DB->>DB: 执行查询(5ms)
    DB->>Network: 返回结果数据
    Network->>App: 数据返回(30ms延迟)
    
    %% 第三部分:业务逻辑处理
    App->>App: 业务计算与数据组装(10ms)
    
    %% 第四部分:执行UPDATE(网络往返2)
    Note over Client,DB: 阶段三:执行UPDATE操作
    App->>Network: UPDATE语句
    Network->>DB: UPDATE请求(30ms延迟)
    DB->>DB: 获取行锁(5ms)
    DB->>DB: 执行更新(8ms)
    DB->>Network: 返回影响行数
    Network->>App: 结果返回(30ms延迟)
    
    %% 第五部分:可能的额外操作
    Note over Client,DB: 阶段四:更新相关数据
    App->>Network: 更新关联表/计数器
    Network->>DB: 第二个UPDATE(30ms延迟)
    DB->>DB: 执行更新(3ms)
    DB->>Network: 返回结果
    Network->>App: 结果返回(30ms延迟)
    
    %% 第六部分:提交事务
    Note over Client,DB: 阶段五:事务提交
    App->>Network: COMMIT提交
    Network->>DB: 提交请求(30ms延迟)
    DB->>DB: 写入redo log(3ms)
    DB->>DB: 释放锁(2ms)
    DB->>Network: 提交完成
    Network->>App: 提交确认(30ms延迟)
    
    %% 第七部分:响应返回
    App->>App: 组装响应(5ms)
    App->>Client: 返回HTTP响应(200 OK)
    
    %% 第八部分:分析对比
    Note over App,DB: 耗时分析
    App->>DB: 理想情况(本地执行)
    DB-->>App: 总耗时:BEGIN(1ms)+SELECT(5ms)+UPDATE(13ms)+COMMIT(5ms)=24ms
    
    App->>DB: 实际情况(远程+网络延迟)
    DB-->>App: 总耗时:24ms + 网络(30ms×6往返×2)=24ms+360ms=384ms
sequenceDiagram
    participant Client as 客户端/浏览器
    participant App as 应用服务器
    participant Network as 网络链路
    participant DB as 数据库服务器

    title UPDATE语句的网络链路耗时分析

    %% 第一部分:请求进入
    Note over Client,DB: 阶段一:请求接收与事务开始
    Client->>App: HTTP POST/PUT请求
    App->>App: 业务验证与处理(15ms)
    App->>Network: 开始事务 BEGIN
    
    %% 第二部分:查询数据(网络往返1)
    Note over Client,DB: 阶段二:查询要更新的数据
    App->>Network: SELECT查询(查数据)
    Network->>DB: 查询请求(30ms延迟)
    DB->>DB: 执行查询(5ms)
    DB->>Network: 返回结果数据
    Network->>App: 数据返回(30ms延迟)
    
    %% 第三部分:业务逻辑处理
    App->>App: 业务计算与数据组装(10ms)
    
    %% 第四部分:执行UPDATE(网络往返2)
    Note over Client,DB: 阶段三:执行UPDATE操作
    App->>Network: UPDATE语句
    Network->>DB: UPDATE请求(30ms延迟)
    DB->>DB: 获取行锁(5ms)
    DB->>DB: 执行更新(8ms)
    DB->>Network: 返回影响行数
    Network->>App: 结果返回(30ms延迟)
    
    %% 第五部分:可能的额外操作
    Note over Client,DB: 阶段四:更新相关数据
    App->>Network: 更新关联表/计数器
    Network->>DB: 第二个UPDATE(30ms延迟)
    DB->>DB: 执行更新(3ms)
    DB->>Network: 返回结果
    Network->>App: 结果返回(30ms延迟)
    
    %% 第六部分:提交事务
    Note over Client,DB: 阶段五:事务提交
    App->>Network: COMMIT提交
    Network->>DB: 提交请求(30ms延迟)
    DB->>DB: 写入redo log(3ms)
    DB->>DB: 释放锁(2ms)
    DB->>Network: 提交完成
    Network->>App: 提交确认(30ms延迟)
    
    %% 第七部分:响应返回
    App->>App: 组装响应(5ms)
    App->>Client: 返回HTTP响应(200 OK)
    
    %% 第八部分:分析对比
    Note over App,DB: 耗时分析
    App->>DB: 理想情况(本地执行)
    DB-->>App: 总耗时:BEGIN(1ms)+SELECT(5ms)+UPDATE(13ms)+COMMIT(5ms)=24ms
    
    App->>DB: 实际情况(远程+网络延迟)
    DB-->>App: 总耗时:24ms + 网络(30ms×6往返×2)=24ms+360ms=384ms
sequenceDiagram
    participant Client as 客户端/浏览器
    participant App as 应用服务器
    participant Network as 网络链路
    participant DB as 数据库服务器

    title UPDATE语句的网络链路耗时分析

    %% 第一部分:请求进入
    Note over Client,DB: 阶段一:请求接收与事务开始
    Client->>App: HTTP POST/PUT请求
    App->>App: 业务验证与处理(15ms)
    App->>Network: 开始事务 BEGIN
    
    %% 第二部分:查询数据(网络往返1)
    Note over Client,DB: 阶段二:查询要更新的数据
    App->>Network: SELECT查询(查数据)
    Network->>DB: 查询请求(30ms延迟)
    DB->>DB: 执行查询(5ms)
    DB->>Network: 返回结果数据
    Network->>App: 数据返回(30ms延迟)
    
    %% 第三部分:业务逻辑处理
    App->>App: 业务计算与数据组装(10ms)
    
    %% 第四部分:执行UPDATE(网络往返2)
    Note over Client,DB: 阶段三:执行UPDATE操作
    App->>Network: UPDATE语句
    Network->>DB: UPDATE请求(30ms延迟)
    DB->>DB: 获取行锁(5ms)
    DB->>DB: 执行更新(8ms)
    DB->>Network: 返回影响行数
    Network->>App: 结果返回(30ms延迟)
    
    %% 第五部分:可能的额外操作
    Note over Client,DB: 阶段四:更新相关数据
    App->>Network: 更新关联表/计数器
    Network->>DB: 第二个UPDATE(30ms延迟)
    DB->>DB: 执行更新(3ms)
    DB->>Network: 返回结果
    Network->>App: 结果返回(30ms延迟)
    
    %% 第六部分:提交事务
    Note over Client,DB: 阶段五:事务提交
    App->>Network: COMMIT提交
    Network->>DB: 提交请求(30ms延迟)
    DB->>DB: 写入redo log(3ms)
    DB->>DB: 释放锁(2ms)
    DB->>Network: 提交完成
    Network->>App: 提交确认(30ms延迟)
    
    %% 第七部分:响应返回
    App->>App: 组装响应(5ms)
    App->>Client: 返回HTTP响应(200 OK)
    
    %% 第八部分:分析对比
    Note over App,DB: 耗时分析
    App->>DB: 理想情况(本地执行)
    DB-->>App: 总耗时:BEGIN(1ms)+SELECT(5ms)+UPDATE(13ms)+COMMIT(5ms)=24ms
    
    App->>DB: 实际情况(远程+网络延迟)
    DB-->>App: 总耗时:24ms + 网络(30ms×6往返×2)=24ms+360ms=384ms

由上图可知,远程的一次update语句或者说一次事务,不是单纯的把Sql甩给服务器就行了,期间有多次交互:

  • 服务端的数据库建立远程连接
  • 服务端开启事务请求
  • 服务端发起update请求
  • 数据库执行update语句,获得行锁,执行更新,返回影响行数
  • 服务端接收结果进行确认,提交事务
  • 数据库写入redo log,释放行锁,返回结果

若服务器与数据库存在巨大网络延迟,这多次交互累加起来的时间可能要数秒之多,会导致我遇到的这个问题。

3. 关键结论

3.1. UPDATE比SELECT更容易受网络影响

  • 事务开销:需要BEGIN、COMMIT的额外网络往返

  • 锁竞争:网络延迟会延长锁持有时间,加剧锁竞争

  • 重试机制:乐观锁、超时重试会倍增网络请求

  • 数据一致性:可能需要更新多个表,增加网络交互

3.2. 快速判断公式

如果:UPDATE本地执行快 + 应用端执行慢 + 网络延迟高 那么:很可能是网络链路问题

解决方法优先级:

  1. 减少网络往返次数(批量、存储过程)
  2. 降低网络延迟(同机房、专线)
  3. 异步化处理(消息队列)
  4. 优化事务范围(减少锁时间)
0%