MySQL客户端通过流式分片和协议帧封装接收查询结果:服务端逐行发送Column Definition帧和Row帧,客户端按MySQL Protocol规范逐帧解析,每帧以3字节长度+1字节序号标识边界,大字段可能被拆分为多个子帧。

MySQL 客户端如何收到 query 结果:从 SELECT 到本地变量的链路
MySQL 不是“把结果集打包发过去”就完事——它用流式分片 + 协议帧封装的方式传输,客户端必须按 MySQL Protocol 规范逐帧解析。你看到的 mysql_fetch_row() 或 cursor.fetchall(),本质是在消费一个 TCP 流里的多个二进制帧。
- 服务端不会等整个结果集生成完再发;只要第一行 ready,就立刻发
Column Definition帧 + 第一行Row帧 - 每帧以
packet length (3 bytes)+sequence ID (1 byte)开头,客户端靠这个识别帧边界(不是靠换行或 JSON 分隔) - 大字段(如
TEXT/BLOB)可能被拆成多个Row Data子帧(MYSQL_TYPE_LONG_BLOB场景下常见) - 如果客户端没及时读取(比如 Python 里忘了调
fetchone()),服务端会在 socket send buffer 满后阻塞在write(),触发wait_timeout断连
为什么 mysql_real_connect() 后不发数据,但 mysql_query() 会卡住?
因为 mysql_query() 是同步阻塞调用,它内部做了三件事:发送 query 帧 → 循环 recv 直到收到完整响应帧 → 解析并缓存元数据。卡住通常不是网络问题,而是服务端还没返回第一个帧——可能正在执行慢查询、锁等待,或结果集太大导致 send buffer 积压。
- 检查是否启用了
net_write_timeout(默认 60s),超时会断开连接并报错Lost connection to MySQL server during query -
SHOW PROCESSLIST中看到状态为Sending data,说明服务端仍在构造/发送结果,不是卡在网络 - C 客户端若用非阻塞 socket,需手动处理
EAGAIN/EWOULDBLOCK并轮询,Python/Java 驱动已封装这一层
大结果集下内存暴涨?别怪 MySQL,先看客户端怎么读
驱动默认把整张结果集 load 到内存(如 MySQLdb 的 fetchall()、Go 的 rows.Scan() 全部展开),和 MySQL 服务端是否流式发送无关。真正控制内存的是客户端读取方式。
- PHP PDO:用
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false启用 unbuffered 模式,fetch()一次只拿一行,但要求必须读完否则连接会被 close - Python MySQLdb:用
sscursor = conn.cursor(MySQLdb.cursors.SSCursor),配合fetchone()流式读,避免fetchall() - Java JDBC:设置
statement.setFetchSize(Integer.MIN_VALUE)触发流式读(MySQL Connector/J 特有),否则即使ResultSet很大也全缓存在 driver heap
SELECT id, content FROM articles WHERE created_at > '2024-01-01' LIMIT 1000000;
TCP 层真实抓包能看到什么?
用 tcpdump -i lo port 3306 -w mysql.pcap 抓包后 Wireshark 打开,你会看到大量小包(通常 ≤ 16KB),每个包 payload 是一个或多个 MySQL protocol frame。关键不是看 SQL 文本,而是关注:
- 帧头的
packet length字段是否突增(比如从 32 字节跳到 8192 字节)→ 对应一个大Row的开始 - 连续多个帧的
sequence ID是否递增且无跳变 → 判断是否丢帧(MySQL 协议本身无重传,丢帧 = 连接异常中断) - 出现
0xFF开头的错误帧(如0xFF 0x15 0x00 #HY000...)→ 表示服务端主动终止了这次 query 流程
read_timeout 和 write_timeout 是两回事;所谓“结果返回”,其实是客户端一边 recv 一边 decode 的持续过程——漏掉任意一帧,整个结果集就不可用。










