一、现象:一个看似“网络”的异常
生产日志里突然冒出:复制
org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend. Caused by: java.io.IOException: Tried to send an out-of-range integer as a 2-byte value: 34524第一反应:
“网断了?连接池炸了?数据库挂了?”
结果 DBA 说数据库一切正常,别的业务也稳如老狗。于是开始漫长的“甩锅”之旅……
二、定位:34524 到底是个啥?
把异常栈翻到底,发现关键行:
at org.postgresql.core.PGStream.sendInteger2(PGStream.java:347)
PostgreSQL JDBC 驱动在组装Parse报文时,需要把参数个数写成 2 字节。
34524 > 32767(0x7FFF),直接越界,驱动抛错。
所以,这不是网络 I/O,而是协议层面的“参数超限”。
三、复现:一条 SQL 如何把参数干到 3W+
简化后的伪代码:
List<String> teams = dealerTeamService.selectAll(); // 1,800+ Map<String, Object> param = new HashMap<>(); param.put("dataDate", dataDate); param.put("teams", teams); // 1,800 个字符串 return mapper.queryWaitDeal(param);对应的 XML:
<select id="queryWaitDeal" resultType="xxx"> SELECT 'survey' AS stage, 1 AS orderId, COALESCE(SUM(wait_cnt), 0) AS waitCnt FROM t_wait_deal WHERE data_date = #{dataDate} AND (dealer_id::text||','||dealer_team_id::text) = ANY <foreach collection="teams" item="t" open="ARRAY[" separator="," close="]"> #{t} </foreach> UNION ALL <!-- 还有 3 个 UNION,每个都 copy 一遍 teams --> </select>1 800 × 4 条 UNION × 3 天批次 ≈ 34 000 个参数,
完美踩雷。
四、协议天花板:32767 的由来
PostgreSQL Frontend/Backend Protocol 文档里写得明明白白:
Parse (F) Int32 length String statementName String query Int16 number of parameter types (→ 2 字节) ...JDBC 驱动只是忠实实现,想改协议?先 fork PG 源码。
五、解法:参数瘦身三板斧
| 方案 | 思路 | 代码量级 | 推荐指数 |
|---|---|---|---|
| 1. 临时表/VALUES 表 | 先COPY团队列表到临时表,SQL 里直接JOIN | 中等 | ★★★★☆ |
| 2. 分批查询 | 内存拆队,每批 1k,结果归并 | 小 | ★★★★★ |
| 3. 服务端数组类型 | 把Array[text]换成int[],一条= ANY(?::int[])即可 | 需 DBA 配合 | ★★★☆☆ |
临时表演示:
sql
-- 会话级临时表,自动回收 CREATE TEMP TABLE tmp_team ON COMMIT DROP AS SELECT unnest(?::text[]) AS team_id; SELECT ... FROM t_wait_deal w JOIN tmp_team t ON (w.dealer_id||','||w.dealer_team_id) = t.team_id;Java 端只需传1 个 java.sql.Array,参数个数瞬间降到 3 个。
分批演示:
List<List<String>> partitions = Lists.partition(allTeams, 800); return partitions.stream() .map(p -> mapper.queryWaitDeal(dataDate, p)) .reduce(this::merge) .orElse(Collections.emptyList());六、踩坑小结
看到“An I/O error occurred while sending to the backend”先别急着重启,
把Caused by翻到底,关键字“out-of-range integer as a 2-byte value”直指参数过多。MyBatis 的
<foreach>爽归爽,集合 size 超过 1k 就要警惕。架构评审时,把“列表查询”当潜在 SQL 炸弹,提前留好分批/临时表口子。
32767 是硬天花板,任何 ORM、任何驱动都绕不过去。
七、一行代码应急兜底
if (teams.size() > 1000) { throw new BizException("一次最多选择 1000 个团队,请缩小范围"); }先保命,再优化。
八、参考
PostgreSQL Protocol 3.0 – Frontend/Backend Protocol
PostgreSQL JDBC Driver –
PGStream.javahistory #2471MyBatis Foreach 陷阱 – MyBatis Documentation