服务端与数据 / 进阶

PostgreSQL 事务边界:把不变量留在数据库里

事务不只是 begin 与 commit。本文从库存扣减示例解释业务不变量、原子更新、锁顺序和外部调用边界。

先写清楚必须永远成立的条件

库存不能为负、订单号不能重复、转账两边金额必须同时变化,这些是不变量。能用 NOT NULL、CHECK、UNIQUE 和 FOREIGN KEY 表达的规则,应交给数据库做最终防线。应用层的先查再写在并发下并不可靠。

EXAMPLE / 01PostgreSQL
UPDATE products
SET stock = stock - $1
WHERE id = $2 AND stock >= $1
RETURNING id, stock;

-- 返回 0 行表示库存不足

确实要读后写时再显式加锁

所有代码按相同顺序获取锁,可以显著减少死锁。即便如此,事务仍可能因死锁或序列化冲突失败,调用方要能重试整个事务,而不是只重试最后一条 SQL。

EXAMPLE / 02PostgreSQL
BEGIN;
SELECT id, balance
FROM accounts
WHERE id IN ($1, $2)
ORDER BY id
FOR UPDATE;

UPDATE accounts SET balance = balance - $3 WHERE id = $1;
UPDATE accounts SET balance = balance + $3 WHERE id = $2;
COMMIT;

事务内不要等待外部世界

不要在持有数据库锁时调用支付、短信或第三方 HTTP 接口。先在短事务中写业务数据和 outbox 事件,提交后由消费者执行外部调用。这样不会因为网络抖动长期占用连接和锁。

使用连接池时,BEGIN 到 COMMIT 的所有语句必须在同一个连接或事务对象上执行,不能每条 SQL 都从池中重新取连接。