乐观锁与悲观锁选型
High Contrast
Dark Mode
Light Mode
Sepia
Forest
2 min read326 words

乐观锁与悲观锁选型

并发控制有两种主要策略:悲观锁假设冲突经常发生,提前加锁;乐观锁假设冲突很少,只在提交时检查。选错策略会导致性能瓶颈(悲观锁过度使用)或数据错误(乐观锁漏检)。


两种策略的核心区别

悲观锁(Pessimistic Locking):
读取时就加锁 → 其他事务必须等待 → 提交时释放锁
"我确信会有冲突,所以先占坑"
乐观锁(Optimistic Locking):
读取时不加锁 → 提交时检查有没有被修改 → 如果有冲突则回滚重试
"我认为冲突很少,如果真的冲突了再处理"
维度 悲观锁 乐观锁
冲突假设 冲突频繁 冲突罕见
加锁时机 读取时 提交时
性能影响 高并发下锁等待多 无锁等待,但冲突时重试有开销
实现方式 SELECT FOR UPDATE version 列或 updated_at 检查
适用场景 库存扣减、余额转账 用户资料编辑、配置更新

悲观锁:SELECT FOR UPDATE

-- 场景:多个用户并发购买同一商品,库存只剩 1 件
-- ✅ 正确:悲观锁确保库存不被超卖
CREATE TABLE inventory (
product_id  INTEGER PRIMARY KEY,
available   INTEGER NOT NULL CHECK (available >= 0)
);
-- 购买逻辑(在事务中)
BEGIN;
-- 1. 加锁读取库存(FOR UPDATE 阻止其他事务同时修改)
SELECT available
FROM inventory
WHERE product_id = 42
FOR UPDATE;
-- 2. 应用层检查库存(假设可用量为 1)
-- 3. 扣减库存
UPDATE inventory
SET available = available - 1
WHERE product_id = 42;
-- 4. 创建订单
INSERT INTO orders (product_id, user_id, quantity) VALUES (42, 101, 1);
COMMIT;
-- 并发安全:第二个事务会在 FOR UPDATE 处等待,
-- 等第一个事务提交后,读到 available=0,然后失败退出

SELECT FOR UPDATE NOWAIT

-- NOWAIT:不等待,如果锁不可用立即报错
BEGIN;
SELECT * FROM inventory WHERE product_id = 42 FOR UPDATE NOWAIT;
-- 如果被锁:ERROR: could not obtain lock on row in relation "inventory"
-- 应用层捕获错误,提示用户"稍后重试"
-- SKIP LOCKED:跳过被锁定的行,适合任务队列
BEGIN;
SELECT id FROM job_queue
WHERE status = 'pending'
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- 多个 worker 可以并发取任务,互不干扰

乐观锁:版本号方案

-- 在表中添加 version 列(整数递增)
CREATE TABLE documents (
id         BIGSERIAL PRIMARY KEY,
title      TEXT NOT NULL,
content    TEXT,
version    INTEGER NOT NULL DEFAULT 1,  -- 乐观锁版本号
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 读取文档(记录当前版本号)
SELECT id, title, content, version FROM documents WHERE id = 1;
-- 结果:version = 5
-- 应用层编辑内容...
-- 提交更新时,用 version 做冲突检测
UPDATE documents
SET
title = '新标题',
content = '新内容',
version = version + 1,    -- 版本号递增
updated_at = NOW()
WHERE id = 1
AND version = 5;          -- 关键:只有 version 仍为 5 时才更新
-- 检查影响行数
-- 返回 1:更新成功(没有其他人在我们读取后修改过)
-- 返回 0:冲突!别人已经修改了文档,需要重新读取并重试

在应用层处理乐观锁冲突

# Python 示例(伪代码)
def update_document(doc_id, new_title, new_content, max_retries=3):
for attempt in range(max_retries):
# 1. 读取当前版本
doc = db.fetchone(
"SELECT id, title, content, version FROM documents WHERE id = %s",
[doc_id]
)
current_version = doc['version']
# 2. 应用层处理...(用户编辑等)
# 3. 尝试更新
affected = db.execute("""
UPDATE documents
SET title = %s, content = %s, version = version + 1
WHERE id = %s AND version = %s
""", [new_title, new_content, doc_id, current_version])
if affected == 1:
return {"success": True}  # 更新成功
# 冲突:重新读取并提示用户
if attempt < max_retries - 1:
time.sleep(0.1 * (2 ** attempt))  # 指数退避
return {"success": False, "reason": "conflict"}

乐观锁:updated_at 时间戳方案

-- 如果不想添加 version 列,可以用 updated_at 时间戳
-- 注意:精度要足够高,避免同毫秒的并发冲突
CREATE TABLE settings (
key        TEXT PRIMARY KEY,
value      JSONB NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 读取时记录 updated_at
SELECT key, value, updated_at FROM settings WHERE key = 'theme';
-- updated_at = '2024-03-22 10:30:00.123456+08'
-- 更新时校验
UPDATE settings
SET
value = '{"mode": "dark"}',
updated_at = NOW()
WHERE key = 'theme'
AND updated_at = '2024-03-22 10:30:00.123456+08';  -- 精确匹配
-- ⚠️ 注意:updated_at 方案在高并发下有极小概率失效
-- (两个请求恰好在同一微秒内读取)
-- 生产环境更推荐 version 整数方案

选型决策:何时用哪种锁

graph TD A["并发写入同一行"] --> B{"冲突发生频率?"} B --> |高(每秒多次)| C{"操作可以等待吗?"} B --> |低(偶尔)| D["乐观锁\n(version 列)"] C --> |可以等待| E["悲观锁\nSELECT FOR UPDATE"] C --> |不能等待| F["悲观锁\nSELECT FOR UPDATE NOWAIT\n+ 应用层提示重试"] D --> G{"重试代价?"} G --> |低(无副作用)| H["标准乐观锁\n+ 指数退避重试"] G --> |高(有副作用)| I["考虑悲观锁\n或队列化处理"]

具体场景对照

✅ 悲观锁适合:
- 商品库存扣减(超卖代价极高)
- 账户余额转账(金融一致性要求)
- 任务队列 worker 取任务(需要 SKIP LOCKED)
- 长事务中的复杂检查-修改操作
✅ 乐观锁适合:
- 用户编辑自己的资料(冲突极少)
- 文档/配置的版本管理(CMS 场景)
- 后台管理界面的表单提交(低并发)
- 分布式系统中避免全局锁的场景

实战:抢购秒杀场景的完整方案

-- 秒杀:高并发下限量商品的库存控制
-- 方案一:悲观锁(简单,但高并发下串行化严重)
BEGIN;
SELECT stock FROM flash_sale WHERE id = 1 FOR UPDATE;
-- 检查 stock > 0
UPDATE flash_sale SET stock = stock - 1 WHERE id = 1;
INSERT INTO flash_orders (user_id, sale_id) VALUES (101, 1);
COMMIT;
-- 方案二:乐观锁 + CHECK 约束(推荐,利用数据库约束防超卖)
CREATE TABLE flash_sale (
id      INTEGER PRIMARY KEY,
stock   INTEGER NOT NULL CHECK (stock >= 0)  -- 负库存直接报错
);
-- 直接用原子 UPDATE,让数据库的 CHECK 约束防超卖
-- 不用显式加锁,效率更高
UPDATE flash_sale
SET stock = stock - 1
WHERE id = 1 AND stock > 0;
-- 返回 0 行:说明库存不足(或已被他人抢走)
-- 返回 1 行:扣减成功,然后 INSERT 订单
-- 方案三:Redis 预扣减(超高并发场景)
-- 先用 Redis DECR 原子扣减,成功后异步写数据库
-- Redis stock 到 0 后立即拒绝,减轻数据库压力
-- (实现细节属于系统设计,超出本书范围)

下一章全文搜索与扩展插件——PostgreSQL 内置的全文搜索引擎,不需要单独部署 Elasticsearch,就能实现中英文分词、相关性排序和高亮显示。