想象一下,你在写一个网店的应用,在订单支付的时候你需要:
- 从客户卡里扣钱。
- 减少仓库里的商品数量。
- 创建一条成功交易的记录。
如果这些操作中间有一步出错了会怎样?比如,钱已经扣了,但商品刚好卖完,还没来得及创建订单记录?一切都乱套了:钱“卡住”了,订单没完成,你的服务器会收到一堆愤怒的邮件(甚至可能被告)。
事务就是用来避免这种情况的。它们可以把多个操作打包成一个“原子”单元操作数据库。就像文本编辑器里的“撤销”按钮:如果哪里出错了,直接回到最初状态。
事务是怎么保证数据完整性的?
事务基于ACID这个概念:
- 原子性 (Atomicity) — 事务里的所有操作要么全做,要么全不做。“要么全有,要么全无”。
- 一致性 (Consistency) — 事务前后,数据都保持一致的状态。
- 隔离性 (Isolation) — 一个事务不会影响到其他事务。
- 持久性 (Durability) — 事务一旦完成,结果就算系统崩了也会保存下来。
为啥我又在重复这些?因为这是大家都追求的理想状态。可是……现实很骨感。等我们后面再讲事务的时候你会发现,有些ACID原则其实不得不妥协。
所以趁现在事务还这么简单美好,好好享受吧。来,直接上例子!
事务的使用示例
来看一个添加学生并给他报名课程的场景。
假设我们在搞一个大学的数据库。最近有些外部听众来上我们的课。如果课程有空位,我们就把这个听众临时注册成学生,然后加到课程里。流程大概是这样:
添加新学生并给他报名课程时,我们需要:
- 往
students表里加一条记录。 - 在
enrollments表里加一条,把学生和课程关联起来。
如果中间出错了(比如课程已经满了),我们就得撤销操作,避免数据在表之间不一致。操作方法如下:
-- 开始事务
BEGIN;
-- 步骤1:添加学生
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 假设返回 id = 10
-- 步骤2:给他报名课程
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- 都成功了?提交更改
COMMIT;
如果出错了会怎样?
比如报名课程时出错了:比如课程不存在。如果你忘了用事务,students表里会留下学生记录,但enrollments表里啥都没有。这样数据就不一致了。为了避免这种情况,我们可以用ROLLBACK命令。
-- 开始事务
BEGIN;
-- 步骤1:添加学生
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 步骤2:尝试给他报名课程
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- 错误:id = 999 的课程不存在!
-- 撤销所有更改
ROLLBACK;
这样,所有操作都不会生效,数据库会回到事务开始前的状态。
用SAVEPOINT做更细致的控制
现在想象一个更复杂的场景。你要做一堆操作,但只想撤销到某个点,而不是全部撤销。
来实现一个分步注册学生的流程:
-- 开始事务
BEGIN;
-- 添加学生
SAVEPOINT add_student; -- 创建保存点
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Female');
-- 给她报名第一个课程
SAVEPOINT enroll_course_1; -- 再建一个保存点
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- 给她报名第二个课程(这里出错)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- 错误!
-- 只回滚到最后一个保存点
ROLLBACK TO enroll_course_1;
-- 继续流程
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- 提交更改
COMMIT;
这样,流程某一部分出错不会影响其他部分的数据保存。
怎么判断有没有实际更改
如果SQL语句有改动数据,可以检测到底有没有真的改动。
比如你执行了DELETE,但WHERE条件没匹配到任何行。或者UPDATE时数据本来就已经是目标值,结果啥都没变。
这时候可以用系统变量FOUND。它能告诉你上一个SQL语句有没有影响到行:
FOUND = TRUE— 有行被更新/删除了;FOUND = FALSE— 没有行被删除或更改。
普通SELECT用不了,只能用来追踪数据变更。
实战:处理支付
事务在金融类应用里特别有用。再来看一个把钱从一个账户转到另一个账户的系统。
-- 开始事务
BEGIN;
-- 步骤1:从第一个账户扣钱
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 步骤2:检查操作是否成功(有行被改动)
IF NOT FOUND THEN
ROLLBACK; -- 钱不够就撤销
RAISE EXCEPTION '余额不足!'; -- 抛出异常
END IF;
-- 步骤3:给第二个账户加钱
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- 提交事务
COMMIT;
这样,如果客户想转的钱比账户余额多,事务会被撤销,数据库不会出现“悬挂”状态。
注意事项和常见错误
忘记COMMIT:如果事务最后忘了COMMIT,数据库会一直“等着”,更改不会保存。
忘记WHERE:更新或删除数据时没加条件,后果很严重。比如DELETE FROM students没加WHERE,会把所有学生都删掉。
事务太久:事务开太久会锁住数据,导致性能问题。一定要尽快结束事务(COMMIT或ROLLBACK)。
事务就是你保证数据完整性的好基友。它能帮你避免数据不一致,尤其是在用户注册、支付处理、更新关联表这些复杂场景。学会用BEGIN、COMMIT、ROLLBACK和SAVEPOINT,你的应用会更靠谱、更安全。
GO TO FULL VERSION