事务与并发控制

当数据库开始处理真正的业务数据时,你很快会遇到一个问题:

如果一组操作只做了一半就失败了,数据怎么办?如果多人同时修改同一批数据,又怎么办?

这就是事务和并发控制要解决的问题。

事务是什么

事务(transaction)可以理解成:一组要么一起成功,要么一起失败的数据库操作。

例如转账场景:

  1. 从 A 账户扣 100
  2. 给 B 账户加 100

这两步必须绑定在一起。

如果只扣了 A,没有加到 B,数据就错了。

所以这类操作不能只看“单条 SQL 是否成功”,而要看“整组操作是否作为一个整体成功”。

为什么要用事务

事务最直观的价值是:保证关键操作不会只做一半。

它特别适合这些场景:

  • 转账
  • 下单扣库存
  • 同时写多张相关表
  • 先检查再更新的一组业务操作

简化来说,事务是在保护“业务步骤的一致性”。

一个最小事务例子

BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;

这里:

  • BEGIN 表示开始事务
  • COMMIT 表示提交事务

如果中间出错,不想保留已经执行的部分,可以回滚:

ROLLBACK;

ACID 是什么

数据库事务经常会提到 ACID。初学阶段不用死背术语定义,更重要的是建立直觉。

Atomicity:原子性

一组操作要么全部成功,要么全部失败。

不要只做一半。

Consistency:一致性

事务前后,数据应该仍然满足规则和约束。

例如:

  • 主键不能重复
  • 外键关系不能乱掉
  • 金额不能凭空消失

Isolation:隔离性

多个事务同时运行时,彼此之间不能随意互相干扰到错误结果。

Durability:持久性

事务一旦提交成功,结果就应该被可靠保存下来,不是“看起来成功,过会儿又没了”。

PostgreSQL 的 MVCC 是什么

学 PostgreSQL 时,一个很重要的关键词是 MVCC,也就是多版本并发控制(Multi-Version Concurrency Control)。

初学者不必先钻底层实现,先记住它带来的直觉:

读和写不一定总是互相堵住。

这很重要。

如果数据库里每次有人写数据,别人就完全不能读,那系统会很容易卡住。

MVCC 的思路可以粗略理解成:数据库会让不同事务看到合适的数据版本,从而减少很多本来会发生的读写阻塞。

所以 PostgreSQL 在并发读写体验上通常比较好,很多常见查询不需要因为别人正在更新而全部停下来。

隔离级别先理解两个最常见的

PostgreSQL 支持多个事务隔离级别。入门阶段,最值得先认识两个:

  • Read Committed
  • Repeatable Read

Read Committed

这是 PostgreSQL 里很常见的默认直觉起点。

它的意思可以先简单理解为:每条语句读取到的是它开始执行时已经提交的数据。

好处是:

  • 行为比较直观
  • 并发性能通常不错
  • 适合很多日常业务场景

但它也意味着,在同一个事务里,前后两次查询可能看到不同结果,因为别人已经提交了更新。

Repeatable Read

Repeatable Read 更强调“同一个事务里的读取视图更稳定”。

你可以先这样理解:事务里重复执行同样的查询时,更倾向看到一致的结果快照。

这对于某些需要稳定读取视图的场景会更有帮助。

不过隔离越强,通常并发处理的代价也越高,所以不是默认越高越好。

行级锁和表锁

当事务开始修改数据时,锁就会变得重要。

行级锁

行级锁影响的是某几行数据。

例如你更新一条订单记录时,数据库通常主要锁住被更新的那几行,而不是整张表。

这也是为什么很多业务系统即使并发很高,仍然能同时处理大量不同记录。

表锁

表锁影响的是整张表。

它的影响范围更大,所以在日常业务里,大家通常更希望尽量避免不必要的重型表级阻塞。

初学阶段可以先记住:行锁更细,表锁更重。

一个库存更新的例子

假设你在做库存扣减。

BEGIN;
SELECT stock
FROM products
WHERE id = 1;
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0;
COMMIT;

这类操作的重点在于:

  • 检查库存
  • 扣减库存
  • 让它们在事务里完成

这样更容易避免“并发下超卖”这类问题。

一个转账例子

再看一个更直观的账户例子:

BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;

如果第二步失败,而你没有事务保护,那么第一步可能已经生效,数据就会不一致。

事务的意义,就是让这种关键业务变成一个整体。

并发时最该建立什么直觉

初学者先不用一开始就研究所有锁类型和底层实现,更重要的是建立下面这些直觉:

  • 多个用户可能会同时改同一批数据
  • 单条 SQL 成功,不代表整个业务步骤安全
  • 事务是为了保护一组操作的完整性
  • PostgreSQL 的 MVCC 让很多读写场景不必总是互相阻塞
  • 隔离级别是在“更强一致读取”和“更高并发效率”之间做平衡

初学者常见误区

误区一:只要 SQL 能跑通,就不用事务

如果业务步骤跨多条写操作,这个想法通常不安全。

误区二:隔离级别越高越好

更高的隔离通常意味着更高代价,不是所有场景都需要最强隔离。

误区三:锁一定是坏事

锁本身不是坏事,它是数据库用来保护一致性的手段。真正的问题通常是:锁范围过大、持有时间过长、事务设计不合理。

这一篇先记住什么

你现在最值得先掌握的是:

  • 事务让一组操作要么一起成功,要么一起失败
  • ACID 描述了事务的四个核心目标
  • PostgreSQL 的 MVCC 帮助减少很多读写阻塞
  • Read CommittedRepeatable Read 是入门阶段最值得先认识的隔离级别
  • 行锁更细,表锁更重
  • 转账、扣库存、多表写入都是典型事务场景

下一篇会继续看 PostgreSQL 很有特色的一部分:JSONJSONB,也就是在关系型数据库里处理半结构化数据。