PostgreSQL中SELECT ... FOR UPDATE 语句的作用是什么?FOR UPDATE 和 FOR SHARE 有什么区别?
在 PostgreSQL 中,SELECT ... FOR UPDATE 和 SELECT ... FOR SHARE 是用于并发控制的行级锁(Row-Level Lock)机制。它们通常在事务(Transaction)中使用,以防止多个事务同时操作相同的数据时产生数据不一致的问题(如“丢失更新”)。
下面为你详细解释它们的作用以及两者的区别。
一、 SELECT ... FOR UPDATE 的作用
作用核心:锁定查询到的行,准备对其进行修改,防止其他事务同时修改这些行。
当你执行 SELECT ... FOR UPDATE 时,PostgreSQL 会对查询返回的所有行加上排他锁(Exclusive Lock)。
这告诉数据库:“我正在读取这些数据,并且我马上就要修改它们,在我的事务结束之前,任何人都不准修改这些数据!”
解决的典型问题:丢失更新(Lost Update)
假设有一个电商库存系统,商品 A 的库存还剩 1 件。
如果两个用户(事务 A 和事务 B)同时购买:
- 事务 A 执行普通的
SELECT,看到库存为 1。 - 事务 B 执行普通的
SELECT,也看到库存为 1。 - 事务 A 在应用程序中计算
1 - 1 = 0,执行UPDATE设置库存为 0。 - 事务 B 也在应用程序中计算
1 - 1 = 0,执行UPDATE设置库存为 0。
结果: 卖出了两件商品,但库存只扣减了一次,这就是超卖(丢失更新)。
使用 FOR UPDATE 解决:
sql
BEGIN;
-- 事务 A 锁定该行
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 如果此时事务 B 也执行 SELECT ... FOR UPDATE,它会被阻塞(卡住),直到事务 A 提交或回滚。
-- 事务 A 更新并提交
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- 事务 A 提交后,锁释放。事务 B 恢复执行,此时 B 拿到的 stock 是 0,应用层逻辑就会拒绝购买。
二、 FOR UPDATE 和 FOR SHARE 的区别
两者的核心区别在于锁的强度(排他性 vs 共享性)以及适用场景。
1. FOR UPDATE(排他锁 Exclusive Lock)
- 含义: 我要修改这行数据。
- 排他性: 极强。它会阻塞其他事务对这些行执行的:
UPDATE(更新操作)DELETE(删除操作)SELECT ... FOR UPDATE(获取排他锁)SELECT ... FOR SHARE(获取共享锁)
- 适用场景: 当你需要读取数据,基于读取到的数据在应用代码中进行计算,然后再写回数据库时(如扣减余额、扣减库存)。
2. FOR SHARE(共享锁 Shared Lock)
- 含义: 我只是要读取这行数据,并且要求在我读取期间,别人不能修改它,但别人可以跟我一起读取并保护它不被修改。
- 共享性: 允许多个事务同时对同一行加上
FOR SHARE锁。 - 排他性: 它会阻塞其他事务对这些行执行的:
UPDATE(更新操作)DELETE(删除操作)SELECT ... FOR UPDATE(获取排他锁)
- 适用场景: 通常用于检查外键关系或确保依赖数据不被更改。
- 例子: 你想在
order表中插入一条记录,关联user_id = 5。为了防止在你执行INSERT的瞬间,另一个事务把user_id = 5的用户删除了,你可以先执行SELECT * FROM users WHERE id = 5 FOR SHARE;。这保证了用户记录在你插入订单期间不会被修改或删除,同时又不影响其他也要给这个用户下订单的事务(它们也可以获取FOR SHARE锁)。
- 例子: 你想在
三、 锁的兼容性矩阵(直观对比)
假设事务 A 已经对某一行加上了锁,此时事务 B 尝试对同一行执行操作:
| 事务 B 的操作 \ 事务 A持有的锁 | FOR SHARE (共享锁) |
FOR UPDATE (排他锁) |
|---|---|---|
普通的 SELECT |
✅ 允许 (无锁) | ✅ 允许 (MVCC机制,读旧版本) |
SELECT ... FOR SHARE |
✅ 允许 (共享) | ❌ 阻塞等待 |
SELECT ... FOR UPDATE |
❌ 阻塞等待 | ❌ 阻塞等待 |
UPDATE / DELETE |
❌ 阻塞等待 | ❌ 阻塞等待 |
四、 进阶补充:避免死等 (NOWAIT 和 SKIP LOCKED)
如果在并发极高的情况下,使用行锁可能会导致事务长时间阻塞。PostgreSQL 提供了两个非常有用的后缀:
FOR UPDATE NOWAIT
如果该行已经被其他事务锁定,不等待,直接报错返回(抛出could not obtain lock on row异常)。应用层捕获异常后可以重试或提示用户。FOR UPDATE SKIP LOCKED
如果该行被锁定了,不等待,也不报错,而是直接跳过这行,返回那些没有被锁定的行。
非常适用于实现任务队列(Task Queue):多个 worker 同时去数据库拉取未处理的任务,使用SKIP LOCKED可以保证每个 worker 拿到不同的任务,互不阻塞。
总结
FOR UPDATE= 独占数据,禁止他人修改和加锁,用于读-计算-写的场景。FOR SHARE= 保护数据,禁止他人修改,但允许他人一起加共享锁,用于确保引用的数据不被篡改的场景。- 所有的锁都会在事务结束(
COMMIT或ROLLBACK)时自动释放,因此它们必须在事务块 (BEGIN ... COMMIT) 中使用。