咱们再来复习下事务类型吧!在数据库事务的世界里,有三个主要的“反派”,能让你一天都不爽:Dirty Read、Non-Repeatable Read 和 Phantom Read。这些“异常”都是因为事务隔离级别不够高才冒出来的。今天我们就来聊聊这些反派到底是谁,它们怎么作怪,以及——最重要的——怎么对付它们。
在看例子之前,先回忆下它们都是什么意思。
Dirty Read(脏读):
你读到了被修改过但还没提交(COMMIT)的数据,甚至更糟的是,这些修改可能会被回滚(ROLLBACK)。就像你给朋友转了钱,看余额发现自己破产了,但后来你反悔又把钱转回来了。魔法吧!Non-Repeatable Read(不可重复读):
你在同一个事务里两次读取同一条数据,但这期间别的事务把数据改了,你就看到两个不一样的结果。就像你看了下护照上的生日,然后把护照给朋友,他偷偷改了数字,你再看一眼,生日变了。Phantom Read(幻读):
你执行了两次同样的查询,第二次却多了几行,是别的事务新插入的。就像你数房间里有几个人,结果有人悄悄带了新朋友进来。
Dirty Read 的问题
假设我们有个表 accounts:
CREATE TABLE accounts (
account_id SERIAL PRIMARY KEY,
owner TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);
事务1改了余额但还没结束,事务2同时想读这些数据。
事务1:
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Alice 的余额变成 800,但事务还没结束。
事务2:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- 看到余额:800(脏读)。
ROLLBACK; -- 事务1被回滚了。
现在事务2用的是不对的数据,因为事务1把改动撤销了。要避免这种情况,可以用 READ COMMITTED 隔离级别,这样就看不到未提交的事务改动了。
Non-Repeatable Read 的问题
想象下,事务1先读了数据,别的事务把它改了,然后事务1再读一次,结果就变了。
事务1:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 看到余额:500。
事务2(并行):
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;
事务1再来一次:
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 看到余额:400。
COMMIT;
注意,同一个事务里数据变了。在实际场景下,比如财务报表,这种情况很要命。可以用更高的隔离级别,比如 REPEATABLE READ,来解决。
Phantom Read 的问题
假设我们有个表 orders:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer TEXT NOT NULL,
total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);
事务1数订单数,别的事务插入新订单,事务1再数一次。
事务1:
BEGIN;
SELECT COUNT(*) FROM orders; -- 看到:2。
事务2(并行):
BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;
事务1再来一次:
SELECT COUNT(*) FROM orders; -- 看到:3。新的“幻影”订单出现了!
COMMIT;
要消除幻读,需要用 SERIALIZABLE 隔离级别,它会完全阻止并发改动影响结果。
怎么防止这些异常
隔离级别 vs. 异常
| 隔离级别 | Dirty Read |
Non-Repeatable Read |
Phantom Read |
|---|---|---|---|
Read Uncommitted |
❌ 会有 | ❌ 会有 | ❌ 会有 |
Read Committed |
✅ 没有 | ❌ 会有 | ❌ 会有 |
Repeatable Read |
✅ 没有 | ✅ 没有 | ❌ 会有 |
Serializable |
✅ 没有 | ✅ 没有 | ✅ 没有 |
怎么选对隔离级别?
- 如果你要快速访问数据,对异常不敏感(比如分析旧数据)——用
READ COMMITTED。 - 如果你想让同一个事务里数据一直稳定——用
REPEATABLE READ。 - 如果你要最强隔离和一致性——选
SERIALIZABLE。注意:性能可能会掉。
实用建议
用适合你场景的隔离级别来写事务。比如:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;
加索引,减少表锁,加快查询速度。
写高效的查询,避免长时间锁表,尤其是在 SERIALIZABLE 级别下。
常见错误和怎么避免
有时候隔离级别选错了,会导致冲突和性能下降。比如在有很多并发事务的系统里用 SERIALIZABLE,可能会锁表,事务“饿死”。
要避免这些问题,就要分析你的查询,在不同隔离级别下做性能测试,并用合适的索引。
GO TO FULL VERSION