CodeGym /课程 /SQL SELF /SERIALIZABLE 隔离级别:完全隔离,杜绝 Phantom Read

SERIALIZABLE 隔离级别:完全隔离,杜绝 Phantom Read

SQL SELF
第 40 级, 课程 2
可用

SERIALIZABLE 是 PostgreSQL 里事务隔离的最高级别。这个级别保证并发事务的结果就像它们是一个接一个顺序执行的一样。这样所有并发异常(比如 Dirty Read、Non-Repeatable Read、Phantom Read)都不会发生。

简单说,SERIALIZABLE 就是让所有并发事务都井井有条、数据一致。就像 PostgreSQL 在说:“大家排队来,别插队!”

为啥要用 SERIALIZABLE?有时候你就是想 100% 确认你的数据绝对一致,不管有多少并发操作。想象下超市收银台,多个收银员同时结账。如果没人管顺序,最后账面上的商品可能比实际卖出去的还多。有了 SERIALIZABLE,这种事根本不可能发生。

怎么设置 SERIALIZABLE 隔离级别

要设置 SERIALIZABLE 隔离级别,用这个命令:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

比如,来个用这个级别的事务:

BEGIN; -- 开始事务
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 设置隔离级别
SELECT * FROM products WHERE category = 'Electronics'; -- 查询商品列表
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- 更新库存
COMMIT; -- 提交更改

案例:电影院座位预订

来看个实际场景,SERIALIZABLE 就是刚需。假如你要开发一个电影院在线订票系统。用户选座,你得保证同一个座位不会被两个人同时买走。

先建个座位表:

CREATE TABLE seats (
    seat_id SERIAL PRIMARY KEY,
    is_booked BOOLEAN DEFAULT FALSE
);

然后加几个座位:

INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);

下面是用 SERIALIZABLE 的事务例子。

安全预订座位可以这样写:

BEGIN; -- 开始事务
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- SERIALIZABLE 隔离级别

-- 检查座位是否空闲
SELECT is_booked FROM seats WHERE seat_id = 1;

-- 预订座位
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;

COMMIT; -- 提交预订

如果有第二个并发事务也想抢这个座位,PostgreSQL 会直接报序列化冲突错误,绝不会乱套。

杜绝 Phantom Read

现在来聊聊我们最想避免的“phantom read”。Phantom Read 是指一个事务在执行过程中,看到别的事务新加或删的数据,导致结果变了。比如你查某个条件下有几行,结果别人插入或删除了行,你再查结果就变了。

来看个例子:

事务开始前的数据

id balance user
1 1000 Alice
2 500 Bob

事务 1

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 统计余额大于 400 的用户数
SELECT COUNT(*) FROM accounts WHERE balance > 400;

-- 期望结果:2(Alice 和 Bob)

事务 2

另一个会话里并发执行:

BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'Charlie');
COMMIT;

回到事务 1

-- 再查一次
SELECT COUNT(*) FROM accounts WHERE balance > 400;

如果不用 SERIALIZABLE,结果会变成 3,因为 Charlie 在事务 1 期间被加进来了。这就是 Phantom Read。

但用 SERIALIZABLE,PostgreSQL 保证事务 1 看不到 Charlie,因为它的“世界观”在事务开始时就定住了。

SERIALIZABLE 的特点和限制

我们已经知道 SERIALIZABLE 怎么实现理想隔离。但世界上哪有完美没缺点的东西?咱们实话实说。

性能下降
SERIALIZABLE 比 READ COMMITTED 或 REPEATABLE READ 要消耗更多资源。为啥?因为 PostgreSQL 得模拟顺序执行,追踪所有事务冲突。

序列化错误
如果 PostgreSQL 发现事务没法“完美排队”,就会抛出序列化错误(serialization_failure),事务会被回滚。

错误示例:

ERROR: could not serialize access due to concurrent update

遇到这种情况,我们可以失败后重试事务:

DO $$
DECLARE
    done BOOLEAN := FALSE;
BEGIN
    WHILE NOT done LOOP
        BEGIN
            -- 开始事务
            BEGIN;
            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

            -- 执行操作
            UPDATE accounts SET balance = balance - 100 WHERE id = 1;

            -- 提交更改
            COMMIT;
            done := TRUE; -- 成功就退出循环
        EXCEPTION WHEN serialization_failure THEN
            ROLLBACK; -- 出错就回滚
        END;
    END LOOP;
END;
$$;

这就是用 SERIALIZABLE 系统里的常规操作方式。

注意!

这段代码用的是 PL-SQL。我们后面还会讲到它。现在先给你个漂亮又能用的代码,也顺便秀一下 PL-SQL 有啥用 :)

什么时候用 SERIALIZABLE?

这个隔离级别适合那些出错代价极高的场景:

  • 金融交易,比如支付处理或奖金分发。
  • 库存管理系统,防止重复下单。
  • 在线预订,必须杜绝资源抢占冲突。

如果你做的系统要求数据 100% 一致,性能可以放一边,SERIALIZABLE 就是你的好基友。

2
任务
SQL SELF, 第 40 级, 课程 2
已锁定
防止重复预订
防止重复预订
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION