PostgreSQL子查询更新并发不安全,需用SELECT FOR UPDATE加锁或INSERT ON CONFLICT DO UPDATE实现原子操作。

PostgreSQL 中不能靠子查询本身实现并发安全更新,必须配合事务隔离、显式锁或原子操作机制。 直接用 UPDATE ... SET x = (SELECT ...) 在高并发下极易产生竞态条件,比如计数超限、重复处理、丢失更新——这不是语法问题,而是语义缺陷。
为什么子查询更新在并发下不可靠
子查询在 UPDATE 中是只读快照,执行时已脱离当前事务的 MVCC 视图一致性保障。典型风险包括:
- 多个事务同时执行
UPDATE t SET cnt = (SELECT cnt FROM t WHERE id=1) + 1,都读到旧值 0,最终写入 1 而非预期的 2 - 子查询结果依赖其他表状态,但未加锁,中间可能被其他事务修改(如“查库存→扣减”流程)
- MySQL 允许的
UPDATE ... LIMIT 1在 PostgreSQL 中不支持,无法靠限制行数规避竞争
用 SELECT FOR UPDATE 替代裸子查询
这是最直接、兼容性最好的方案:把“读取+更新”合并为一个带锁的原子步骤。
- 必须包裹在
BEGIN; ... COMMIT;中,否则锁立即释放 -
FOR UPDATE锁住目标行,后续事务会阻塞直到前一个事务结束 - 若需避免死锁,所有事务按相同顺序访问行(例如始终按
id ASC)
示例:
BEGIN; SELECT id, status, version FROM orders WHERE order_id = 123 AND status = 'pending' FOR UPDATE; -- 应用层判断逻辑后执行 UPDATE orders SET status = 'shipped', version = version + 1 WHERE order_id = 123 AND version = 5; COMMIT;
用 INSERT ON CONFLICT DO UPDATE 实现幂等计数
当目标是“首次初始化 + 后续递增”,ON CONFLICT 是比子查询更安全的选择,尤其适合奖励发放、去重统计等场景。
- 插入不存在的记录,或冲突时执行
DO UPDATE,整个操作原子 - 必须有唯一约束(如
(inviter_id, event_type))才能触发冲突检测 - 不能和
RETURNING id同时使用——这是常见误用点,会报错
示例(限制每人最多领取 2 次邀请奖励):
RETURNING
- CTE 中的
SELECT与后续UPDATE属于同一事务快照,数据一致 -
RETURNING能拿到刚更新的行,可用于触发下游动作(如发消息、写日志) - 避免在 CTE 中写过于复杂的嵌套,否则执行计划可能退化
示例(同步更新订单状态并获取客户邮箱):
WITH updated AS (
UPDATE orders
SET status = 'shipped', shipped_at = NOW()
WHERE order_id IN (
SELECT order_id FROM pending_shipments
WHERE created_at <p>真正容易被忽略的是:**子查询更新从来不是并发问题的解法,而是问题的起点**。只要业务要求“读-判-写”三步必须原子,就该放弃纯子查询思路,转向锁、冲突处理或 CTE 这类数据库原生保障机制。</p>










