SQLx 源码深度解析
第12章 Connection trait:connect / close / ping 的生命周期
Connection用 14 个方法描述一条连接的完整生命周期。其中最微妙的是close与close_hard的分野——因为Drop不能 async,也就发不出那条 graceful shutdown 消息。
本章要点
Connectiontrait(sqlx-core/src/connection.rs:14)定义一条数据库连接的完整生命周期协议——共 14 个方法(7 必实现 + 7 默认):close/close_hard/ping/begin/shrink_buffers/flush/should_flush必实现;begin_with/is_in_transaction/transaction/cached_statements_size/clear_cached_statements/connect/connect_with带默认实现。涵盖建立、探活、事务、缓存管理、关闭五类职责。close(self)vsclose_hard(self)——前者发 graceful shutdown 消息(Postgres 的Terminate、MySQL 的COM_QUIT),后者只shutdown()关闭 socket(发 TCP FIN)。Postgres / MySQL 连接的Drop连close_hard都不调——它们根本没有impl Drop,只靠 socket 析构由操作系统收尾——因为 Drop 不能 async、不能发消息。ping探活——发协议级的轻量请求、等回应。Postgres 发一个Sync消息再等ReadyForQuery(write_sync()+wait_until_ready())、MySQL 发COM_PING等OK_Packet、SQLite 往 worker 线程投一条Command::Ping等回执。begin开事务——默认走 ANSIBEGIN;begin_with(statement)允许自定义(比如BEGIN ISOLATION LEVEL SERIALIZABLE)。返回Transaction<'_, DB>一个 RAII guard(第 15 章详细)。transaction(closure)便捷方法——给一个闭包、内部自动 begin / callback / commit-or-rollback。用户无需手动 begin/commit 两个方法。ConnectOptionstrait——url.parse()解析连接字符串、connect()真正建立连接。driver 特定的所有配置(SSL mode、application name、statement cache 容量)都通过这个 trait 的具体实现(PgConnectOptions等)配置。PgConnection内部是Box<PgConnectionInner>(sqlx-postgres/src/connection/mod.rs:36-75)——box 化让 PgConnection 本身只有指针大小;inner 有 stream、statement cache(LRU)、三张 type cache、transaction_status/depth、log_settings 等 12 个字段。SqliteConnection是一个 worker 线程的 proxy——SQLite 的 C API 是同步的,sqlx 用一个专用 OS 线程承载 sqlite3 handle,主线程通过 channel 发命令到 worker。这是三家驱动里唯一用到 OS 线程的。
12.1 问题引入:连接不只是 TCP socket
一次数据库连接的生命周期远比"打开 socket、关闭 socket"复杂:
let mut conn = PgConnection::connect("postgres://...").await?; // 建立
conn.ping().await?; // 探活
let mut tx = conn.begin().await?; // 开事务
sqlx::query("...").execute(&mut *tx).await?; // 跑 SQL
tx.commit().await?; // 提交
conn.close().await?; // 关闭
六个阶段——每一个都对应具体的协议级交互:
connect:TCP 连接 + TLS 握手 + 认证握手 + 参数协商。Postgres 的 StartupMessage / SASL / AuthenticationOk 协议;MySQL 的 Handshake / HandshakeResponse / OK_Packet;SQLite 的sqlite3_open_v2(走 C FFI)。ping:轻量探活——发最小请求等响应、时间完全由网络 RTT 决定。用在连接池的"取连接前校验"和"归还前检查"。begin:协议级开事务。三家驱动统一调用sqlx-core/src/transaction.rs:277的begin_ansi_transaction_sql(depth)——depth 为 0 时返回字面量BEGIN——一条 SQL 兼容三家。嵌套层(depth>0)转SAVEPOINT _sqlx_savepoint_<N>。用户可以通过begin_with(statement)传自定义(例如BEGIN ISOLATION LEVEL SERIALIZABLE或 SQLite 的BEGIN IMMEDIATE)。execute(属于 Executor trait):发 SQL,后续协议按驱动不同。commit / rollback(在 Transaction 上):发COMMIT/ROLLBACK。close:告诉服务端"我要走了"——让服务端立即清理 session state 而不是等 TCP keepalive 超时。
如果跳过 close 直接 drop 会怎样?
- Rust 端:TCP socket 被 drop,操作系统发出 FIN 包。
- Postgres 端:后端进程读到 EOF 就知道客户端走了,退出时回滚未提交的事务、清掉会话资源——前提是 FIN 真的送到了。如果网络中断或对端主机宕机,服务端只能等 TCP keepalive 超时才发现连接已死,
close的文档注释(sqlx-core/src/connection.rs:24-27)说的"延迟"指的就是这种情况。 - 连接限制场景:源码注释提醒,短时间内创建又直接丢弃大量连接,可能让服务端报错——那些"senescent"(垂死)连接在服务端察觉之前仍计入连接数上限。
Connection trait 的每个方法都是在管理这个协议级的生命周期——让用户代码能礼貌地和数据库服务端交互。本章拆开每个方法的具体协议行为。
12.2 Connection trait 的 14 个方法
sqlx-core/src/connection.rs:14 的 trait 定义简化后(下面把默认实现的方法体略去了,但别被这个形状误导——14 个方法里只有 7 个是驱动必须实现的:close、close_hard、ping、begin、shrink_buffers、flush、should_flush;另外 7 个都带默认实现:begin_with(:59-67,转发 Transaction::begin)、is_in_transaction(:76,get_transaction_depth() != 0)、transaction(:97)、cached_statements_size(:127,返回 0)、clear_cached_statements(:136,返回 Ok)、connect(:168,解析 URL 后转 connect_with)、connect_with(:178,转发 options.connect())。写一个新驱动时,真正绕不开的就是那 7 个):
pub trait Connection: Send {
type Database: Database<Connection = Self>;
type Options: ConnectOptions<Connection = Self>;
fn close(self) -> BoxFuture<'static, Result<(), Error>>;
fn close_hard(self) -> BoxFuture<'static, Result<(), Error>>;
fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>>;
fn begin(&mut self) -> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
where Self: Sized;
fn begin_with(&mut self, statement: impl Into<Cow<'static, str>>)
-> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
where Self: Sized;
fn is_in_transaction(&self) -> bool;
fn transaction<F, R, E>(&mut self, callback: F) -> BoxFuture<'_, Result<R, E>>
where ...;
fn cached_statements_size(&self) -> usize
where Self::Database: HasStatementCache;
fn clear_cached_statements(&mut self) -> BoxFuture<'_, Result<(), Error>>
where Self::Database: HasStatementCache;
fn shrink_buffers(&mut self);
fn flush(&mut self) -> BoxFuture<'_, Result<(), Error>>;
fn should_flush(&self) -> bool;
fn connect(url: &str) -> BoxFuture<'static, Result<Self, Error>>
where Self: Sized;
fn connect_with(options: &Self::Options) -> BoxFuture<'_, Result<Self, Error>>
where Self: Sized;
}
两个关联类型 Database 和 Options——通过这两者把 Connection 和 Database trait 家族、ConnectOptions trait 家族关联起来。
14 个方法按功能分五组:
- 生命周期:close / close_hard / flush / should_flush
- 探活:ping
- 事务:begin / begin_with / is_in_transaction / transaction
- 缓存管理:cached_statements_size / clear_cached_statements / shrink_buffers
- 建立:connect / connect_with
每组我们逐个看。
12.3 close vs close_hard:两种关闭仪式
sqlx-core/src/connection.rs:19-41 的两个方法:
/// Explicitly close this database connection.
fn close(self) -> BoxFuture<'static, Result<(), Error>>;
/// Immediately close the connection without sending a graceful shutdown.
#[doc(hidden)]
fn close_hard(self) -> BoxFuture<'static, Result<(), Error>>;
close(self) 做的事:
- 发服务端特定的 "我要走了" 消息:
- Postgres:
Terminate消息(类型字节'X',后面只有 4 字节长度、没有载荷)。 - MySQL:
COM_QUIT。 - SQLite:没有服务端,
close先按配置执行PRAGMA optimize,再给 worker 线程发Shutdown命令,由 worker 析构连接状态时调sqlite3_close()(C API)。
- Postgres:
- 不等服务端回应,紧接着
shutdown()关闭 socket(Postgres 协议本就规定前端发完Terminate立即关连接;MySQL 的COM_QUIT服务端可能直接断开也可能回一个 OK,sqlx 都不读)。
close_hard(self) 做的事:
- 不发协议消息。
- 直接
shutdown()关闭 socket(让服务端靠 FIN 发现客户端走了)。
为什么需要两个? 源码 close 的文档注释说得很清楚(sqlx-core/src/connection.rs:24-27):
While connections can simply be dropped to clean up local resources, the
Drophandler itself cannot notify the server that the connection is being closed because that may require I/O to send a termination message.
Drop 不能做 async I/O——发 Terminate 消息是异步操作,drop 是同步函数。所以 sqlx 提供两条路径:
close(self).await—— 用户显式关、正确发消息。drop(conn)—— 连 close_hard 都不调,只靠 socket 析构、由操作系统发 FIN(见 §12.3.1)。
close_hard 是 #[doc(hidden)]——不推荐用户直接调,它主要给 Pool 用:ping 失败、回调报错这类"连接已经坏了"的场合,Pool 用它丢弃连接而不尝试优雅关闭(sqlx-core/src/pool/inner.rs:477)。
12.3.1 drop 一条连接时到底发生了什么
值得强调的是:sqlx 0.8.6 里 PgConnection 和 MySqlConnection 根本没有 impl Drop(sqlx-postgres 里的 Drop 实现只有四个:sqlx-postgres/src/transaction.rs 的 Rollback、sqlx-postgres/src/listener.rs 的 PgListener、sqlx-postgres/src/copy.rs 的 PgCopyIn 和 sqlx-postgres/src/advisory_lock.rs 的 PgAdvisoryLockGuard,没有一个是给连接本身的)。drop 一条 PgConnection 就是普通的字段递归析构——PgStream 里的 TcpStream 被关掉,操作系统发出 FIN,仅此而已。Terminate 消息不会被发出去。
对照 Postgres 的 close / close_hard 实现(sqlx-postgres/src/connection/mod.rs:153-174)就一目了然:
fn close(mut self) -> BoxFuture<'static, Result<(), Error>> {
// The normal, graceful termination procedure is that the frontend sends a Terminate
// message and immediately closes the connection.
Box::pin(async move {
self.inner.stream.send(Terminate).await?;
self.inner.stream.shutdown().await?;
Ok(())
})
}
fn close_hard(mut self) -> BoxFuture<'static, Result<(), Error>> {
Box::pin(async move {
self.inner.stream.shutdown().await?;
Ok(())
})
}
两者只差一条 Terminate。而 drop 连 close_hard 都不如——它连 shutdown() 这个异步动作都做不了,只能靠 socket 析构。这就是"Drop 不能 async"在 sqlx 里最具体的体现(第 1 章 §1.6 讨论过 Transaction::drop 的类似局限)。想让协议消息真的发出去,只有显式 close().await 一条路。
SQLite 是例外——它没有网络 I/O,sqlite3_close 是同步 C 调用,所以 ConnectionState / ConnectionHandle 都能在 Drop 里正经关库(sqlx-sqlite/src/connection/mod.rs:562、sqlx-sqlite/src/connection/handle.rs:88)。
12.4 ping:探活
sqlx-core/src/connection.rs:44:
fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>>;
发轻量请求验证连接还活着。每家实现不同:
- Postgres(
sqlx-postgres/src/connection/mod.rs:176-186):write_sync()发一条Sync消息、再wait_until_ready()等ReadyForQuery。源码注释里还留着旧做法:执行一条/* SQLx ping */注释语句(选注释是为了连接正处在读行集中途时也不报错),后来因为"用户抱怨它出现在服务端的查询统计里"才换成现在这种最简单的一问一答。开销就是一个网络往返。 - MySQL(
sqlx-mysql/src/connection/mod.rs:82-90):wait_until_ready()后发Ping(COM_PING)包、recv_ok()收OK_Packet。同样一个往返。 - SQLite(
sqlx-sqlite/src/connection/worker.rs:405-407):往 worker 线程投一条Command::Ping、等 oneshot 回执。不走网络,但仍是一次线程间往返。
ping 的典型用户是 Pool(第 14 章 §14.6.1)——acquire 从空闲队列取出的连接,先 ping 验证还能用、再交给用户(不看闲置了多久,每次都 ping;新建的连接不 ping)。这避免"用户拿到一个已经被服务端超时断开的连接"——ping 失败的话 Pool 用 close_hard 丢弃这个连接、再去拿下一条或新建。此外 0.8.6 在连接归还时也会无条件 ping 一次(sqlx-core/src/pool/connection.rs:314,第 14 章 §14.9),这一次关不掉。
ping 的代价:每次 acquire 都多一个往返。sqlx PoolOptions::test_before_acquire 默认 true(sqlx-core/src/pool/options.rs:149)—— 这条是"拿到坏连接再失败"和"acquire 路径多一跳"的权衡、sqlx 选后者保证正确性优先。延迟极敏感的业务可以 .test_before_acquire(false) 关掉、配合 before_acquire 回调做更轻量的检查、或者干脆让坏连接失败触发 Pool 自愈。
12.5 begin / begin_with / transaction
sqlx-core/src/connection.rs:49-124 的事务四方法(begin 必实现,其余三个有默认实现):
fn begin(&mut self) -> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
where
Self: Sized;
// …(这四个方法各自的文档注释与 doc 示例已略去,只留签名与默认实现)
fn begin_with(
&mut self,
statement: impl Into<Cow<'static, str>>,
) -> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
where
Self: Sized,
{
Transaction::begin(self, Some(statement.into()))
}
#[inline]
fn is_in_transaction(&self) -> bool {
<Self::Database as Database>::TransactionManager::get_transaction_depth(self) != 0
}
fn transaction<'a, F, R, E>(&'a mut self, callback: F) -> BoxFuture<'a, Result<R, E>>
where
for<'c> F: FnOnce(&'c mut Transaction<'_, Self::Database>) -> BoxFuture<'c, Result<R, E>>
+ 'a
+ Send
+ Sync,
Self: Sized,
R: Send,
E: From<Error> + Send,
{
Box::pin(async move {
let mut transaction = self.begin().await?;
let ret = callback(&mut transaction).await;
match ret {
Ok(ret) => {
transaction.commit().await?;
Ok(ret)
}
Err(err) => {
transaction.rollback().await?;
Err(err)
}
}
})
}
begin() 默认用 ANSI SQL 的 BEGIN(第一层事务)或 SAVEPOINT _sqlx_savepoint_N(嵌套)——发到服务端、等 OK、返回 Transaction。
begin_with(stmt) 允许自定义 BEGIN 语句——典型用例:
let mut tx = conn.begin_with("BEGIN ISOLATION LEVEL SERIALIZABLE READ WRITE").await?;
覆盖默认的 BEGIN——Postgres 下可以指定 isolation level。MySQL 不行:它的 BEGIN / START TRANSACTION 不接受隔离级别子句(隔离级别要事先用 SET TRANSACTION ISOLATION LEVEL ... 设置),能传的是 START TRANSACTION READ ONLY、START TRANSACTION WITH CONSISTENT SNAPSHOT 这类。不论哪家,驱动执行完自定义语句后都会检查连接是否真的进入了事务,没有就返回 Error::BeginFailed;已经在事务里时再调 begin_with 则返回 Error::InvalidSavePointStatement。
transaction(closure) 是便捷包装器——闭包返回 Ok 就 commit、Err 就 rollback。用户代码省掉手动 commit/rollback 两行:
// 用 transaction 包装
let result = conn.transaction(|tx| Box::pin(async move {
sqlx::query("UPDATE ...").execute(&mut **tx).await?;
sqlx::query("INSERT ...").execute(&mut **tx).await?;
Ok::<_, sqlx::Error>(())
})).await?;
// 等价的手写版
let mut tx = conn.begin().await?;
sqlx::query("UPDATE ...").execute(&mut *tx).await?;
sqlx::query("INSERT ...").execute(&mut *tx).await?;
tx.commit().await?;
// drop(tx) // 如果 commit 没调 drop 会 rollback
transaction 的价值在错误路径——手写版本里 ? 冒泡时如果忘了 rollback 会依赖 Drop(Drop 的"尽力 rollback"在第 15 章详细);transaction 版本里错误路径显式 rollback。第 15 章会详细对比两者的事务保证差异。
12.6 cached_statements_size / clear_cached_statements
sqlx-core/src/connection.rs:126-141:
fn cached_statements_size(&self) -> usize
where Self::Database: HasStatementCache,
{ 0 }
fn clear_cached_statements(&mut self) -> BoxFuture<'_, Result<(), Error>>
where Self::Database: HasStatementCache,
{ Box::pin(async move { Ok(()) }) }
两个方法受 HasStatementCache 约束(第 3 章 §3.9)。要说明的是:在 0.8.6 里三家驱动都实现了这个 marker trait——sqlx-postgres/src/database.rs:40、sqlx-mysql/src/database.rs:38、sqlx-sqlite/src/database.rs:39 各有一行 impl HasStatementCache for Xxx {}。SQLite 也照样能调,只是实现方式不同:cached_statements_size 读 worker 共享状态里的原子量,clear_cached_statements 往 worker 投一条清缓存命令(sqlx-sqlite/src/connection/mod.rs:267-276)。
这个 bound 存在的意义是给未来不带语句缓存的驱动留门——marker trait 让"有没有这项能力"落在类型层,而不是运行时返回 NotSupported。
cached_statements_size 返回 LRU 缓存里当前有几条 prepared statement。典型用法是监控:
tracing::debug!("statements cached: {}", conn.cached_statements_size());
clear_cached_statements 主动清空缓存——本地 LRU 清空,同时让服务端释放每个 prepared statement:Postgres 逐条发 Close(还顺手清空 cache_type_oid,这是它唯一被清空的地方)、MySQL 逐条发 COM_STMT_CLOSE,SQLite 则在 worker 线程里本地 finalize。什么时候用?
- long-running 连接内存占用——把
statement_cache_capacity调到上千的长连接,缓存会一直占着这些语句——手动 clear 回收。 - schema 变更后——DDL 改了列类型,旧的 prepared statement 基于旧 schema、继续用会"列类型不匹配"。clear 强制下次重新 prepare。
- 测试场景——测试之间清空缓存隔离状态。
大多数生产代码不调——让 LRU 自动驱逐(三家驱动的 statement_cache_capacity 默认都是 100)。
12.7 shrink_buffers
sqlx-core/src/connection.rs:143-155:
/// Restore any buffers in the connection to their default capacity, if possible.
fn shrink_buffers(&mut self);
释放连接内部的扩展 buffer——用过大查询后 buffer 可能扩到几 MB,shrink_buffers 把 capacity 缩回 DEFAULT_BUF_SIZE,也就是 8192 字节(sqlx-core/src/net/socket/buffered.rs:10)。典型场景:
// 拉回一行约 10 MB 的 huge_blob
sqlx::query("SELECT huge_blob FROM ...").fetch_all(&mut conn).await?;
// 为装下这条大消息,buffer 被撑到 10 MB 量级
conn.shrink_buffers(); // 缩回默认
// buffer 现在 ~8KB
撑大 buffer 的是单条大消息(一行很宽、一条很长的 SQL),不是结果集总量——行是一条条读进来的。另外 SQLite 连接没有 socket 缓冲,它的 shrink_buffers 是空操作。
为什么不自动?因为 shrink 需要重新分配 Vec——如果下一条 query 又要大 buffer,就浪费了一次 alloc + copy。sqlx 让用户显式声明"这条大查询之后我不再需要大 buffer"——常见于连接归还 Pool 前。
12.8 connect 和 connect_with
sqlx-core/src/connection.rs:163-183:
fn connect(url: &str) -> BoxFuture<'static, Result<Self, Error>>
where Self: Sized,
{
let options = url.parse();
Box::pin(async move { Self::connect_with(&options?).await })
}
fn connect_with(options: &Self::Options) -> BoxFuture<'_, Result<Self, Error>>
where Self: Sized,
{
options.connect()
}
两条路径:
connect(url_string)—— 解析 URL 成 Options、再 connect_with。适合"URL 里所有配置都够用"的简单场景。connect_with(options)—— 已经手动构造好 Options(带 log_statements、statement_cache_capacity 等高级配置)——直接连。
URL 解析走 Options: FromStr——由 ConnectOptions trait 保证(见下节)。解析失败(URL 格式错、scheme 不对)返回错误。
典型用法:
use log::LevelFilter;
use sqlx::postgres::{PgConnectOptions, PgConnection};
use sqlx::{ConnectOptions, Connection};
// URL 简单配置
let conn = PgConnection::connect("postgres://user:pass@host/db").await?;
// Options 详细配置
let opts = PgConnectOptions::new()
.host("host")
.database("db")
.username("user")
.statement_cache_capacity(500) // 默认 100
.log_statements(LevelFilter::Info);
let conn = PgConnection::connect_with(&opts).await?;
12.9 ConnectOptions trait
sqlx-core/src/connection.rs:214-265 的 ConnectOptions:
pub trait ConnectOptions: 'static + Send + Sync + FromStr<Err = Error> + Debug + Clone {
type Connection: Connection<Options = Self> + ?Sized;
fn from_url(url: &Url) -> Result<Self, Error>;
fn to_url_lossy(&self) -> Url { unimplemented!() }
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
where Self::Connection: Sized;
fn log_statements(self, level: LevelFilter) -> Self;
fn log_slow_statements(self, level: LevelFilter, duration: Duration) -> Self;
fn disable_statement_logging(self) -> Self { ... }
}
super-trait bound 很重——'static + Send + Sync + FromStr + Debug + Clone:
FromStr:URL 字符串解析。Debug + Clone:日志友好、支持多次连接(从同一 Options 建多个连接)。'static + Send + Sync:能跨线程传(Pool 需要)。
from_url 把 url::Url 解析成 Options。包括解析 scheme、host、port、user:password、database、query string 参数。Postgres 的 PgConnectOptions::from_url 转发到 parse_from_url(sqlx-postgres/src/options/parse.rs)——它并不检查 scheme(按 scheme 挑驱动是 Any 驱动的事),只读 host / port / 用户名密码 / 库名,再从 query string 读 sslmode、statement-cache-capacity、application_name、options 等参数,不认识的参数打一条 warn 后忽略,最后还会套用 .pgpass 文件。
to_url_lossy 反向——把 Options 序列化回 URL。标记 lossy 因为没有 URL 表示的配置会丢失:trait 文档只保证用户名、密码、主机、端口、库名、socket / 文件路径和 SSL 相关几项;以 Postgres 为例(build_url),日志设置、application_name、options 都不会写进 URL。trait 的默认实现是 unimplemented!(),由各驱动覆盖。
connect 是真正建立连接的方法——driver 特定的协议握手全在这里。
log_statements / log_slow_statements 是链式配置——返回 Self 支持 .log_statements(Info).log_slow_statements(Warn, Duration::from_secs(1))。
12.9.1 PgConnectOptions 的配置示例
use std::time::Duration;
use log::LevelFilter;
use sqlx::postgres::{PgConnectOptions, PgSslMode};
use sqlx::ConnectOptions;
let opts = PgConnectOptions::new()
.host("localhost")
.port(5432)
.database("myapp")
.username("user")
.password("pass")
.ssl_mode(PgSslMode::Require)
.application_name("my_service")
.statement_cache_capacity(500)
.log_statements(LevelFilter::Debug)
.log_slow_statements(LevelFilter::Warn, Duration::from_millis(100));
sqlx-postgres/src/options/ 下 PgConnectOptions 一共 27 个 pub fn(含 new 和若干 getter)——从 SSL 模式到 application_name、options 参数、Unix socket 路径都在里面。
同样口径下 MySqlConnectOptions 是 31 个、SqliteConnectOptions 是 28 个——注意并不是 Postgres 最多:SQLite 的配置面(journal mode、synchronous、busy timeout、各种 pragma、只读/内存库)和 MySQL 的(charset、collation、pipes-as-concat 等 SQL mode 开关)都不小,每家的协议与实现特性决定了各自的配置项集合。
12.10 LogSettings:日志配置
sqlx-core/src/connection.rs:186-202:
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct LogSettings {
pub statements_level: LevelFilter,
pub slow_statements_level: LevelFilter,
pub slow_statements_duration: Duration,
}
impl Default for LogSettings {
fn default() -> Self {
LogSettings {
statements_level: LevelFilter::Debug,
slow_statements_level: LevelFilter::Warn,
slow_statements_duration: Duration::from_secs(1),
}
}
}
三字段:
statements_level—— 所有 SQL 语句的日志级别(默认 Debug)。slow_statements_level—— 慢查询(超过 duration)的日志级别(默认 Warn)。slow_statements_duration—— 慢查询阈值(默认 1 秒)。
这些设置通过 ConnectOptions 的 log_statements / log_slow_statements 方法配置。每条 SQL 执行后 sqlx 的 QueryLogger(第 21 章)会检查时长、决定用哪个 level 记录。
实际效果:
// 默认 —— 每条 SQL 以 DEBUG 级别发一条 tracing 事件(target 为 "sqlx::query"),
// 耗时超过 1 秒的改用 WARN 级别,消息里带 "slow statement"
// 输出与否由 tracing_subscriber(或 log 一侧的 logger)的级别过滤决定
开发环境通常设 statements = Info 看每条 SQL;生产环境设 statements = Off、slow_statements = Warn 只记慢查询。这是 ConnectOptions::log_statements 和 log_slow_statements 两个方法的典型搭配。
12.11 PgConnection 内部结构
sqlx-postgres/src/connection/mod.rs:36-75:
pub struct PgConnection {
pub(crate) inner: Box<PgConnectionInner>,
}
pub struct PgConnectionInner {
pub(crate) stream: PgStream,
process_id: u32,
secret_key: u32,
next_statement_id: StatementId,
cache_statement: StatementCache<(StatementId, Arc<PgStatementMetadata>)>,
cache_type_info: HashMap<Oid, PgTypeInfo>,
cache_type_oid: HashMap<UStr, Oid>,
cache_elem_type_to_array: HashMap<Oid, Oid>,
pub(crate) pending_ready_for_query_count: usize,
transaction_status: TransactionStatus,
pub(crate) transaction_depth: usize,
log_settings: LogSettings,
}
PgConnection 外层是 Box<PgConnectionInner> —— Box 化之后 PgConnection 本身只有一个指针大小(8 字节)。源码没写这样做的动机,按效果推断:Inner 有十几个字段(含带缓冲的 stream 和多张 HashMap),直接内嵌的话每次 move PgConnection 都要 memcpy 整块结构——Box 化省这份开销。MySqlConnection 也是同样的 Box<MySqlConnectionInner> 结构。
12 个字段的分工:
stream: PgStream—— TCP 或 Unix socket 的缓冲 I/O stream,带 TLS 和序列化器。process_id+secret_key—— Postgres startup 时服务端发的 BackendKeyData。字段注释说"用于发送 cancel 请求",但两者都标着#[allow(dead_code)]——0.8.6 只把它们存下来,并没有实现 CancelRequest。next_statement_id—— 每次 prepare 分配的递增 ID,从 1 开始,服务端看到的语句名是sqlx_s_1、sqlx_s_2、...(sqlx-postgres/src/io/mod.rs)。cache_statement—— prepared statement 的 LRU 缓存(§12.12)。cache_type_info/cache_type_oid/cache_elem_type_to_array—— 三张类型元数据 HashMap,用户自定义类型的 OID 查询缓存(第 6 章 §6.4.5 讨论过)。pending_ready_for_query_count—— 未处理的 ReadyForQuery 消息计数——Postgres 协议的流水线跟踪。transaction_status+transaction_depth—— 事务状态机(第 15 章详细)。log_settings—— 前面讨论的日志配置。
这 12 个字段合起来描述一条 Postgres 连接的所有 in-memory state。除了为将来的 cancel 预留的那两个,每个字段都对应一项正在使用的协议需求。
12.12 StatementCache:LRU 预处理缓存
sqlx-core/src/common/statement_cache.rs:5-8:
pub struct StatementCache<T> {
inner: LruCache<String, T>,
}
基于 hashlink::LruCache 的 LRU——key 是 SQL 字符串、value 是驱动特定的 statement 句柄(Postgres 下是 (StatementId, Arc<PgStatementMetadata>))。
insert 方法(sqlx-core/src/common/statement_cache.rs:27-39):
pub fn insert(&mut self, k: &str, v: T) -> Option<T> {
let mut lru_item = None;
if self.capacity() == self.len() && !self.contains_key(k) {
lru_item = self.remove_lru(); // 满了、踢最老
} else if self.contains_key(k) {
lru_item = self.inner.remove(k); // 同 key 替换
}
self.inner.insert(k.into(), v);
lru_item
}
两种淘汰情况:
- 缓存满且 key 新——踢最 least recently used 的那条。
- key 已存在——替换旧值、返回旧值(调用方会 close 掉对应的 server-side statement)。
容量默认 100(PgConnectOptions::new() 的默认)。用户可以调:
let opts = PgConnectOptions::new().statement_cache_capacity(500);
容量选择的权衡:
- 小容量(10-50) —— memory footprint 小、但频繁 miss 导致 re-prepare 开销。
- 中等容量(100-500) —— 大多数业务的 sweet spot。
- 大容量(1000+) —— 长连接 + 高 SQL 基数场景有用,但 server 侧每个 statement 占资源。
12.13 SqliteConnection:worker 线程模型
SQLite 的 Connection 是三家里最独特的。SQLite 的 C API(sqlite3_step、sqlite3_bind_*)全部同步——调用时阻塞直到完成。sqlx 要把它包装成 async,只能让一个专用 OS 线程承载 sqlite3 handle、主线程通过 channel 发命令过去。
sqlx-sqlite/src/connection/mod.rs:58-62:
pub struct SqliteConnection {
optimize_on_close: OptimizeOnClose,
pub(crate) worker: ConnectionWorker, // 到 worker 线程的通道
pub(crate) row_channel_size: usize,
}
ConnectionWorker(sqlx-sqlite/src/connection/worker.rs:32-42)只有两个字段:
pub(crate) struct ConnectionWorker {
command_tx: flume::Sender<(Command, tracing::Span)>,
/// Mutex for locking access to the database.
pub(crate) shared: Arc<WorkerSharedState>,
}
pub(crate) struct WorkerSharedState {
transaction_depth: AtomicUsize,
cached_statements_size: AtomicUsize,
pub(crate) conn: Mutex<ConnectionState>,
}
command_tx—— 单向的有界 flume channel,往 worker 线程投命令(顺带把调用方的tracing::Span一起送过去,保证跨线程后 span 不断)。- 回程不是一条固定的 channel——需要回复的
Commandvariant 各自带着回程发送端:Prepare/Describe/Ping/ClearCache等是futures_channel::oneshot::Sender,Begin/Commit/Rollback是要求对方确认收到的rendezvous_oneshot,Execute则带一条有界 flume channel,把结果逐行流回来(容量就是row_channel_size,默认 50)。这里也没有JoinHandle字段,线程由establish时thread::Builder::spawn出去后自行运行,收到Shutdown命令或 channel 断开时退出。 shared—— 一个Arc<WorkerSharedState>:两个原子量(事务深度、缓存语句数)让is_in_transaction()/cached_statements_size()这类同步方法不用往 worker 跑一趟;conn: Mutex<ConnectionState>则是LockedSqliteHandle直接上手 sqlite3 句柄的入口。
源码顶部的注释写得很直白:"Each SQLite connection has a dedicated thread."——一条连接一个线程,紧跟着还有一条 TODO 说想改成"每个 SQLite 连接池共用一个线程池以减少 OS 资源占用",并注明优先级低。
工作流程:
sequenceDiagram
participant U as 用户代码
participant SC as SqliteConnection
participant W as worker 线程
participant SL as libsqlite3
U->>SC: query(...).execute(&mut conn).await
SC->>W: command_tx.send(Command::Execute { query, arguments, tx, .. })
W->>SL: sqlite3_prepare_v3 / bind / step
SL-->>W: rows / result
W->>SC: tx.send(行或执行结果),逐条经 flume 回传
SC-->>U: Ok(result)
每次 async 调用都是跨线程 channel 通信——命令过去、结果回来。Worker 线程专心做 C API 调用、主线程的 async task 正常跑其他 Future。
代价:
- 每次 query 多一次跨线程 channel 往返——命令过去、结果(行)经回程 channel 回来。
- 一个 OS 线程 per connection——100 条连接的池就是 100 个线程(上面那条 TODO 想解决的正是这点)。
收益:
- SQLite 的同步 API 和 async 世界解耦——用户代码里 SQLite 看起来和 Postgres/MySQL 一样异步。
- worker 线程内部可以自由调 C API——不用担心阻塞 Tokio runtime。
替代方案("真 async SQLite")需要修改 libsqlite3 本身——工作量巨大。sqlx 选了务实路径——工作线程包装。
这条设计也解释了 unsafe impl Send 出现在哪里——不是 SqliteConnection(它只持有 channel 和 Arc,本来就是 Send),而是包着裸指针的那一层:ConnectionHandle(sqlx-sqlite/src/connection/handle.rs:27)里是 NonNull<sqlite3>,几个回调 handler(sqlx-sqlite/src/connection/mod.rs:70 等)里是 NonNull<dyn FnMut…>。sqlx-sqlite/src/connection/handle.rs:18-25 的注释写明了担保条件:只要同一时刻只有一个线程在用、且 SQLite 以线程安全方式编译并启用多线程模式,sqlite3 句柄就可以在线程间移交。平时句柄只在 worker 线程里用;lock_handle() 把它借给调用方线程时,由 WorkerSharedState 里的 Mutex 保证独占。这与第 7 章 §7.8.1 的结论一致:unsafe 担保应当落在直接包着裸指针的那一层——在那里是 ValueHandle(sqlx-sqlite/src/value.rs:110-112),SqliteRow 上那两行 unsafe impl 只是 0.5.10 #1551 之后的历史遗留。
12.14 三家 Connection 对照
总结三家的结构差异:
| 维度 | PgConnection | MySqlConnection | SqliteConnection |
|---|---|---|---|
| 核心 I/O | PgStream(TCP/UDS + TLS) |
MySqlStream |
ConnectionWorker(OS 线程 + channel) |
| 状态字段 | Box<Inner> 12 字段(transaction / cache 等) |
Box<Inner> 5 字段(stream / 事务深度 / status_flags / 语句缓存 / 日志设置) |
Worker 代理 + WorkerSharedState |
| statement cache | LRU(默认 100) | LRU(默认 100) | LRU(默认 100,在 worker 持有的 ConnectionState 里) |
| 类型缓存 | 三张 HashMap(OID 等) | 无(MySQL 类型信息随列定义下发) | 无 |
| close 消息 | Terminate |
COM_QUIT |
sqlite3_close() C 调用 |
| ping 动作 | Sync → ReadyForQuery |
COM_PING → OK_Packet |
Command::Ping 走 worker channel |
| 代码量(connection/) | 2186 行(除去 describe.rs 的 681 行是 1505) | 1314 行 | 4472 行(除去 explain.rs 的 1779 行是 2693) |
Postgres Connection 最复杂——协议 + 类型缓存两层叠加;SQLite Connection 最特殊——worker 线程模型是独一份。MySQL 是中间——协议复杂但没有类型缓存。
12.15 连接建立的协议握手详解
connect_with 表面上一行调用,底下是一套复杂的协议握手。以 Postgres 为例:
sequenceDiagram
participant R as Rust 客户端
participant NS as OS 网络
participant PG as Postgres Server
R->>NS: TCP SYN (端口 5432)
NS->>PG: SYN
PG->>NS: SYN+ACK
NS->>R: SYN+ACK
R->>NS: ACK
Note over R,PG: TCP 3 次握手完成
opt SSL / TLS
R->>PG: SSLRequest (8 字节: len=8, code=80877103)
PG-->>R: 'S'(支持 SSL)
R->>PG: TLS ClientHello
PG-->>R: TLS 握手消息若干
end
R->>PG: StartupMessage(用户名、数据库名、参数)
PG-->>R: AuthenticationSASL(SCRAM-SHA-256 挑战)
R->>PG: SASLInitialResponse
PG-->>R: AuthenticationSASLContinue
R->>PG: SASLResponse
PG-->>R: AuthenticationSASLFinal
PG-->>R: AuthenticationOk
PG-->>R: ParameterStatus x N(server_version, TimeZone 等)
PG-->>R: BackendKeyData(process_id, secret_key)
PG-->>R: ReadyForQuery('I' = idle 状态)
Note over R,PG: 连接就绪,可以发 Query
一次建连要走完 TCP 三次握手、可选的 TLS 握手、StartupMessage、一轮 SASL 挑战应答,再收若干 ParameterStatus 和 BackendKeyData,最后等到 ReadyForQuery——十来条消息、多个网络往返。这就是为什么连接池如此重要——避免每次 query 都重跑这套握手。
认证机制:PostgreSQL 10 起支持 SCRAM-SHA-256,14 起它成为 password_encryption 的默认值;老部署可能用 MD5、trust、cert。每种机制对应不同的消息序列——sqlx 在 sqlx-postgres/src/connection/establish.rs:68-110 里处理明文密码、MD5 和 SASL 三种挑战,其余方法直接报错:流程错位的 SASL 消息等落到 match 末尾的兜底分支,报错格式串是 unsupported authentication method: {:?}(:104-109);GSSAPI、SSPI 这类未识别的类型更早,在解码 Authentication 消息时就报错,格式串是 unknown authentication method: {}(sqlx-postgres/src/message/authentication.rs:82-84)。sqlx-postgres/src/connection/sasl.rs 实现 SCRAM 的细节(HMAC、salted password、proof exchange)。
MySQL 握手结构类似但协议细节不同——CapabilitiesFlags 协商、Handshake V10、HandshakeResponse41、OK_Packet。sqlx-mysql/src/connection/establish.rs 实现全套。
SQLite 没有网络握手——sqlite3_open_v2 直接打开文件(或 :memory:)、初始化内部结构、准备好。但它有工作线程启动——每条连接一次 std::thread::spawn,这是本地操作,比任何网络往返都便宜。
12.16 连接池和单连接的对比
用户代码里有两种获取连接的路径——直接 Connection::connect 和通过 Pool:
// 路径 A:直接单连接
let mut conn = PgConnection::connect("postgres://...").await?;
sqlx::query("...").execute(&mut conn).await?;
conn.close().await?;
// 路径 B:通过 Pool
let pool = PgPool::connect("postgres://...").await?;
sqlx::query("...").execute(&pool).await?; // Pool 自动借连接 + 归还
什么时候用 A(直接 Connection)?
- 单次性脚本(migration、一次性维护任务)——不需要池化。
- CLI 工具——跑一会儿就退出,一个连接够了。
- 专用用途(Listener、Copy FROM/TO)——需要独占连接,不能让 Pool 自动回收。
什么时候用 B(Pool)?
- 长时间跑的服务(web server、worker)——Pool 复用连接。
- 高并发请求——Pool 管理并发 limit。
- 连接要共享(多个 async task 用同一服务)——Pool 是 Clone + Arc 友好。
本章讲的 Connection 是 A 场景的主角——直接 connect、调方法、close。第 13-14 章讲的 Pool 是 B 场景——内部管理一堆 Connection、给用户抽象成"取一条来用"。
但两者在 Executor trait 层统一——无论 A 还是 B,最终 fetch / execute 方法都是对 Connection 的操作。Pool 的 fetch 内部 pool.acquire() 拿到一条 PoolConnection、调 conn.fetch_many(query)(方法调用经 DerefMut 自动解引用到底层连接,等价于 (&mut *conn).fetch_many(query))——&mut XxxConnection 的 Executor 实现就是为了支持这条路径(见第 4 章 §4.4.1)。
12.17 连接断开与重连
生产环境里连接会因各种原因断开:
- 数据库重启——server 重启、所有连接 reset。
- 网络抖动——TCP 连接超时或 RST。
- 防火墙超时——idle 连接被中间防火墙断。
- 云数据库 maintenance——AWS RDS 的定期维护窗口。
sqlx 对断开的处理:
单 Connection 的处理:
- 下次调 fetch / execute 时,底层 stream read/write 失败——返回
Error::Io。 - 用户代码通常把
Result<_>?冒泡——调用方自己决定重建连接。
Pool 的处理(第 14 章):
- Pool 发现 connection broken(acquire 前或归还时的 ping 失败)——从池里删除、建新的。
- 坏连接若在空闲时就被 ping 发现,用户代码的视角只是这次 acquire 多等了一次建连——背后 Pool 自动恢复;但已经发到坏连接上的那条查询,错误仍会返回给用户,Pool 不会替你重试。
没有"单 Connection 自动重连"——sqlx 故意不提供。因为自动重连要处理事务中断(第 15 章讨论)——如果事务中途连接断了,sqlx 不能透明重建(数据一致性会出问题)。只有 Pool 级别的重建安全(新连接 = 新事务边界)。
12.18 Connection trait 的演进
Connection trait 从 0.3 到 0.8 的演进:
以下逐条对照各 tag 下的 sqlx-core/src/connection.rs 和 CHANGELOG.md:
- 0.3.0:
Connection上只有begin/close/ping三个方法,而且begin(self)是消费 self 的;建立连接由另一个Connect: Connectiontrait 提供。 - 0.4.0:
Connect被并回Connection——connect/connect_with进来;同时加入transaction便捷方法、cached_statements_size/clear_cached_statements、flush/should_flush;begin改成&mut self。今天这个 trait 的骨架此时已经成型。 - 0.6.0:
close_hard从close里分出来(PR #1901)——让"发 Terminate 再关"和"直接关"两条路径分开;ConnectOptions的 super-trait 补上Clone。 - 0.7.0:
shrink_buffers加入(PR #2379,和PoolConnection::close同一个 PR);ConnectOptions加from_url,并把log_statements/log_slow_statements从&mut self改成消费self的链式风格。 - 0.8.4:
is_in_transaction和begin_with一起进来(PR #3765,合并了 #3427 的get_transaction_depth()和 #3614 的begin_with)——注意这是 0.8 系列中途加的,不是 0.7 就有。
每次加方法都是从生产反馈里长出来的需求——close_hard 来自"Drop 路径发不出协议消息"这条现实;shrink_buffers 来自长连接的内存问题;begin_with 来自用户想指定隔离级别之类的数据库特有事务选项。
这条演进也显示 Connection trait 是有机生长的——没有一次性设计、而是每个版本加一两个 method。这让 sqlx 的 Connection API 始终贴近实际需要——没有过度抽象、没有"预留未来"的废方法。
12.19 常见 Connection 使用陷阱
生产代码里 Connection 相关的常见错误:
陷阱 1:忘 close 导致 server 资源泄漏
async fn do_work() {
let mut conn = PgConnection::connect(...).await.unwrap();
sqlx::query(...).execute(&mut conn).await.unwrap();
// 函数结束 conn drop —— 只发 TCP FIN 不发 Terminate
}
一次性还好;高频调用时,按 close 文档注释的提醒,服务端尚未察觉断开的连接会在短时间内继续占用连接数配额,可能撞到 max_connections。正确:conn.close().await? 显式关。
陷阱 2:handler 里直接 connect 而不用 Pool
async fn handler(database_url: String) -> Result<Json<User>> {
let mut conn = PgConnection::connect(&database_url).await?; // 每请求都握手
let user = sqlx::query_as!(User, "...").fetch_one(&mut conn).await?;
Ok(Json(user))
}
每个 HTTP 请求都做一遍 §12.15 那套完整协议握手(TCP + 可选 TLS + 认证,若干个网络往返)——handler 延迟直接加上这个。正确:handler 收 State(pool: PgPool)——Pool 内部复用连接。
陷阱 3:跨 await 持有 &mut Connection
let mut conn = pool.acquire().await?;
let mut user_stream = sqlx::query(...).fetch(&mut *conn); // PoolConnection 要先解引用
some_other_long_op().await; // conn 仍被 user_stream 借着
let user = user_stream.try_next().await?; // 需要 use futures::TryStreamExt
注意 &mut *conn:PoolConnection 本身不实现 Executor,写成 &mut conn 在 0.8.6 会报 E0277(the trait bound `&mut sqlx::pool::PoolConnection<Sqlite>: Executor<'_>` is not satisfied,实测,见第 4 章 §4.4.2)。fetch 返回的 stream 借用了 conn——别的 async 操作期间连接被占着不能给其他任务用。高并发场景 Pool 的连接数 = 并发占用数。正确:先 fetch 完再做其他 await;或者 fetch_all 收到 Vec 再释放 conn。
陷阱 4:transaction 里调 Self::close
let mut tx = conn.begin().await?;
sqlx::query(...).execute(&mut *tx).await?;
conn.close().await?; // 编译错 —— tx 借了 conn
Transaction 持有 &mut conn——close 需要 owned self——编译拒绝,报 error[E0505]: cannot move out of `conn` because it is borrowed(实测),并附一条说明:借用可能在 tx 被 drop、运行 Transaction 的 Drop 代码时还要用到。所以这条检查的是 tx 是否还活——Transaction 有 Drop,借用会延续到它被消费或离开作用域;先 tx.commit().await?(或 rollback)把 tx 消费掉,borrow 释放、close 合法。
陷阱 5:同一 connection 并发执行多条 query
let mut conn = pool.acquire().await?;
tokio::join!(
sqlx::query(...).execute(&mut *conn), // 编译错
sqlx::query(...).execute(&mut *conn),
);
&mut *conn 同一时刻只能有一个——join! 编译失败,报 error[E0499]: cannot borrow `conn` as mutable more than once at a time(实测)。SQL 协议本身是单连接串行的——并发执行必须两条连接(两次 acquire)。正确:从 pool 各 acquire 一条,并发。
这五个是 Connection 使用里最常见的坑。记住它们能避免大多数生产环境的 "奇怪问题"。
12.20 阶段小结
到这里,Connection trait 和三家实现的生命周期已经完整拆开:
- Connection trait 14 个方法(§12.2)—— 生命周期(close/close_hard/flush/should_flush)、探活(ping)、事务(begin/begin_with/is_in_transaction/transaction)、缓存管理(cached_statements_size/clear_cached_statements/shrink_buffers)、建立(connect/connect_with)。两个关联类型(Database 和 Options)把 trait 家族串起来。
- close vs close_hard(§12.3)—— 前者发 graceful 消息、后者只关 socket(发 TCP FIN)。Postgres / MySQL 连接的 drop 连 close_hard 都不调,只靠 socket 析构——因为 Drop 不能 async。
- ping 协议级探活(§12.4)—— Postgres 发
Sync等ReadyForQuery、MySQL 发COM_PING、SQLite 往 worker 投Command::Ping。 - transaction 便捷方法(§12.5)——闭包 + 自动 commit/rollback,比手写
begin / commit更安全(错误路径显式)。 - cached_statements_size / clear(§12.6)—— 受
HasStatementCachemarker trait 守护;0.8.6 里三家驱动都实现了它。 - shrink_buffers 主动释放(§12.7)—— 避免连接内存占用持续膨胀。用户需显式调。
- connect / connect_with(§12.8)—— URL 解析 vs 详细 Options 配置两条路径。
- ConnectOptions trait(§12.9)—— FromStr / Clone / Debug / Send / Sync super-trait;from_url / connect / log_* 方法。
- LogSettings(§12.10)—— statements_level + slow_statements_level + slow_statements_duration。
- PgConnection 内部 12 字段(§12.11)——
Box<Inner>让 PgConnection 本身只有指针大小;字段包括 stream、statement cache、三张 type cache、transaction state、log_settings。 - StatementCache LRU(§12.12)—— 默认容量 100,满了踢最老、同 key 替换返回旧值供 server-side close。
- SqliteConnection worker 线程(§12.13)—— 独一份的 OS 线程模型(一条连接一个线程),把同步 C API 包装成 async;代价是每次操作多一次跨线程 channel 往返。
12.21 Connection 方法的典型调用频率
把本章讨论的 Connection 方法按"谁在调、大概多频繁"排一排(定性,不是实测数据):
| 方法 | 相对频次 | 典型调用方 |
|---|---|---|
| Executor::fetch_*(间接) | 最高 | 每条业务查询 |
ping |
每次从空闲队列 acquire(默认开启)+ 每次归还 | Pool 的 test_before_acquire 与归还检查 |
begin / transaction |
每个事务一次 | 事务边界(写操作) |
connect / connect_with |
建池 + 补充连接时 | Pool 扩容 / 连接被淘汰后 |
close |
连接退休 / 应用关闭时 | Pool 淘汰、优雅停机 |
cached_statements_size |
偶尔 | 监控 / debug |
shrink_buffers |
按需 | 大查询后手动调 |
clear_cached_statements |
极少 | DDL 后 / 长连接内存压力 |
close_hard |
用户从不直接调 | #[doc(hidden)],Pool 内部用 |
一条观察:业务代码几乎 100% 时间在调 Executor 方法(fetch/execute),Connection 自身的方法是辅助管理——连接建立、事务开启、健康检查。这也是为什么 Connection trait 的14 个方法里用户直接调的只有 2-3 个(connect、begin、偶尔 close)——其他全是被 Pool / Executor 等高层间接调。
这条分布给你生产 profiling 的优先级:
- 性能问题优先查 fetch 路径(第 16-18 章的驱动实现)。
- 连接数问题查 connect / close 节奏(第 13-14 章的 Pool)。
- 内存问题查 statement cache / shrink_buffers(本章)。
按这个优先级定位问题比盲目 profile 所有 Connection 方法快得多。
12.22 Connection 设计的三条原则
本章讲的 Connection trait 可以萃取出三条可复用的 API 设计原则:
1. 显式 close 优于依赖 Drop。sqlx 的 close().await 鼓励用户显式关连接——而不是让 drop 悄悄做。这是"用方法表达副作用"的原则——任何有网络 I/O 的关闭动作都应该有显式方法、让用户意识到"这里有远程操作"。Drop 只做最小必要清理。
2. Trait bound 守护能力差异。cached_statements_size / clear_cached_statements / Query::persistent 都用 where DB: HasStatementCache 守护。注意 0.8.6 里三家 in-tree 驱动(含 SQLite)都实现了这个 marker(§12.6),所以这道门槛实际拦住的是不实现它的第三方驱动——在那种驱动上,这几个方法编译期就不存在。这比"运行时返回 NotSupported 错误"优雅得多——把能力差异编码进类型系统。Rust 的 trait bound 让这件事低成本可行。
3. 提供便捷方法但不废除原始方法。transaction(closure) 便捷 + begin 原始两者并存——用户按场景选。便捷方法降低常见路径的代码量、原始方法保持灵活性。不强迫所有人用便捷版——因为总有闭包无法表达的场景(比如跨 transaction 复杂控制流)。
这三条对 Rust 下设计"网络资源"trait 都适用。sqlx 的 Connection 是此类设计的良好范例——协议完整性、类型安全、灵活性三者兼得。
12.23 Connection 的总体定位
第四部分"连接与事务"的起点是 Connection trait——它是 sqlx 整个架构的底层资源抽象:
Application (handlers, business logic)
↓
Query / QueryBuilder / query! (第 9-11 章)
↓
Executor trait (第 4 章)
↓
Pool ← 第 13-14 章
↓
Transaction ← 第 15 章
↓
Connection ← 本章
↓
Driver-specific stream / worker
↓
Database server
Connection 在这条栈里扮演"资源对等体"——上层(Executor / Pool / Transaction)都是在 Connection 之上构造的抽象。理解 Connection 的生命周期,你才能理解 Pool 的 Acquire/Release 为什么要小心、Transaction 的 Drop 为什么只能"尽力 rollback"——所有这些都绕不开 Connection 本身的协议约束。
这也是为什么第四部分从 Connection 开始——把"最底的底"讲清楚,之后的 Pool / Transaction 讨论都建立在这个基础上。读完本章你对"一条 Postgres/MySQL/SQLite 连接到底是什么"有了具体答案——不只是 TCP socket,而是一整套协议状态 + 本地缓存 + 生命周期礼仪。
12.24 跨 DB 初始化的一致性对比
把三家 DB 的初始化代码放一起有助于直观理解它们的共性:
// Postgres
use sqlx::postgres::{PgConnectOptions, PgSslMode};
use sqlx::ConnectOptions;
let opts = PgConnectOptions::new()
.host("localhost").port(5432).database("mydb")
.username("user").password("pass")
.ssl_mode(PgSslMode::Require);
let mut conn = opts.connect().await?;
// MySQL
use sqlx::mysql::MySqlConnectOptions;
let opts = MySqlConnectOptions::new()
.host("localhost").port(3306).database("mydb")
.username("user").password("pass");
let mut conn = opts.connect().await?;
// SQLite
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqliteSynchronous};
let opts = SqliteConnectOptions::new()
.filename("mydb.sqlite")
.create_if_missing(true)
.journal_mode(SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal);
let mut conn = opts.connect().await?;
共同模式:XxxConnectOptions::new() + 链式配置 + .connect().await?。用户一旦学会一家,其他两家看配置列表就能上手。
差异:
- Postgres:SSL 模式分 6 档(比 MySQL 多一个
allow),另有 application_name、extra_float_digits;statement_timeout这类服务端会话参数没有专门方法,要经options()以-c形式传。 - MySQL:SSL 模式 5 档,另有 charset、collation、timezone、pipes_as_concat 等会话设置。
- SQLite:文件相关(filename、create_if_missing)+ journal mode。没有网络配置。
这条对比显示 sqlx 的一致 API 跨 DB 能力落到 Connection 层——配置方法可能不同(因为 DB 差异是本质的)但调用路径和链式风格统一。用户代码可以通过泛型 fn do_work<DB: Database>(pool: &Pool<DB>) 写跨 DB 的业务逻辑,只在初始化阶段(本章主题)选具体驱动。
12.25 Connection 与更底层的 I/O 抽象
Connection 方法背后的 I/O 路径最终落到运行时的 socket 上,但中间隔着 sqlx 自己的一层抽象:sqlx-core 定义了 Socket trait(sqlx-core/src/net/socket/mod.rs:15),把不同运行时、TCP/UDS、明文/TLS 全部抹平成同一个接口。以 PgConnection 的 PgStream 为例(sqlx-postgres/src/connection/stream.rs:28-40):
PgStream // 序列化/解析 Postgres 消息
└── BufferedSocket<Box<dyn Socket>> // sqlx-core 的读写缓冲(默认 8192 字节)
└── dyn Socket // TLS 包装后的 socket,或明文 socket
└── tokio::net::TcpStream / UnixStream
└── OS socket 系统调用
源码里对这处 Box<dyn Socket> 还专门留了注释:"这里用 trait object 没问题,缓冲把动态调用和系统调用的开销都摊平了。"每一层都是薄包装——PgStream 负责协议消息、BufferedSocket 负责批量 I/O、TLS 层负责加密、TcpStream 负责 socket。
这套分层让 sqlx 的协议实现(sqlx-postgres/src/message/)不用关心加密 / 缓冲 / socket 细节——只关心"按协议格式读写字节"。这种职责分离是 Rust async 生态能力的典型体现——tokio + rustls + sqlx 各司其职、组合出一条可靠的协议栈。
读懂这条分层,你在遇到连接相关的性能问题(比如"为什么我这条查询慢 20ms")时能知道去哪看:
- TLS 握手慢?——查 rustls 或
PgConnectOptions::ssl_mode。 - 网络 RTT 慢?——查网络层(不是 sqlx 问题)。
- 协议序列化慢?——sqlx-postgres 的 message 模块(第 16 章)。
- statement cache 不命中?——调大 cache capacity。
知道抽象栈的每一层承担什么,比盲目怀疑任何一层都更高效。Connection 是这条栈在 sqlx 里的顶层入口——本章讲完了它的全貌、为后续几章讨论 Pool / Transaction / 驱动实现奠基。
12.26 Connection 与 tokio 生态的对比
把 sqlx 的 Connection 放到 tokio 生态里的类似资源抽象对照:
| 资源类型 | 提供者 | trait 名 | 关闭方式 |
|---|---|---|---|
| TCP 连接 | tokio | AsyncRead + AsyncWrite |
shutdown().await |
| HTTP 连接(服务端) | hyper 1.x | server::conn::http1::Connection(需要驱动的 Future) |
graceful_shutdown() |
| Redis 连接 | redis-rs | ConnectionLike |
手动 drop |
| sqlx DB 连接 | sqlx | Connection |
close().await |
一个共性:凡是关闭时要和对端打招呼的连接(TCP 的半关闭、HTTP 的优雅停机、数据库的 Terminate),都得提供显式的关闭方法——因为 Drop 不能做 async;没有提供的(如表中的 redis-rs),就只能靠 drop 关 socket。sqlx 的 Connection 属于前一类。
一个不同:其他库的 Connection 多数不暴露 statement cache / buffer shrink 这类方法——因为它们的协议不走"prepared statement"路径(HTTP 无状态、Redis 每次发 command、Kafka 长连接但不预处理)。只有 SQL DB 有这条需求——prepared statement 是 SQL 协议特有的 feature。
这条对比让你理解 Connection trait 的独特职责——它不只是"能收发数据"的 socket wrapper,而是"能跨多条命令保持状态"的对话管理者。SQL 协议的特殊性(事务、prepared、session-level 设置)决定了 Connection trait 需要比一般连接 trait 更丰富的方法集。这也是 sqlx-core 的 Connection 有 14 个方法、而 tokio 的 AsyncRead+AsyncWrite 加起来只有 4 个必须实现的 poll 方法(poll_read、poll_write、poll_flush、poll_shutdown)的原因——抽象丰富度对应被抽象领域的复杂度。
12.27 本章收尾的六个自检题
读完本章,测试你对 Connection 的理解:
close()和drop()的区别是什么?——前者发 graceful 消息、后者只关 socket。Drop 不能 async 所以不能发消息。- 为什么 SQLite 需要 worker 线程而 Postgres 不需要?——SQLite C API 同步、阻塞 Tokio runtime;Postgres 走网络协议,socket 读写本身就能用异步 I/O(如 tokio::net::TcpStream)完成。
statement_cache_capacity调大调小的权衡?——大容量 miss 率低但服务端每条 prepared statement 都占资源;小容量反之。三家驱动默认都是 100。- 用户什么时候应该显式调
ping()?——几乎从不直接调——它是 Pool 的 test_before_acquire 机制内部用的。 begin()和begin_with("BEGIN ISOLATION LEVEL ...")的区别?——前者用默认BEGIN、后者用自定义语句——典型用于指定事务隔离级别。- PgConnection 为什么内层包
Box<PgConnectionInner>?——源码没写动机;效果是 PgConnection 本身只有指针大小,不管 Inner 有多大,移动都便宜。
12.28 Connection 层面的调优 checklist
生产环境中一些 Connection 层面能做的调优:
1. statement_cache_capacity 按业务规模调。默认 100 够大多数中型服务;上千种 SQL 模式的大型服务改到 500-1000。改之前先观察:cached_statements_size() 只能看到当前缓存了几条,看不到命中率——如果它长期顶在容量上限,说明 LRU 在不断驱逐,才值得调大。
2. 开 log_slow_statements 帮助捕获慢查询。默认 1 秒阈值可以缩到 100ms(对 OLTP 场景合适)。生产日志里看到 WARN 级别的 slow query 比等用户投诉快得多。
3. 关闭 log_statements 在生产。默认 Debug 级别在本地好用;生产每条 SQL 都记日志量巨大、大部分没价值。设 LevelFilter::Off——只靠 slow_statements 抓异常。
4. 给 PgConnectOptions 设 application_name。让 Postgres 的 pg_stat_activity 能区分不同服务的连接——监控连接排查必备。
5. 用 Unix socket 代替 TCP(本机 DB 场景)。PgConnectOptions::socket("/var/run/postgresql") 显式指定,或者按 libpq 的约定让 host 以斜杠开头(PgConnectOptions::host 的文档注释写明了这条规则:以 / 开头的 host 表示 Unix domain socket 所在目录)。省掉 TCP/IP 协议栈这一层。
6. 大消息之后 shrink_buffers。读过一行 10MB 的数据(或发过一条很长的 SQL)后,连接 buffer 会保持在 10MB 量级——归还 Pool 前调 shrink_buffers 让下次 acquire 从小 buffer 开始(SQLite 上是空操作)。
7. 关键路径考虑关 ping。test_before_acquire 默认 true(sqlx-core/src/pool/options.rs:149),每次从空闲队列 acquire 都多一个往返(归还时那次 ping 另算,关不掉)。延迟敏感 API 路径可以 .test_before_acquire(false)——靠拿到坏连接后失败、Pool 自动重建兜底。
8. 超时配置心里有数。PoolOptions 的默认值是 acquire_timeout 30 秒、idle_timeout 10 分钟、max_lifetime 30 分钟(sqlx-core/src/pool/options.rs:159-162)。注意没有独立的 "connect_timeout"——建连超时被算在 acquire_timeout 里(PoolOptions 的 Debug 实现甚至还把这个字段打印成 connect_timeout)。30 秒的 acquire 超时对面向用户的 API 通常偏长,值得按 SLA 调小。
这八条 checklist 是 Connection / Pool 配置里最值得先看的几项。不是每条都要做——按业务场景选 3-5 条合适的即可。过度调优反而可能引入问题(比如 statement cache 过大撑爆 server 资源)。按实际指标驱动调优才是可持续的做法。
下一章进入 Pool 的外部 API——PoolOptions / acquire / try_acquire / close / fetch_*——看连接池如何管理一群 Connection 的生命周期、分配与归还。