在PostgreSQL这类支持多版本并发控制(MVCC)的关系型数据库中,多个会话同时读写同一批数据是十分常见的运行态。若缺乏恰当的并发控制手段,就会出现丢失更新、脏读或不可重复读等问题。乐观锁与悲观锁是两种截然不同的冲突解决哲学,前者假设冲突很少发生、只在提交时校验,后者假设冲突频繁、在读取阶段就阻断竞争者。实际系统中并不能笼统地说谁更好,而要结合业务容忍度、请求峰值与数据热点分布来落地。

悲观锁的实现机制与代码示例
悲观锁的核心思想是“先取锁再操作”。在PostgreSQL中,最典型的做法是使用SELECT ... FOR UPDATE语句,它会在读取目标行时为其加上行级排他锁,使其他事务对该行的UPDATE、DELETE以及加锁读取都被阻塞,直到当前事务提交或回滚。这种机制建立在MVCC之上,并没有改变多版本快照读的特性,只是额外引入了锁等待队列。
下面是一段在库存扣减场景中使用的悲观锁示例。事务开启后,先锁定商品库存行,再执行扣减,最后提交。由于锁的存在,并发的两个请求不会同时读到相同的剩余量,从而避免了超卖。
BEGIN; SELECT stock FROM products WHERE id = 1001 FOR UPDATE; -- 假设查到 stock = 10 UPDATE products SET stock = stock - 1 WHERE id = 1001; COMMIT;
悲观锁的优点在于逻辑直观、不易出现业务层的并发漏洞,特别适合强一致性要求的金融记账。但其代价是吞吐量受锁等待影响明显,如果热点行被频繁修改,大量会话会堆积在锁上,甚至引发死锁。因此在高并发秒杀场景中,往往需要配合应用层排队或分桶扣减来缓冲突。
乐观锁的版本号方案与冲突处理
乐观锁并不在读取时加锁,而是为每行数据引入一个版本标记,常见做法是增加version整数字段或updated_at时间戳。更新时通过WHERE子句带上旧版本值,若影响行数为零,说明期间已被其他事务改动,当前提交应视为冲突失败,由应用决定重试或报错。
以下示例展示了基于版本号的乐观锁更新。第一次读取得到版本为3,提交更新时要求版本仍为3,若成功则版本变为4;若失败则应用捕获异常并重试读取新值。
-- 读取时获取版本 SELECT id, stock, version FROM products WHERE id = 1001; -- 假设 version = 3, stock = 10 UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 3; -- 若返回影响行数 0,表示冲突
乐观锁在冲突率低的系统中表现优异,因为它避免了锁等待,读操作完全无阻塞。但当写竞争剧烈时,大量更新会失败并重试,反而浪费资源。此外,乐观锁要求业务代码显式处理冲突分支,增加了上层逻辑的复杂度,且必须保证version字段在所有更新路径中被正确维护。
基于MVCC原理的锁选择策略
要合理选型,需理解PostgreSQL的MVCC如何隔离事务。默认隔离级别下,读不阻塞写、写不阻塞读,快照保证了一致性视图。悲观锁的FOR UPDATE实际上是在此之上叠加了物理锁,用来强制串行化特定行的修改;乐观锁则完全利用快照读加条件写,依赖原子更新检测冲突。
从实践角度看,若业务操作耗时长、涉及多表联动且不能接受重试,悲观锁更稳妥;若请求简短、冲突少、需要高吞吐,乐观锁更轻量。还可以混合使用,例如读取用快照、关键提交用条件更新。下表归纳了二者差异:
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 读取阶段 | 提交阶段 |
| 冲突表现 | 等待或死锁 | 更新失败重试 |
| 适用场景 | 强一致、低并发 | 高并发、弱冲突 |
在真实架构中,还应关注索引对锁范围的影响。没有命中索引的UPDATE或FOR UPDATE会升级为表级扫描并锁住更多行,因此必须为并发字段建立合理索引。同时,将大事务拆小、控制锁持有时间,是无论哪种锁都适用的优化原则。
PostgreSQL乐观锁悲观锁修改时间:2026-08-17 04:10:23