一、事务隔离级别是什么?

想象你在银行转账:A给B转100元,同时B又在给C转50元。如果这两个操作同时进行,数据库怎么保证A的钱不会凭空消失,或者B的余额不会计算错误?这就是事务隔离级别要解决的问题。

PostgreSQL提供了4种标准隔离级别(从宽松到严格):

  1. 读未提交(Read Uncommitted):能读到别人还没提交的数据,容易踩坑。
  2. 读已提交(Read Committed):默认级别,只读已提交的数据,但可能重复读同一数据结果不同。
  3. 可重复读(Repeatable Read):同一个事务内多次读相同数据结果一致,但可能有幻读。
  4. 串行化(Serializable):完全隔离,像排队一个一个来,性能最差但最安全。

二、并发问题实战演示

场景1:脏读(读未提交的坑)

-- 技术栈:PostgreSQL
-- 会话1(事务A)
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1; -- 改了但没提交!
-- 此时balance已减100,但事务还没结束

-- 会话2(事务B)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM users WHERE id = 1; -- 读到A未提交的数据!
-- 如果A回滚,B读到的就是"脏数据"

:PostgreSQL实际不会发生脏读,因为它的READ UNCOMMITTED行为等同于READ COMMITTED,这里仅为说明概念。

场景2:更新丢失(两个修改互相覆盖)

-- 会话1(事务A)
BEGIN;
SELECT balance FROM users WHERE id = 1; -- 假设查到1000
-- 正在计算时...

-- 会话2(事务B)
BEGIN;
UPDATE users SET balance = balance + 200 WHERE id = 1; -- 先提交
COMMIT;

-- 会话1继续执行
UPDATE users SET balance = balance - 100 WHERE id = 1; -- 覆盖了B的修改!
COMMIT; -- 最终余额是900而不是预期的1100

解决方法:使用SELECT FOR UPDATE锁定数据:

BEGIN;
SELECT balance FROM users WHERE id = 1 FOR UPDATE; -- 加锁
-- 其他事务会被阻塞直到当前事务完成

三、如何选择隔离级别?

1. 读已提交(Read Committed)

适用场景:大多数业务场景(如电商订单查询)。
优点:平衡性能与一致性。
缺点:非重复读(同一事务内两次查询结果可能不同)。

2. 可重复读(Repeatable Read)

适用场景:对数据一致性要求高的场景(如对账系统)。
优点:避免不可重复读。
缺点:可能遇到幻读(新增数据影响查询结果)。

3. 串行化(Serializable)

适用场景:金融交易等绝对不允许并发冲突的场景。
优点:完全隔离。
缺点:性能差,可能频繁锁超时。

四、高级技巧:乐观锁与悲观锁

悲观锁示例(先锁定再操作)

-- 技术栈:PostgreSQL
BEGIN;
SELECT * FROM products WHERE id = 123 FOR UPDATE; -- 加行锁
-- 检查库存...
UPDATE products SET stock = stock - 1 WHERE id = 123;
COMMIT;

乐观锁示例(通过版本号控制)

-- 先查出当前版本
SELECT id, stock, version FROM products WHERE id = 123;

-- 更新时检查版本
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 123 AND version = 2; -- 如果version被其他人修改过,则更新0行

五、实战建议

  1. 默认用读已提交,除非有明确需求。
  2. 短事务原则:事务越短,冲突概率越低。
  3. 监控锁等待SELECT * FROM pg_locks WHERE granted = false;
  4. 避免长事务:长时间运行的事务会阻塞其他操作。