SQLx 源码深度解析

第12章 Connection trait:connect / close / ping 的生命周期

作者 杨艺韬 · 11,945 字 · 发布于 · 更新于

Connection 用 14 个方法描述一条连接的完整生命周期。其中最微妙的是 close 与 close_hard 的分野——因为 Drop 不能 async,也就发不出那条 graceful shutdown 消息。

本章要点

  • Connection trait(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) vs close_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 开事务——默认走 ANSI BEGIN;begin_with(statement) 允许自定义(比如 BEGIN ISOLATION LEVEL SERIALIZABLE)。返回 Transaction<'_, DB> 一个 RAII guard(第 15 章详细)。
  • transaction(closure) 便捷方法——给一个闭包、内部自动 begin / callback / commit-or-rollback。用户无需手动 begin/commit 两个方法。
  • ConnectOptions trait——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) 做的事:

  1. 发服务端特定的 "我要走了" 消息:
    • Postgres:Terminate 消息(类型字节 'X',后面只有 4 字节长度、没有载荷)。
    • MySQL:COM_QUIT。
    • SQLite:没有服务端,close 先按配置执行 PRAGMA optimize,再给 worker 线程发 Shutdown 命令,由 worker 析构连接状态时调 sqlite3_close()(C API)。
  2. 不等服务端回应,紧接着 shutdown() 关闭 socket(Postgres 协议本就规定前端发完 Terminate 立即关连接;MySQL 的 COM_QUIT 服务端可能直接断开也可能回一个 OK,sqlx 都不读)。

close_hard(self) 做的事:

  1. 不发协议消息。
  2. 直接 shutdown() 关闭 socket(让服务端靠 FIN 发现客户端走了)。

为什么需要两个? 源码 close 的文档注释说得很清楚(sqlx-core/src/connection.rs:24-27):

While connections can simply be dropped to clean up local resources, the Drop handler 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。什么时候用?

  1. long-running 连接内存占用——把 statement_cache_capacity 调到上千的长连接,缓存会一直占着这些语句——手动 clear 回收。
  2. schema 变更后——DDL 改了列类型,旧的 prepared statement 基于旧 schema、继续用会"列类型不匹配"。clear 强制下次重新 prepare。
  3. 测试场景——测试之间清空缓存隔离状态。

大多数生产代码不调——让 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
}

两种淘汰情况:

  1. 缓存满且 key 新——踢最 least recently used 的那条。
  2. 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——需要回复的 Command variant 各自带着回程发送端: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。

代价:

  1. 每次 query 多一次跨线程 channel 往返——命令过去、结果(行)经回程 channel 回来。
  2. 一个 OS 线程 per connection——100 条连接的池就是 100 个线程(上面那条 TODO 想解决的正是这点)。

收益:

  1. SQLite 的同步 API 和 async 世界解耦——用户代码里 SQLite 看起来和 Postgres/MySQL 一样异步。
  2. 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: Connection trait 提供。
  • 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 和三家实现的生命周期已经完整拆开:

  1. 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 家族串起来。
  2. close vs close_hard(§12.3)—— 前者发 graceful 消息、后者只关 socket(发 TCP FIN)。Postgres / MySQL 连接的 drop 连 close_hard 都不调,只靠 socket 析构——因为 Drop 不能 async。
  3. ping 协议级探活(§12.4)—— Postgres 发 Sync 等 ReadyForQuery、MySQL 发 COM_PING、SQLite 往 worker 投 Command::Ping。
  4. transaction 便捷方法(§12.5)——闭包 + 自动 commit/rollback,比手写 begin / commit 更安全(错误路径显式)。
  5. cached_statements_size / clear(§12.6)—— 受 HasStatementCache marker trait 守护;0.8.6 里三家驱动都实现了它。
  6. shrink_buffers 主动释放(§12.7)—— 避免连接内存占用持续膨胀。用户需显式调。
  7. connect / connect_with(§12.8)—— URL 解析 vs 详细 Options 配置两条路径。
  8. ConnectOptions trait(§12.9)—— FromStr / Clone / Debug / Send / Sync super-trait;from_url / connect / log_* 方法。
  9. LogSettings(§12.10)—— statements_level + slow_statements_level + slow_statements_duration。
  10. PgConnection 内部 12 字段(§12.11)—— Box<Inner> 让 PgConnection 本身只有指针大小;字段包括 stream、statement cache、三张 type cache、transaction state、log_settings。
  11. StatementCache LRU(§12.12)—— 默认容量 100,满了踢最老、同 key 替换返回旧值供 server-side close。
  12. 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 的理解:

  1. close() 和 drop() 的区别是什么?——前者发 graceful 消息、后者只关 socket。Drop 不能 async 所以不能发消息。
  2. 为什么 SQLite 需要 worker 线程而 Postgres 不需要?——SQLite C API 同步、阻塞 Tokio runtime;Postgres 走网络协议,socket 读写本身就能用异步 I/O(如 tokio::net::TcpStream)完成。
  3. statement_cache_capacity 调大调小的权衡?——大容量 miss 率低但服务端每条 prepared statement 都占资源;小容量反之。三家驱动默认都是 100。
  4. 用户什么时候应该显式调 ping()?——几乎从不直接调——它是 Pool 的 test_before_acquire 机制内部用的。
  5. begin() 和 begin_with("BEGIN ISOLATION LEVEL ...") 的区别?——前者用默认 BEGIN、后者用自定义语句——典型用于指定事务隔离级别。
  6. 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 的生命周期、分配与归还。