Rust in Everything
基于 sqlx 0.9.0

事务

主线位置:查询主线和连接池主线的交叉点。pool.begin() 先走连接池主线 acquire 一条连接,再在上面发 BEGIN;事务里的每条查询照常走查询主线,执行器是 &mut *tx,也就是事务里那条 PgConnection。事务结束(commit、rollback 或 Drop)之后,PoolConnection 被 drop,回到连接池主线的归还那一段。

flowchart TB
  beginnode["begin()"] --> depth{"transaction_depth"}
  depth -->|"0"| b1["Query BEGIN"]
  depth -->|"N 大于 0"| b2["SAVEPOINT _sqlx_savepoint_N"]
  b1 --> inc["深度加一"]
  b2 --> inc
  inc --> work["&mut *tx 上跑查询"]
  work --> commitnode["commit()"]
  work --> rb["rollback()"]
  work --> dropnode["drop 没提交"]
  commitnode --> c1["COMMIT 或 RELEASE SAVEPOINT"]
  rb --> r1["ROLLBACK 或 ROLLBACK TO SAVEPOINT"]
  dropnode --> q["ROLLBACK 排进写缓冲"]
  q --> next["下次用连接时发出"]
  q --> ping["归还时 ping 一并发出"]

代码只有两个文件:与数据库无关的 sqlx-core/src/transaction.rs(约 300 行,一半是被注释掉的 Executor 实现),和 Postgres 的 sqlx-postgres/src/transaction.rs(约 100 行)。为什么事务要写 &mut *tx 才能当执行器、Acquire 怎么让一个函数同时收池和事务,见 Executor 与 Acquire。

Transaction:一条连接加一个 open 标记

Transaction 只有两个字段:

pub struct Transaction<'c, DB>
where
    DB: Database,
{
    connection: MaybePoolConnection<'c, DB>,
    open: bool,
}

MaybePoolConnection 是二选一:借来的 &'c mut DB::Connection,或者一条自己拥有的 PoolConnection<DB>。conn.begin() 借连接,事务的生命周期绑在 conn 上;Pool::begin 先 acquire 一条再交进去,所以返回 Transaction<'static, DB>,可以存进结构体、跨 await 传来传去。两种情况都靠 Deref<Target = DB::Connection> 把事务当连接用。

事务有没有开、开了几层,不记在 Transaction 上,记在连接上:PgConnection 的 inner.transaction_depth。Transaction 只记自己这一层还要不要在 Drop 时回滚(open)。Connection::is_in_transaction 读的就是这个深度:

    #[inline]
    fn is_in_transaction(&self) -> bool {
        <Self::Database as Database>::TransactionManager::get_transaction_depth(self) != 0
    }

Transaction::begin、commit、rollback 都转给驱动的 TransactionManager。这个 trait 文档里写着只给实现 Connection 的人用,0.9 起 sqlx 门面也不再重新导出它(CHANGELOG #3821)。commit 和 rollback 按值收 self:成功后把 open 设成 false,然后 self 被 drop,Drop 看到 open == false 什么也不做。

深度决定发什么

三条 SQL 都由深度算出来(begin_ansi_transaction_sql 和它后面两个函数):

pub fn begin_ansi_transaction_sql(depth: usize) -> SqlStr {
    if depth == 0 {
        "BEGIN".into_sql_str()
    } else {
        AssertSqlSafe(format!("SAVEPOINT _sqlx_savepoint_{depth}")).into_sql_str()
    }
}

pub fn commit_ansi_transaction_sql(depth: usize) -> SqlStr {
    if depth == 1 {
        "COMMIT".into_sql_str()
    } else {
        AssertSqlSafe(format!("RELEASE SAVEPOINT _sqlx_savepoint_{}", depth - 1)).into_sql_str()
    }
}

begin 拿的是开始前的深度,commit / rollback 拿的是当前深度,所以减一之后两边的名字对得上:

开始前深度begin 发开始后深度commit 发rollback / Drop 发
0BEGIN1COMMITROLLBACK
1SAVEPOINT _sqlx_savepoint_12RELEASE SAVEPOINT _sqlx_savepoint_1ROLLBACK TO SAVEPOINT _sqlx_savepoint_1
2SAVEPOINT _sqlx_savepoint_23RELEASE SAVEPOINT _sqlx_savepoint_2ROLLBACK TO SAVEPOINT _sqlx_savepoint_2

Postgres 的 PgTransactionManager::begin:

    async fn begin(conn: &mut PgConnection, statement: Option<SqlStr>) -> Result<(), Error> {
        let depth = conn.inner.transaction_depth;

        let statement = match statement {
            // custom `BEGIN` statements are not allowed if we're already in
            // a transaction (we need to issue a `SAVEPOINT` instead)
            Some(_) if depth > 0 => return Err(Error::InvalidSavePointStatement),
            Some(statement) => statement,
            None => begin_ansi_transaction_sql(depth),
        };

        let rollback = Rollback::new(conn);
        rollback.conn.queue_simple_query(statement.as_str())?;
        rollback.conn.wait_until_ready().await?;
        if !rollback.conn.in_transaction() {
            return Err(Error::BeginFailed);
        }
        rollback.conn.inner.transaction_depth += 1;
        rollback.defuse();

        Ok(())
    }

几个要点:

  • queue_simple_query 把一条 Query 消息写进写缓冲、把待收的 ReadyForQuery 计数加一,wait_until_ready 冲刷缓冲、一直读到计数归零。事务控制语句走简单查询协议,不进语句缓存。
  • 成功与否看的是服务端:ReadyForQuery 的状态字节(I 空闲、T 事务中、E 事务出错)存进 transaction_status,in_transaction 只在 T 时为真。消息格式见 协议消息。
  • 深度在服务端确认之后才加一。
  • commit 和 rollback 用 conn.execute(SqlStr):SqlStr 的 take_arguments 返回 None,同样是一条 Query 消息(Execute 的分界线)。深度为 0 时它们什么都不发,直接返回 Ok(())。

实验:线上的 BEGIN、SAVEPOINT 和 COMMIT

实验用 一次查询的全过程 里的 TCP 代理打印每条协议消息,池只有一条连接(src/bin/tx_wire.rs,M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

let mut tx = pool.begin().await?;
sqlx::query("insert into items values ($1)").bind(1_i64).execute(&mut *tx).await?;
let sp = tx.begin().await?;
sp.commit().await?;
let sp = tx.begin().await?;
sp.rollback().await?;
tx.commit().await?;

每一步之间睡 200 毫秒再打印一行说明:

--- A: begin / 嵌套 begin / commit / rollback ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Query("BEGIN")
  [conn 1] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
depth 1 begin 返回
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
  [conn 1] C->S Query("SAVEPOINT _sqlx_savepoint_1")
  [conn 1] S->C CommandComplete("SAVEPOINT"), ReadyForQuery(T)
depth 2 begin 返回
  [conn 1] C->S Query("RELEASE SAVEPOINT _sqlx_savepoint_1")
  [conn 1] S->C CommandComplete("RELEASE"), ReadyForQuery(T)
depth 2 commit 返回
  [conn 1] C->S Query("SAVEPOINT _sqlx_savepoint_1")
  [conn 1] S->C CommandComplete("SAVEPOINT"), ReadyForQuery(T)
  [conn 1] C->S Query("ROLLBACK TO SAVEPOINT _sqlx_savepoint_1")
  [conn 1] S->C CommandComplete("ROLLBACK"), ReadyForQuery(T)
depth 2 rollback 返回
  [conn 1] C->S Query("COMMIT")
  [conn 1] S->C CommandComplete("COMMIT"), ReadyForQuery(I)
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
depth 1 commit 返回
  • 开头的 Sync 是 acquire 时的 ping(test_before_acquire 默认开),结尾的 Sync 是 PoolConnection 归还时的 ping:tx.commit() 消费了 tx,里面的 PoolConnection 随之被 drop。
  • 事务里的 INSERT 照常走预备语句(Bind 到缓存里的 sqlx_s_1),事务控制语句全是 Query。
  • 第二次嵌套 begin 又叫 _sqlx_savepoint_1:名字只看深度,同一深度上先后开的保存点重名,前一个已经 RELEASE 了,不冲突。
  • ReadyForQuery 的状态从 I 变 T,COMMIT 之后回到 I。

Drop:只能排队

没调 commit 也没调 rollback 的事务被 drop 时,Drop 调驱动的 start_rollback:

    fn drop(&mut self) {
        if self.open {
            // starts a rollback operation

            // what this does depends on the database but generally this means we queue a rollback
            // operation that will happen on the next asynchronous invocation of the underlying
            // connection (including if the connection is returned to a pool)

            DB::TransactionManager::start_rollback(&mut self.connection);
        }
    }

Postgres 的 start_rollback 是同步的,只写缓冲、改计数:

    fn start_rollback(conn: &mut PgConnection) {
        if conn.inner.transaction_depth > 0 {
            conn.queue_simple_query(
                rollback_ansi_transaction_sql(conn.inner.transaction_depth).as_str(),
            )
            .expect("BUG: Rollback query somehow too large for protocol");

            conn.inner.transaction_depth -= 1;
        }
    }

ROLLBACK 躺在写缓冲里,深度已经减了一,pending_ready_for_query_count 加了一。真正发出去要等下一个会 await 这条连接的调用:任何查询在发自己的消息之前都先 wait_until_ready,归还连接时的 ping 也是 write_sync 加 wait_until_ready。

📝NOTE
没有蠢问题

问: 为什么不在 Drop 里直接把 ROLLBACK 发出去、等它完成?

答: Drop::drop 是同步函数,里面不能 await。要等网络来回,只能阻塞当前线程(异步运行时的工作线程不该这样用),或者 spawn 一个任务去发(任务得拿走连接,可 conn.begin() 借来的是 &mut PgConnection,搬不走)。排进写缓冲对两种连接都适用:借来的连接由持有者下次使用时冲刷,池连接由 return_to_pool 里的 ping 冲刷。

实验:ROLLBACK 什么时候上线

接着上面的程序,B 段在池连接上、C 段在一条单独 connect 出来的 PgConnection 上,各插一行然后直接 drop(tx):

// B: pool connection
let mut tx = pool.begin().await?;
sqlx::query("insert into items values ($1)").bind(2_i64).execute(&mut *tx).await?;
drop(tx);

// C: bare PgConnection
let mut tx = conn.begin().await?;
sqlx::query("insert into items values ($1)").bind(3_i64).execute(&mut *tx).await?;
drop(tx);
println!("is_in_transaction = {}", conn.is_in_transaction());
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let n: i64 = sqlx::query_scalar("select count(*) from items").fetch_one(&mut conn).await?;
--- B: 池连接上的 Transaction 不提交就 drop ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Query("BEGIN")
  [conn 1] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
drop(tx)
  [conn 1] C->S Query("ROLLBACK"), Sync
  [conn 1] S->C CommandComplete("ROLLBACK"), ReadyForQuery(I)
  [conn 1] S->C ReadyForQuery(I)
drop 之后 200ms
--- C: 裸 PgConnection 上不提交就 drop ---
  [conn 2] C->S Query("BEGIN")
  [conn 2] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
drop(tx)
is_in_transaction = false
drop 之后 500ms,下一次使用:
  [conn 2] C->S Query("ROLLBACK")
  [conn 2] S->C CommandComplete("ROLLBACK"), ReadyForQuery(I)
  [conn 2] C->S Parse(sqlx_s_3, "select count(*) from items"), Describe(S, sqlx_s_3), Sync
  [conn 2] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_3), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
count = 1
  • 池连接:drop(tx) 先跑 Transaction::drop(排进 ROLLBACK),再 drop 字段里的 PoolConnection,后者把归还任务 spawn 出去。归还时的 ping 写一个 Sync,wait_until_ready 把缓冲里的 Query("ROLLBACK") 和 Sync 一次写出,读两个 ReadyForQuery。这一步的顺序和代价见 归还、Drop 与维护任务。
  • 裸连接:500 毫秒里线上什么都没有,服务端这段时间一直处在事务里,INSERT 拿的行锁也一直没放。客户端这边 is_in_transaction() 已经是 false,因为深度在排队时就减掉了。直到下一条查询,ROLLBACK 才赶在它的 Parse 前面发出去。
  • count = 1:表里只有 A 段提交的那一行,B、C 插的都回滚了。没提交的事务被 drop 不报任何错,写入静默消失。

begin_with:自定义开头

Connection::begin_with 和 Pool::begin_with 把整条开头语句交给你,用来带隔离级别、READ ONLY、DEFERRABLE 这些选项。它在 CHANGELOG 里记在 0.8.4 下(#3614),0.8.4 被撤回,第一个带它的 tag 是 v0.8.5。0.9 的参数是 impl SqlSafeStr,format! 出来的字符串要包 AssertSqlSafe(见 Query 与 SqlStr)。

文档写了两种报错:「Returns an error if the connection is already in a transaction or if statement does not put the connection into a transaction.」对应上面 begin 里的两个分支:深度大于 0 时直接返回 Error::InvalidSavePointStatement,一条消息都不发;语句跑完服务端状态不是 T 时返回 Error::BeginFailed。它不检查语句是不是以 BEGIN 开头,只看 ReadyForQuery 的状态字节。

// D
let mut tx = conn.begin_with("BEGIN ISOLATION LEVEL SERIALIZABLE").await?;
let level: String = sqlx::query_scalar("select current_setting('transaction_isolation')")
  .fetch_one(&mut *tx)
  .await?;
tx.commit().await?;

// E
let r = conn.begin_with("SELECT 1").await;
--- D: begin_with 指定隔离级别 ---
  [conn 2] C->S Query("BEGIN ISOLATION LEVEL SERIALIZABLE")
  [conn 2] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 2] C->S Parse(sqlx_s_4, "select current_setting('transaction_isolation')"), Describe(S, sqlx_s_4), Sync
  [conn 2] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(T)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_4), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, DataRow(列长 12), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(T)
  [conn 2] C->S Query("COMMIT")
  [conn 2] S->C CommandComplete("COMMIT"), ReadyForQuery(I)
transaction_isolation = serializable
--- E: begin_with 不是 BEGIN ---
  [conn 2] C->S Query("SELECT 1")
  [conn 2] S->C RowDescription, DataRow(列长 1), CommandComplete("SELECT 1"), ReadyForQuery(I)
结果 Err(BeginFailed)

SELECT 1 照样发给了服务端、执行完了,返回的 ReadyForQuery(I) 让 begin 判定失败。InvalidSavePointStatement 那一支的后果比报错本身更麻烦,留作下面的动脑筋。

❗IMPORTANT
动脑筋:事务里再调 begin_with,表里最后剩什么?

接着上面的程序(同一条裸连接,表 items 里此时只有 1。M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

let mut tx = conn.begin().await?;
sqlx::query("insert into items values ($1)").bind(4_i64).execute(&mut *tx).await?;
let r = tx.begin_with("BEGIN ISOLATION LEVEL SERIALIZABLE").await;
println!("结果 {:?}", r.map(|_| ()));
println!("外层 is_in_transaction = {}", tx.is_in_transaction());
sqlx::query("insert into items values ($1)").bind(5_i64).execute(&mut *tx).await?;
let c = tx.commit().await;
println!("外层 commit 结果 {c:?}");

begin_with 返回 Err(InvalidSavePointStatement) 是确定的。外层的 commit 返回什么?最后表里是 [1, 4, 5] 吗?

取消和失败:open 先于 BEGIN

Transaction::begin 在调驱动之前就把 open 设成了 true:

        Box::pin(async move {
            let mut tx = Self {
                connection: conn,

                // If the call to `begin` fails or doesn't complete we want to attempt a rollback in case the transaction was started.
                open: true,
            };

            DB::TransactionManager::begin(&mut tx.connection, statement).await?;

            Ok(tx)
        })

意图写在注释里:begin 失败或者 future 半路被丢掉,tx 被 drop,Drop 看到 open 去回滚「可能已经开了的事务」。Postgres 驱动内部还有一层同样用途的守卫:Rollback 在 defuse 之前被 drop 也会调 start_rollback。

📝NOTE
冷知识:open 先于 BEGIN 是 0.9 才改的

0.8.6 的 Transaction::begin 先 await 驱动的 begin,成功了才构造 Self { connection: conn, open: true }。#3980(首次发布于 0.9.0)把顺序倒过来,说明里写道:「Previously if the transaction was dropped while the transaction was being set up it was possible that the transaction was successfully opened but not closed. In common usage this would result in returning an open transaction to the connection pool」,改法是「constructing the Transaction object (which activates the drop handler) before starting to open the transaction」,并认为最坏情况只是「trying to ROLLBACK a transaction that was never started」。它修的是 SQLite 上报告的 #3932。

两层守卫最后都调 start_rollback,而 start_rollback 只看深度。深度要等服务端确认 BEGIN 之后才加一,于是:

  • 深度 0 时取消 begin:BEGIN 已经在写缓冲里或已经发出,深度还是 0,两层守卫都什么也不排。连接留在服务端事务里,客户端以为没有事务。
  • 深度 N 时 begin 失败:失败发生在加一之前,Transaction::drop 按深度 N 排的是外层那一级的回滚,并把深度减成 N-1。begin_with 在事务里返回 InvalidSavePointStatement 就走这条路,结果见动脑筋答案。

H 段把裸连接上的 begin() 只 poll 一次就丢掉(futures::FutureExt::now_or_never),I 段在池连接上做同样的事再归还:

// H
let polled = conn.begin().now_or_never().map(|r| r.map(|_| ()));
println!("now_or_never = {polled:?}");
println!("is_in_transaction = {}", conn.is_in_transaction());
sqlx::query("insert into items values ($1)").bind(7_i64).execute(&mut conn).await?;
conn.close().await?;

// I
let mut pc = pool.acquire().await?;
let polled = pc.begin().now_or_never().map(|r| r.map(|_| ()));
drop(pc);
sqlx::query("insert into items values ($1)").bind(8_i64).execute(&pool).await?;
pool.close().await;
--- H: begin 只 poll 一次就丢掉 ---
now_or_never = None
  [conn 2] C->S Query("BEGIN")
  [conn 2] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
is_in_transaction = false
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
关闭连接
  [conn 2] C->S Terminate
--- I: 池连接上 begin 只 poll 一次就丢掉,然后归还 ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
now_or_never = None
  [conn 1] C->S Query("BEGIN")
  [conn 1] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
drop(pc)
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(T)
下一次从池里用:
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(T)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(T)
pool.close()
  [conn 1] C->S Terminate
--- 最终 tx.items: [1, 5]

BEGIN 发出去了,服务端回 ReadyForQuery(T),客户端的 is_in_transaction() 却是 false。之后的 INSERT 都落在这个没人管的事务里,Terminate 关连接时服务端回滚,7 和 8 都没留下。池的情况更糟:归还时的 ping 收到 ReadyForQuery(T) 也照单全收(ping 只是 write_sync 加 wait_until_ready,不看状态字节),这条连接带着事务回到空闲队列,下一个借它的人的写入全部悬空。上游报告的场景是 pool.begin() 在 BEGIN 来回途中被超时取消。

同一个程序换成 sqlx 0.8.6 再跑:H 段输出相同(0.8.6 的 Rollback 守卫同样受深度 0 的限制);动脑筋那段里 begin_with 也返回 InvalidSavePointStatement,但外层事务原样保留,最终表里是 [4, 5]。所以深度 0 的取消是老问题,事务内 begin_with 吞掉外层事务是 #3980 带进 0.9.0 的。上游的记录:深度 0 的取消是 #4393,修复 #4394 在 v0.9.0 之后合并,做法是在 BEGIN 来回之前先把深度加一;深度大于 0 的情况是 #4445,写这一页时还开着。在 0.9.0 上能做的是:别在事务里调 begin_with;要给 begin 设超时,就先 acquire 拿到 PoolConnection,在 &mut *conn 上带超时 begin,超时之后 conn.close().await,不让它回池(直接给 pool.begin() 套超时的话,连接在被丢掉的 future 里,调用方碰不到)。

Connection::transaction:闭包写法

Connection::transaction 在 0.9 里还在,是 Connection trait 的默认方法:

    fn transaction<'a, F, R, E>(
        &'a mut self,
        callback: F,
    ) -> impl Future<Output = Result<R, E>> + Send + 'a
    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,
    {
        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)
                }
            }
        }
    }

它只省掉手写的 commit / rollback:闭包返回 Ok 就提交,返回 Err 就当场 rollback().await,不用等 Drop 排队。闭包的签名要求返回 BoxFuture,所以调用处是 Box::pin(async move { ... }),里面用 &mut **t:t 是 &mut Transaction,解两次才是 &mut PgConnection。它是 Connection 的方法,Pool 不是 Connection,在池上用要先 acquire。

let r: Result<(), sqlx::Error> = conn
  .transaction(|t| {
    Box::pin(async move {
      sqlx::query("insert into items values ($1)").bind(6_i64).execute(&mut **t).await?;
      Err(sqlx::Error::RowNotFound)
    })
  })
  .await;
--- G: Connection::transaction 闭包,返回 Err ---
  [conn 2] C->S Query("BEGIN")
  [conn 2] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
  [conn 2] C->S Query("ROLLBACK")
  [conn 2] S->C CommandComplete("ROLLBACK"), ReadyForQuery(I)
结果 Err(RowNotFound)

和 B、C 两段对比,这里的 ROLLBACK 紧跟在闭包返回之后发出,不是等下一次使用。闭包 panic 时 rollback 那一支不会执行,回滚又回到 Drop 排队那条路。

动脑筋答案

--- F: 事务里再 begin_with ---
  [conn 2] C->S Query("BEGIN")
  [conn 2] S->C CommandComplete("BEGIN"), ReadyForQuery(T)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(T)
结果 Err(InvalidSavePointStatement)
外层 is_in_transaction = false
  [conn 2] C->S Query("ROLLBACK")
  [conn 2] S->C CommandComplete("ROLLBACK"), ReadyForQuery(I)
  [conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 2] S->C BindComplete, CommandComplete("INSERT 0 1"), CloseComplete, ReadyForQuery(I)
外层 commit 结果 Ok(())

程序结尾打印的表内容(前面的 B、C、G、H、I 段都没留下行):--- 最终 tx.items: [1, 5]。

  • tx.begin_with(...) 进入 Transaction::begin,先造出 open: true 的内层事务,再调驱动的 begin。深度是 1,驱动立刻返回 InvalidSavePointStatement,一条消息都没发。
  • 内层事务被 drop,open 还是 true,start_rollback 按深度 1 排进 ROLLBACK,深度减成 0。这是外层事务的 ROLLBACK,4 号那行没了。于是 外层 is_in_transaction = false。
  • 下一条 INSERT 5 先把排着的 ROLLBACK 冲出去,然后在自动提交模式下执行:ReadyForQuery(I),5 号直接落盘。
  • 外层 commit 看到深度 0,什么也不发,返回 Ok(())。调用方拿到的是一次「成功提交」,实际是 4 丢了、5 绕过了事务。换成 tx.rollback() 也一样返回 Ok(()),5 照样留下。

小结

  • Transaction 是 MaybePoolConnection 加一个 open 标记;嵌套深度记在连接的 transaction_depth 上,begin / commit / rollback 发的 SQL 全由深度算出:BEGIN、SAVEPOINT _sqlx_savepoint_N、COMMIT、RELEASE SAVEPOINT、ROLLBACK [TO SAVEPOINT],都是简单查询协议的 Query 消息。
  • 成功与否看 ReadyForQuery 的状态字节,深度在服务端确认后才加一;深度为 0 时 commit / rollback 什么都不发,返回 Ok(())。
  • 没提交就 drop:start_rollback 把 ROLLBACK 写进缓冲、深度减一,不报错。池连接上由归还时的 ping 一并发出;裸连接要等下一次使用,期间服务端一直在事务里、锁不放。
  • begin_with 只认「语句跑完后服务端处在事务中」,否则 BeginFailed;深度大于 0 时返回 InvalidSavePointStatement,但在 0.9.0 里它顺带把外层事务回滚了,外层之后的 commit 假装成功。
  • begin 在深度 0 被取消时两层守卫都失效,连接带着服务端事务继续用,池连接会带着它回池(0.8.6 起就有,修复在 0.9.0 之后)。
  • Connection::transaction 按闭包的 Ok / Err 当场提交或回滚,闭包要写成 Box::pin(async move { ... }),里面用 &mut **t。

💬 关联讨论

发起讨论
加载中...