前言
呵呵 这个也是一个之前碰到的一个问题, 主要的问题是 发送给数据库的 sql 参数太多了, 呵呵 超过了 postgres 客户端, 服务器 交互的参数的数量的上限了
之前记录了一个 todo, 最近的时候 看了一下 这部分的代码, 以及 涉及到的相关的协议
另外 例子中会有一个对比, 就是通过 jpa 的 specification 交互的参数来传递这 4w 个参数是有问题的, 会抛出异常
但是假设 我们是将 4w 个参数的查询封装成 sql 语句, 直接传递个服务器, 那么是可以正常执行的
可以参见 下面的 findBySpeficication 部分和 下面的 jdbcTemplate.queryForObject 的部分的差异
测试用例
呵呵 repository 基于 JPARepository, 这里为了构造情况, 因此构造了一个 id not in (param1, param2, ... param40000)
完整的堆栈信息大致如下
Caused by: org.hibernate.exception.JDBCConnectionException: could not extract ResultSet
at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:112)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:67)
at org.hibernate.loader.Loader.getResultSet(Loader.java:2341)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2094)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2056)
at org.hibernate.loader.Loader.doQuery(Loader.java:953)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:350)
at org.hibernate.loader.Loader.doList(Loader.java:2887)
at org.hibernate.loader.Loader.doList(Loader.java:2869)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2701)
at org.hibernate.loader.Loader.list(Loader.java:2696)
at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:506)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:400)
at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:219)
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1415)
at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1565)
at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1533)
at org.hibernate.query.Query.getResultList(Query.java:165)
at org.hibernate.query.criteria.internal.compile.CriteriaQueryTypeQueryAdapter.getResultList(CriteriaQueryTypeQueryAdapter.java:76)
at com.hello.world1.repository.jpa.AbstractJpaRepository.readPage(AbstractJpaRepository.java:130)
at com.hello.world1.repository.jpa.AbstractJpaRepository.findBySpecification(AbstractJpaRepository.java:97)
at com.hello.world.repository.custom.impl.NotificationTemplateRepositoryImpl.search(NotificationTemplateRepositoryImpl.java:47)
at com.hello.world.repository.custom.impl.NotificationTemplateRepositoryImpl$$FastClassBySpringCGLIB$$215758fa.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 120 common frames omitted
Caused by: org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:337)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:447)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:368)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:158)
at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:108)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:57)
... 146 common frames omitted
Caused by: java.io.IOException: Tried to send an out-of-range integer as a 2-byte value: 40003
at org.postgresql.core.PGStream.sendInteger2(PGStream.java:266)
at org.postgresql.core.v3.QueryExecutorImpl.sendParse(QueryExecutorImpl.java:1500)
at org.postgresql.core.v3.QueryExecutorImpl.sendOneQuery(QueryExecutorImpl.java:1823)
at org.postgresql.core.v3.QueryExecutorImpl.sendQuery(QueryExecutorImpl.java:1386)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:302)
... 153 common frames omitted
抛出异常的地方如下, 可以看到这里约束的 parameterCount 是在 2字节
sendInteger2 的校验
可以看出这里是如果 val 超过了 32767 的话, 就回抛出异常?, 但是 2byte 可以表示 65536 阿? 先暂时理解为这里参数的长度最大为 32767, 我们后面再看看 "参数长度为 int16 客户端这边的限制为 32767 ? "
我们查看一下原始的 传递的 sql, 也能更清晰的意识到这 4w 多个参数分别是什么
select distinct bank0_.ID as id1_56_, 省略部分参数, from BANK bank0_ where (bank0_.TITLE_TEMPLATE not like $1) and (bank0_.CONTENT_TEMPLATE not like $2) and (bank0_.ID not in ($3 , $4 , $5 , $6, 省略部分参数 , $39996 , $39997 , $39998 , $39999 , $40000 , $40001 , $40002)) order by bank0_.CREATED_ON desc limit $40003
客户端参数发送
呵呵 这是我之前记录下来的一些东西, 核心的是关于 客户端给服务端传递的数据, 待会儿 我们可以 参照规范看一下
QueryExecutorImpl. sendOneQuery, add at 2020.12.21
// sendOneQuery sends a single statement via the extended query protocol.
// Per the FE/BE docs this is essentially the same as how a simple query runs
// (except that it generates some extra acknowledgement messages, and we
// can send several queries before doing the Sync)
//
// Parse S_n from "query string with parameter placeholders"; skipped if already done previously
// or if oneshot
// Bind C_n from S_n plus parameters (or from unnamed statement for oneshot queries)
// Describe C_n; skipped if caller doesn't want metadata
// Execute C_n with maxRows limit; maxRows = 1 if caller doesn't want results
// (above repeats once per call to sendOneQuery)
// Sync (sent by caller)
1. sendParse
pgStream.sendChar('P'); // Parse
pgStream.sendInteger4(encodedSize);
if (encodedStatementName != null) {
pgStream.send(encodedStatementName);
}
pgStream.sendChar(0); // End of statement name
pgStream.send(queryUtf8); // Query string
pgStream.sendChar(0); // End of query string.
pgStream.sendInteger2(params.getParameterCount()); // # of parameter types specified
for (int i = 1; i <= params.getParameterCount(); ++i) {
pgStream.sendInteger4(params.getTypeOID(i));
}
2. sendDescribeStatement
pgStream.sendChar('D'); // Describe
pgStream.sendInteger4(encodedSize); // Message size
pgStream.sendChar('S'); // Describe (Statement);
if (encodedStatementName != null) {
pgStream.send(encodedStatementName); // Statement name
}
pgStream.sendChar(0); // end message
3. sendBind
pgStream.sendChar('B'); // Bind
pgStream.sendInteger4((int) encodedSize); // Message size
if (encodedPortalName != null) {
pgStream.send(encodedPortalName); // Destination portal name.
}
pgStream.sendChar(0); // End of portal name.
if (encodedStatementName != null) {
pgStream.send(encodedStatementName); // Source statement name.
}
pgStream.sendChar(0); // End of statement name.
pgStream.sendInteger2(params.getParameterCount()); // # of parameter format codes
for (int i = 1; i <= params.getParameterCount(); ++i) {
pgStream.sendInteger2(params.isBinary(i) ? 1 : 0); // Parameter format code
}
pgStream.sendInteger2(params.getParameterCount()); // # of parameter values
// If an error occurs when reading a stream we have to
// continue pumping out data to match the length we
// said we would. Once we've done that we throw
// this exception. Multiple exceptions can occur and
// it really doesn't matter which one is reported back
// to the caller.
//
PGBindException bindException = null;
for (int i = 1; i <= params.getParameterCount(); ++i) {
if (params.isNull(i)) {
pgStream.sendInteger4(-1); // Magic size of -1 means NULL
} else {
pgStream.sendInteger4(params.getV3Length(i)); // Parameter size
try {
params.writeV3Value(i, pgStream); // Parameter value
} catch (PGBindException be) {
bindException = be;
}
}
}
pgStream.sendInteger2(numBinaryFields); // # of result format codes
for (int i = 0; i < numBinaryFields; ++i) {
pgStream.sendInteger2(fields[i].getFormat());
}
4. sendExecute
// Total size = 4 (size field) + 1 + N (source portal) + 4 (max rows)
pgStream.sendChar('E'); // Execute
pgStream.sendInteger4(4 + 1 + encodedSize + 4); // message size
if (encodedPortalName != null) {
pgStream.send(encodedPortalName); // portal name
}
pgStream.sendChar(0); // portal name terminator
pgStream.sendInteger4(limit); // row limit
QueryExecutorImpl. sendQuery, 之后会发送一个 sync 的命令
QueryExecutorImpl. sendSync
pgStream.sendChar('S'); // Sync
pgStream.sendInteger4(4); // Length
pgStream.flush();
客户端接收服务端的响应
QueryExecutorImpl. processResults : 从服务端获取数据, 并且封装 ResultSet
客户端这边 依次会收到服务器这边的 "Close Complete", "Parse Complete", ""
1. Close Complete : 消费了服务器返回回来的长度信息
2. Parse Complete : 消费了服务器返回回来的长度信息, 并且移除了 sendParse 放到 pendingParseQueue 里面的 query
3. Bind Complete : 消费了服务器返回回来的长度信息, 并且移除了 sendBind 放到 pendingBindQueue 里面的 portal, 并向 portalMap 注册
4. Row Description : 消费了服务器返回回来的长度信息, 并且移除了 sendParse 放到 pendingDescribePortalQueue 里面的 query, 并设置了 fields 的元数据
5. Data Transfer : 消费了服务器返回的数据的信息, 并记录在了 tuples 的容器里面
6. Command Status : 消费了服务器返回的状态信息, 并且移除了 sendExecute 放到 pendingExecuteQueue 里面的 executeRequest, 校验 fields 元数据 + 服务器返回的数据, 并且处理数据, 封装 ResultSet
7. Ready For Query : SYNC 的响应, 移除 SYNC 存放在 pendingExecuteQueue 里面的 executeRequest, 清理掉 pendingParseQueue, pendingBindQueue, pendingDescribePortalQueue, pendingExecuteQueue
Loader.doQuery 里面对于结果进行处理
这里的处理 就不细看了, 呵呵 服务器那边返回的 字节序列 byte[][] 直接 transform 一下, 就是对应的 一行的数据
章44. 前/后端协议
呵呵 这个应该才是 本文的重点, 参照的协议, 章44. 前/后端协议
上面的客户端发送给服务端的数据如何组织, 服务端返回给客户端的数据怎么解析, 都是基于协议的
另外本文的 参数数量只使用 2个字节 来表示长度 为什么会这么约束? 都可以在协议中找到原因
我们这里着重需要关注的是 44.4. 消息格式
备注一下一部分的内容, 截图 来自于文档 44.4. 消息格式
扩展查询
我们上面从客户端代码中看到的查询, 在规范中称之为 44.2.3. 扩展查询
本文相关客户端发送的一些请求协议如下
Parse
呵呵 从这里可以看出为什么 Parse 阶段 参数数量只使用 2个字节 来表示长度, 看到这个 int16 了么
Describe
Bind
Execute
Sync
本文相关服务端发送的一些请求协议如下
CloseComplete
ParseComplete
BindComplete
RowDescription
DataTransfer
CommandComplete
ReadyForQuery
参数长度为 int16 客户端这边的限制为 32767 ?
呵呵 众所周知, 2个字节 无符号整数表示的话, 最多可以表示到 0xffff = 65535, 但是为什么这里的限定是 32767 呢?
我的理解是, 属于客户端这边的一些限制吧?, 可能是处理这块的时候 没有想到什么太好的转换方式吧
这里我们来尝试, 调整一下, 呵呵
在 PGStream. sendInteger2 之前加上一段如下处理, 做一些有符号到无符号的数据的适配处理, 当然这里只是简单处理, 只是为了验证这里的主体, 可能没有太多的其他的各种情况的考虑
if(val > Short.MAX_VALUE) {
val = val - 65536;
}
然后调整一下我们这里的测试用例, 呵呵 4w 个已有的参数, 再加上数据库中已经存在的大部分的 实体的 id, 去掉了三个是用于测试效果, 呵呵
然后我们可以查看一下返回的数据, 呵呵 似乎是突破了目前客户端已有的 32767 的限制, 当然作用也不是太大, 只是吧这里的数据交互 更加逻辑严谨化了一些而已
jdbcTemplate. queryForObject 不是走的上面的 P+D+B+E+S ?
最开始我碰到这个问题, 想要看下 相关代码的时候, 我当时的复现方式是 想使用 jdbcTemplate 封装 sql 构造大量的参数来试试, 谁知道 灭有成功
后面才 换成了项目中本来使用的 jpa 的一些 api 来使用
呵呵 使用 jdbcTemlpate.query 来查询, 你会发现完全没有问题, 呵呵 没有被校验住, 并且拿到了最终的数据结果 3 条记录, 去掉了三个是用于测试效果
最开始我在想 这里是给出了完整的 sql, 那么 它是不是走的 协议中所说的简单查询, 所以它没有传递参数数量阿, 这些 所以没有报错呢? 但是实际看了一下 呵呵 和我想的不一样
它还是走的时候 扩展查询的这一系列的步骤, 呵呵 只是这里 encodedSize, parameterCount, 以及一些 其他的限制都在合理的范围内, 传递到了服务器那边 去执行
完
参考
章44. 前/后端协议
44.2.3. 扩展查询
44.4. 消息格式