# 实战：观察一笔订单事务

LLMS 索引： [llms.txt](/llms.txt)

---

这个实验不追求制造最大并发，而是把一条最小 blocking edge 观察完整：blocker 写入未提交版本，普通 reader 读旧版本，waiter 写同一行并等待；observer 同时采集 activity、blocking PID 与 locks，最后取消精确实验 query，让两个事务都回滚并验证状态。

风险分级：

- `verify` / `observe`：`R0·观察`，只读 catalog、sample row 和 WAL positions；
- `transaction`：`R2·受控演练`，触发三个预期 error，并 rollback 一次真实 UPDATE；
- `blocking`：`R2·受控演练`，两个 session 对订单 1002 UPDATE，调用 `pg_cancel_backend` 取消精确 blocker；
- `all` / `review`：`R2·受控演练`，执行前验、全部实验和后验。

即使不提交，写入仍产生 tuple/WAL/lock。只在已确认可演练的 Pigsty L1 或本地测试库运行；生产只能复用只读取证方法，不能复用“主动注入阻塞”。

## 5.6.1 从 SQL 观察会话、快照、锁和 WAL 位置 {#item-5-6-1}

先使用绝对路径指向自己的私有 service file，不把 password 写进命令历史：

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin

cd static/labs/ch05
export PG36_EVIDENCE_DIR="$PWD/evidence/ch05/$(date -u +%Y%m%dT%H%M%SZ)"

psql -X -w "service=$PGSERVICE" \
  -c '\conninfo' \
  -c "SELECT current_database(), pg_is_in_recovery();"
./task.sh verify
```

context guard 要求 database=`pg36_shop`、primary/writable、可 `SET ROLE pg36_owner`，且 `shop_private.schema_version` 是 ch04-v1。`verify.sql`复用完整 ch04 验收，再额外要求：

```text
active_lab_workers=0
order_1002_fingerprint=2bfa6eac30b9a1cfa2d51e98c4e98332
relation_checksum=f8a7bfae59c6d16cd323abecfefe1014
```

### 观察一个没有 write XID 的 transaction

运行：

```bash
./task.sh observe
sed -n '1,120p' "$PG36_EVIDENCE_DIR/observe.txt"
```

[`observe.sql`](/labs/ch05/observe.sql)显式开启：

```sql
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED READ ONLY;
```

然后只取一次 `pg_current_snapshot()`，解析其边界，并从 `pg_stat_activity` 反查自己的 `backend_xid/backend_xmin`。典型片段：

```text
transaction_isolation=read committed
transaction_read_only=on
assigned_xid_before_write=<none>
snapshot=959:959:
snapshot_xmin=959
snapshot_xmax=959
snapshot_in_progress_count=0
backend_snapshot=<none>|959|<none>|<none>
```

XID 数值随实例推进，不能 hard-code。要观察的是：transaction/snapshot 已存在，read-only backend 却可以没有 assigned write XID；`backend_xmin` 暴露它对清理 horizon 的影响。

同一脚本输出：

```text
tuple_diagnostic=1002|xmin|xmax|ctid|request_fingerprint
wal_positions=insert_lsn|write_lsn|flush_lsn
```

`xmin/xmax/ctid`只用于版本取证。反复运行 rollback 实验后，一个仍可见 tuple 甚至可能有非零 `xmax`，这正说明不能从 `xmax<>0` 直接推断“已删除”。三个 WAL position 分别表示 insert、write、flush 进度；短暂相等也不证明未来始终没有 pending WAL。

### 观察 failed transaction 与 savepoint

```bash
./task.sh transaction
sed -n '1,120p' "$PG36_EVIDENCE_DIR/transaction-errors.stderr"
sed -n '1,160p' "$PG36_EVIDENCE_DIR/wal-rollback.txt"
```

task 要求 error stream 中每个 SQLSTATE 恰好一次：

```text
22012  division_by_zero
25P02  in_failed_sql_transaction
23514  check_violation
```

前两项证明 error 后普通 SQL 不能继续；第三项发生在 savepoint 后，`ROLLBACK TO` 恢复 transaction，再完成一条合法 UPDATE，最终 outer rollback。脚本不靠本地化错误消息判断，而用稳定 SQLSTATE。

WAL probe 要同时满足：

```text
wal_insert_advanced=t
state_restored=t
```

LSN 是实例全局位置，差值中可能包含其他 backend；本实验在隔离 L1 中只用它证明“回滚路径仍有 WAL 活动”，不把字节数当单条 SQL benchmark。

## 5.6.2 从 Pigsty 观察连接、事务与等待指标 {#item-5-6-2}

SQL catalog 是当前瞬时状态，Pigsty/Grafana 提供时间序列、层级导航和跨组件上下文。两者不是替代关系：

| 问题 | PostgreSQL 原生证据 | Pigsty v4.5 入口 |
|---|---|---|
| cluster 是否出现 session/load/lock 波峰 | `pg_stat_activity`、database stats | **PGSQL Activity** |
| 某 instance 的 active/idle/idle-in-tx 演变 | activity + backend timestamps | **PGSQL Session** |
| TPS/QPS、transaction 与 lock 趋势 | database/xact stats、locks | **PGSQL Xacts** |
| WAL、XID、checkpoint、archive、I/O 是否异常 | WAL/admin/stats views | **PGSQL Persist** |
| 当前 database 的 activity 与 lock wait 明细 | activity、`pg_blocking_pids`、`pg_locks` | **PGCAT Locks** |

官方 v4.5 dashboard 索引把 PGSQL Activity 定义为 cluster 级 session/load/QPS/TPS/locks，把 Persist 定义为 WAL/XID/checkpoint/archive/I/O，把 PGCAT Locks 定义为 catalog-derived activity 与 lock wait。部署若定制 dashboard、collector 或版本，面板与 metric 可能变化，所以正文依赖的是问题映射，不是像素位置。

### 让连接可归因

所有 worker 都带唯一 `application_name`：

```text
pg36-ch05-blocker-<UTC timestamp>-<shell pid>
pg36-ch05-waiter-<UTC timestamp>-<shell pid>
```

真实应用也应给 service/driver 设置稳定 application name，并在 tracing 中关联：

```text
cluster / instance
database / user / application
request trace ID
backend PID + backend_start
transaction/query start
dashboard time range + timezone
```

只记录 PID 不够，PID 会重用；只记录 SQL 也不够，同一 statement 可由大量租户并发执行。涉及权限时还要知道：普通角色在 `pg_stat_activity` 中只能完整看到自己的 session，跨用户 query text/细节需要 `pg_read_all_stats` 等受控监控权限或 superuser。不要为了 dashboard 方便给业务账号 superuser。

### 处理采样与瞬时现场的差异

catalog query 能在 waiter 正等待时看到精确 edge；Prometheus/exporter 按采集周期采样，短于一个 scrape interval 的实验可能根本不出现在图上。默认 blocking harness 取完 SQL 证据便立即释放，不靠固定 sleep 同步。

若只在 L1 教学库中需要让 dashboard 有机会采到，可显式延长观察窗：

```bash
export PG36_DASHBOARD_HOLD_SECONDS=20  # 只允许 0..25
./task.sh blocking
```

此变量只在已经确认 edge 后 sleep，不能参与 worker 同步；它会人为延长订单行等待，不允许用于生产。打开 Pigsty Web UI 后，在同一 UTC 时间窗依次看 PGSQL Activity、PGSQL Session/Xacts、PGCAT Locks，再回到 evidence 的 PID/app name 对照。若 panel 没采到，SQL evidence 仍是实验验收依据，不应继续延长生产锁来“等图变漂亮”。

dashboard 擅长回答“何时开始、范围多大、是否反复、同时还有什么资源变化”；catalog 擅长回答“现在这条边究竟是谁阻塞谁”。事故诊断通常先由告警/趋势定位时间窗，再用原生视图和日志确认现场。

## 5.6.3 注入阻塞并解释“现象—证据—原理” {#item-5-6-3}

单独执行 blocking：

```bash
unset PG36_DASHBOARD_HOLD_SECONDS
./task.sh blocking

sed -n '1,160p' "$PG36_EVIDENCE_DIR/blocking/summary.txt"
sed -n '1,160p' "$PG36_EVIDENCE_DIR/blocking/activity.csv"
sed -n '1,220p' "$PG36_EVIDENCE_DIR/blocking/locks.csv"
```

[`blocking-lab.sh`](/labs/ch05/blocking-lab.sh)不靠“sleep 两秒大概启动好了”同步。它轮询 blocker 直到：

```text
state=active
wait_event_type=Timeout
wait_event=PgSleep
```

这证明未提交 UPDATE 已经完成并正持有 transaction。随后 ordinary reader 带 1 秒 statement timeout 读取旧 fingerprint；再启动 waiter，直到 `pg_blocking_pids(waiter)` 精确等于 blocker PID 且 wait type 是 Lock，才采集 CSV。

```mermaid
sequenceDiagram
    participant B as Blocker
    participant R as Ordinary reader
    participant W as Waiter
    participant O as Observer

    B->>B: BEGIN; UPDATE order 1002
    Note over B: uncommitted new tuple version
    R->>B: plain SELECT
    B-->>R: no row-lock wait; old committed version
    W->>B: UPDATE same logical row
    Note over W: waits for Blocker's XID
    O->>O: activity + blocking_pids + locks
    O->>B: pg_cancel_backend(exact PID)
    Note over B: psql exits on 57014; transaction rolls back
    B-->>W: lock released
    W->>W: UPDATE succeeds; explicit ROLLBACK
    O->>O: verify baseline and no workers
```

一次实测 summary：

```text
status=ok
reader_saw_previous_committed_version=true
waiter_blocked_by=<blocker_pid>
waiter_wait_event_type=Lock
waiter_wait_event=transactionid
cancel_exact_blocker=t
blocker_expected_nonzero_exit=3
waiter_exit=0
state_restored=true
remaining_workers=0
```

### 从三组证据回到原理

| 现象 | 直接证据 | 可以得出的原理 | 不能过度推出 |
|---|---|---|---|
| reader 成功返回旧指纹 | 1 s timeout 内结果等于 baseline | 普通读使用 MVCC 旧 committed version，不等 row update lock | 所有 SELECT 永不等待 |
| waiter 停住 | active + Lock + transactionid | 同一行 writer 必须等待前一 XID outcome | 表被“全锁死” |
| `pg_blocking_pids` 单边 edge | waiter → blocker PID | 当前 regular lock queue 的直接 blocker 已识别 | 数秒前/后的历史仍完全相同 |
| locks 中 waiter XID ShareLock 未 granted | transactionid=blocker XID | 等待的是 blocker transaction completion | 必须找到 blocker 的 ungranted tuple lock |
| cancel 后 waiter 前进 | blocker 收到 57014 并断连 rollback | conflicting transaction 结束会释放 lock | cancel 任意生产 query 都安全 |
| 前后 checksum 相同 | verify-before/after | 两个业务写入最终都未提交 | 没产生 WAL/dead tuple/统计代价 |

这种“现象—证据—原理—边界”四列，比只保存一张 dashboard 截图更可审计。它允许后来者复核当时看见什么、为什么得出结论、结论没有覆盖哪些情况。

### 失败清理和停止线

正常路径只 `pg_cancel_backend` 精确 blocker query；blocker psql 因 `ON_ERROR_STOP` 收到 SQLSTATE `57014` 后退出，server rollback connection transaction。waiter 获锁后显式 rollback。

若 harness 中途失败，EXIT trap 只按本次唯一 application names 终止它启动的 blocker/waiter，并等待本地 psql process；不会扫描或清理其他会话。随后仍应运行：

```bash
./task.sh verify
```

若 `active_lab_workers<>0`、fingerprint/checksum 漂移，或 blocker identity 不再精确，停止自动处置并人工核对。这个实验没有 reset，因为成功路径不应留下需要 reset 的对象或数据；“写一个 reset 抹掉异常”反而会掩盖事务边界错误。

最后做全章验收：

```bash
export PG36_EVIDENCE_DIR="$PWD/evidence/ch05/final-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all
```

只有 `verify-after` 恢复稳定摘要，且 blocker cancellation、waiter exit、SQLSTATE 和 WAL/rollback 断言全部通过，才能把本章标记为完成。

---

[上一节：隔离现象与后续路线](../05/) · [返回本章目录](../) · [下一章：立木取信：开发规约与交付基线](/development-standards/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
