这节课我们主要聊聊在用事务时经常会踩到的坑,以及怎么绕开它们。相信我,就算是最牛的 SQL 大佬,有时候也会忘记写 COMMIT!我们会给你一些建议,让事务出错变成小概率事件。
很遗憾(也许也是好事),数据库可不是魔法城堡,啥都能自动搞定。事务出错其实挺常见的,尤其是刚开始写代码的时候。我们来详细拆解一下。
忘记写 COMMIT 或 ROLLBACK
忘记结束事务,简直就是“经典老梗”。想象一下你在餐厅点了菜,结果服务员忘了给你结账。在 PostgreSQL 里,这就意味着数据库“卡”在事务状态,占着资源,还会锁住别的操作。
错误示例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 哎呀!我们忘了加 COMMIT 或 ROLLBACK。
当事务“卡住”时,资源锁可能会影响整张表。如果 DBA 发现情况不妙,他可能会用“强硬”手段结束它。但最好别让事情走到这一步。
怎么避免?
- 每次都要明确结束事务:
COMMIT或ROLLBACK。 - 用一些客户端工具,它们会自动提醒你有挂起的事务。
- 如果事务没结束你就重启了应用,数据库会自动
ROLLBACK,但这对系统状态来说不一定方便。
用了不合适的隔离级别
选隔离级别看起来很无聊,但其实它对防止各种奇怪问题很关键。比如你在重要的金融操作里用了 READ UNCOMMITTED,那就可能读到“脏数据”,这些数据后面可能会被撤销。
示例:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 读取被其他事务修改的数据
SELECT balance FROM accounts WHERE account_id = 1;
-- 另一个事务执行了 ROLLBACK,你的数据就失效了。
怎么避免?
- 先想清楚你的应用对数据有多重要。
- 大多数场景用
READ COMMITTED,这样能避免“脏读”。 - 如果特别怕数据被改或者出现幻读,就用更严格的
REPEATABLE READ、SERIALIZABLE。
事务冲突和锁
有时候两个或更多事务想同时改同一份数据。这时 PostgreSQL 会锁住其中一个,等另一个结束。这可能会导致 死锁(deadlock)。
错误示例:
-- 第一个事务
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 第二个事务
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- 等待第一个事务...
如果两个事务都占着对方需要的资源,就死锁了。PostgreSQL 会检测到死锁,结束其中一个事务并报错:
ERROR: deadlock detected
怎么避免?
- 事务里的操作顺序要固定。
- 尽量让事务执行得快一点,减少被锁的概率。
- 只有真的需要时才用
SERIALIZABLE隔离级别。
SAVEPOINT 用错了
SAVEPOINT 是个很棒的部分回滚工具,但用不好会让人一头雾水。比如你忘了释放保存点(RELEASE SAVEPOINT),就可能多出一些没必要的锁或者报错。
错误示例:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- 忘了释放 SAVEPOINT!
怎么避免?
- 用完
SAVEPOINT记得删掉它。 - 别搞太多保存点,不然 SQL 会变得很难看懂。
事务和外部系统不兼容
想象一下,PostgreSQL 里的事务要和外部系统打交道:发通知、更新 API 等等。如果外部系统出问题,回滚就很麻烦了。
示例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 发送通知失败:email-server 没响应。
COMMIT; -- 数据改了,但通知没发出去。
怎么避免?
- 尽量把和外部系统的操作隔离开。
- 用中间表或者任务队列,来协调和外部系统的交互。
大事务导致的错误
大事务里有很多操作,更容易出错,比如被锁、超时、死锁啥的。
示例:
BEGIN;
-- 几千条更新操作
UPDATE orders SET status = 'completed' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- 可能会很慢。
怎么避免?
- 把大事务拆成几个小事务。
- 用批处理(batch)来更新数据。
- 尽量减少单个事务里要改的数据量。
忘了检查错误
不是所有 SQL 语句在事务里都会成功。比如有一条操作报错,整个事务就挂了。
示例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- 错误:account_id 不存在。
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- 因为有错误,没执行。
怎么避免?
- 每次都要检查每个操作的执行结果。
- 在 SQL 或客户端代码里加上错误处理。
误解 ROLLBACK 的行为
很多开发者以为 ROLLBACK 能撤销所有更改,把一切恢复原样。其实 ROLLBACK 只对当前事务有效。
误区示例:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- 错误!没用事务,这样不行。
怎么避免?
- 记住:
BEGIN是好基友,没有它ROLLBACK就没用。 - 所有关键操作都要放在事务里。
GO TO FULL VERSION