想象一下,你在玩网游,突然有个外挂玩家在后台偷偷改代码增强自己,或者你在图书馆看书,结果有人趁你不注意换了几页、插了新章节,甚至把整本书掉包。是不是很崩溃?REPEATABLE READ 隔离级别就是用来防止这种“惊喜”的。
REPEATABLE READ 能保证你在一个事务里看到的数据,从头到尾都不会变。就算有别的事务想改这些数据,你的事务也不会被影响,数据还是你一开始看到的那样。
主要特点:
- 防止
Dirty Read(读到还没提交的数据)。 - 最重要的是,防止
Non-Repeatable Read。也就是说,你在事务开始时查到一批数据,后面再查还是一样的,就算别人改了也没用。
不过 REPEATABLE READ 没法防 Phantom Read。如果有别的事务插入了新行,你下次查可能会看到这些新行。要想连这个都防住,就得用 SERIALIZABLE,这个以后再说。
怎么设置 REPEATABLE READ 隔离级别
在看例子之前,先说说怎么在 PostgreSQL 里开这个隔离级别。有两种常用方法:
只给某个事务设置隔离级别:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- 你的查询 COMMIT;给当前 session 全部事务都设置这个隔离级别:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
第二种方式下,这个 session 里的所有事务都会用 REPEATABLE READ。
例子:防止 Non-Repeatable Read
假设我们有个 accounts 表,结构如下:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pending'), ('Bob', 'pending');
先来看最基础的场景:一个事务改数据,另一个事务读数据。
没有 REPEATABLE READ 的场景(READ COMMITTED 级别)
事务 1 开始:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 得到:100
这时事务 2 改了数据:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
事务 1 继续:
SELECT balance FROM accounts WHERE account_id = 1;
-- 得到:150(数据变了!)
COMMIT;
你看,在 READ COMMITTED 下,同一个事务里两次查数据,结果可能变。这就是 Non-Repeatable Read。
用 REPEATABLE READ 的场景
现在我们用 REPEATABLE READ 再试一次。
事务 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 得到:100
事务 2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
事务 1 继续:
SELECT balance FROM accounts WHERE account_id = 1;
-- 还是:100(数据没变!)
COMMIT;
不管别的事务怎么改,事务 1 看到的数据都是它刚开始时的样子。这样 Non-Repeatable Read 就被防住了。
REPEATABLE READ 是怎么工作的
PostgreSQL 用了 MVCC(Multi-Version Concurrency Control,多版本并发控制)来实现 REPEATABLE READ。MVCC 的核心思想是,每个事务都能看到一个固定的“快照”,这个快照在事务结束前不会变。实现方式就是给每行数据维护多个版本。
事务一开始,就看到当时的数据快照。别的事务要是改了数据,PostgreSQL 会新建一行新版本,老版本还是留给用它的事务看。
所以事务会慢一点,也更吃内存。也正因为这样,大家一般不会用最高的隔离级别:虽然最安全,但也最慢。
REPEATABLE READ 的局限:Phantom Read
前面说了,REPEATABLE READ 防不了 Phantom Read。啥意思?来看个查范围数据的例子。
假设有个 orders 表:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
事务 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 得到:2
事务 2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
事务 1 继续:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 得到:3(新行出现在结果里了!)
COMMIT;
这里,事务 2 插入的新行(amount = 200)“幽灵”一样出现在事务 1 的查询结果里,虽然用的是 REPEATABLE READ。
要是你想连 Phantom Read 都防住,就得用 SERIALIZABLE,但那样性能会更差。
REPEATABLE READ 的优缺点
REPEATABLE READ 隔离级别很适合你需要保证数据不会在事务里变的时候。你查到的数据会一直不变,直到 COMMIT,就算别人改了也没用。
这样既防止了 dirty read(脏读),也防止了 non-repeatable read(不可重复读)。你一直用的都是最开始那批数据,不会突然被人“空降”新值。做报表、决策、需要数据一致性的时候特别有用。
但 REPEATABLE READ 防不了“幽灵行”(phantom read)——就是你查过的范围里突然多了新行。而且高并发下,这个级别可能让事务互相冲突,特别是大家都查改同一批数据时,容易锁表、回滚,哪怕 SQL 写得没问题。
总的来说,REPEATABLE READ 是安全和性能的折中,但高并发场景下可能要多调优、多注意。
实用建议和常见坑
- 记住,隔离级别选高了会影响性能。只有真需要保证数据不变时才用
REPEATABLE READ。 - 别把
REPEATABLE READ和SERIALIZABLE搞混了。你要是查到新行,REPEATABLE READ下这是正常的。 - 事务时间太长容易锁表,注意别让事务拖太久,不然会影响别人操作。
PostgreSQL 给你很多事务隔离的工具。REPEATABLE READ 很适合你想保证事务里查到的数据不会变的场景。
GO TO FULL VERSION