CodeGym /课程 /SQL SELF /使用事务保证数据完整性

使用事务保证数据完整性

SQL SELF
第 22 级 , 课程 2
可用

想象一下,你在写一个网店的应用,在订单支付的时候你需要:

  1. 从客户卡里扣钱。
  2. 减少仓库里的商品数量。
  3. 创建一条成功交易的记录。

如果这些操作中间有一步出错了会怎样?比如,钱已经扣了,但商品刚好卖完,还没来得及创建订单记录?一切都乱套了:钱“卡住”了,订单没完成,你的服务器会收到一堆愤怒的邮件(甚至可能被告)。

事务就是用来避免这种情况的。它们可以把多个操作打包成一个“原子”单元操作数据库。就像文本编辑器里的“撤销”按钮:如果哪里出错了,直接回到最初状态。

事务是怎么保证数据完整性的?

事务基于ACID这个概念:

  • 原子性 (Atomicity) — 事务里的所有操作要么全做,要么全不做。“要么全有,要么全无”。
  • 一致性 (Consistency) — 事务前后,数据都保持一致的状态。
  • 隔离性 (Isolation) — 一个事务不会影响到其他事务。
  • 持久性 (Durability) — 事务一旦完成,结果就算系统崩了也会保存下来。

为啥我又在重复这些?因为这是大家都追求的理想状态。可是……现实很骨感。等我们后面再讲事务的时候你会发现,有些ACID原则其实不得不妥协。

所以趁现在事务还这么简单美好,好好享受吧。来,直接上例子!

事务的使用示例

来看一个添加学生并给他报名课程的场景。

假设我们在搞一个大学的数据库。最近有些外部听众来上我们的课。如果课程有空位,我们就把这个听众临时注册成学生,然后加到课程里。流程大概是这样:

添加新学生并给他报名课程时,我们需要:

  1. students表里加一条记录。
  2. 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,会把所有学生都删掉。

事务太久:事务开太久会锁住数据,导致性能问题。一定要尽快结束事务(COMMITROLLBACK)。

事务就是你保证数据完整性的好基友。它能帮你避免数据不一致,尤其是在用户注册、支付处理、更新关联表这些复杂场景。学会用BEGINCOMMITROLLBACKSAVEPOINT,你的应用会更靠谱、更安全。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION