跳到主要内容

数据库锁的应用策略

本文按锁的类型整理各自的适用场景、用法要点与避坑,偏应用策略,不绑定具体数据库方言。


一、先想清楚:锁保护的是什么

加锁之前,先写出要保住的业务不变量,再选机制:

场景不变量失败表现
库存 / 额度余量不为负,扣减量正确超卖、透支
状态机只允许合法迁移已完成被改回初始态
唯一资源同一资源只被占用一次重复领券、重复占座
多字段一致更新读到的是同一版完整数据半更新、丢更新

动手前再问三件事:

  1. 冲突频率:热点行还是偶尔撞车?
  2. 失败可否重试:能退避重试,还是必须当场串行成功?
  3. 临界区有多长:只改几行,还是中间还要调外部服务?

锁策略由冲突形态决定,不由「感觉要更安全」决定。

flowchart TD
A[写出业务不变量] --> B{冲突多吗?}
B -->|少| O[乐观: 版本号 / 条件更新]
B -->|多或必须串行| P[悲观: 行锁 / SELECT FOR UPDATE]
O --> C{失败可重试?}
C -->|是| R[退避重试]
C -->|否| F[明确失败给用户]
P --> D{临界区短吗?}
D -->|否| X[缩小事务 / 别锁着调 RPC]
D -->|是| S[事务内读写后提交]

二、按控制思路:乐观锁与悲观锁

这是最上层的二分,后面所有具体锁都落在这两侧。

2.1 乐观锁

假设冲突很少,先读后改,提交时用版本或条件校验;冲突则失败或重试,而不是先占住资源。

典型手段:

  • 版本号 / 时间戳字段(CAS)
  • 把不变量写进更新条件(条件更新)

适合:

  • 读多写少、冲突率低
  • 状态机、资料整行更新
  • 调用方可接受短暂失败并重试

不适合:

  • 同一热点行高频争用(重试风暴)
  • 必须「拿到资源再往下走」的串行流程

应用要点:

  1. 更新必须检查影响行数(或等价返回值),不能只看「语句没报错」。
  2. 冲突后要有有限重试 + 退避,并区分业务失败与并发冲突。
  3. 可重入、可幂等的接口才能放心重试。

2.2 悲观锁

假设冲突很可能发生,先获取锁再读算写,持锁期间他人无法并发改同一资源。

典型手段:

  • 事务内的排他读(如 SELECT ... FOR UPDATE 一类语义)
  • 显式表/行锁、咨询锁

适合:

  • 热点行、强一致扣减
  • 多步读写必须基于同一快照串行完成
  • 事务可以做得很短

不适合:

  • 持锁期间做远程调用、人工确认、长计算
  • 冲突其实很低却全盘加锁(白白降低吞吐)

应用要点:

  1. 持锁短:锁内只做必要计算与写库。
  2. 顺序固定:多行/多资源加锁时全局统一排序,降低死锁。
  3. 范围可控:只锁真正要改的数据,避免扫大片无关行。

2.3 怎么选

冲突频繁? ──是──▶ 事务能极短且必须成功? ──是──▶ 悲观锁
│ │
│ └──否──▶ 排队串行 / 分片热点 + 乐观或悲观

└──否──▶ 乐观锁(版本号或条件更新)

实际系统常是:冷路径乐观,热路径悲观或排队


三、按互斥强度:共享锁与排他锁

3.1 共享锁(读锁,S)

多人可同时持有,用于「读的时候不希望被改写」。

应用策略:

  • 需要基于稳定快照做只读聚合、校验,且期间不能被写打乱时使用。
  • 业务上更常见的是:读路径尽量无锁或快照读;真正要改时再上排他。
  • 避免「长时间持有共享锁」:会堵住写路径,拖垮吞吐。

3.2 排他锁(写锁,X)

独占资源,保证写过程中不会被他人读写干扰(具体读是否阻塞取决于隔离与实现,应用层按「写互斥」理解即可)。

应用策略:

  • 所有会改业务不变量的路径,最终都要落到某种写互斥:排他锁、条件更新或唯一约束。
  • 写锁粒度越小越好:能锁一行不锁一表。
  • 持有写锁时不要扩展无关查询,防止锁集合意外变大。

3.3 组合原则

路径建议
纯展示、可容忍稍旧数据不加锁,或用快照读
读完立刻按读结果改悲观:排他读;乐观:版本/条件写
只读校验且必须与写互斥短共享锁,尽快释放

四、按作用范围:行锁、范围锁、表锁

粒度决定并发度误伤面,是应用策略里仅次于乐观/悲观的选择。

4.1 行级锁

锁住单条(或少量)记录,现代 OLTP 的默认选择。

应用策略:

  • 业务更新尽量按主键 / 业务唯一键定位单行。
  • 批量改多行时:缩小集合、固定顺序、拆短事务。
  • 行锁的正确性仍依赖「更新条件或持锁读」表达不变量,不是「锁了就一定对」。

4.2 范围锁 / 间隙类锁

锁住一段键范围,用来防止范围内「幻影插入/删除」干扰当前事务的判断。

应用策略:

  • 需要「这段区间在事务期间保持稳定」(如连续编号、区间配额)时才值得用。
  • 能改成点查 + 行锁的,优先点查,减少范围锁带来的并发塌陷。
  • 避免宽泛条件更新(大状态、大时间窗)与在线热点写叠在一起。

4.3 表级锁

锁住整表,实现简单,代价是整表写(甚至读)排队。

应用策略:

  • 适合低频运维:结构变更、全表重算、迁移窗口内的互斥。
  • 不适合在线请求路径的常规业务写。
  • 若业务只能表锁,优先考虑改为行级模型或拆表,而不是长期依赖表锁硬扛流量。

4.4 粒度选择速记

能行锁 → 不行锁表
能点查 → 不扫范围
能短事务 → 不把多步远程流程塞进同一把锁

五、各类锁的落地模式

下面按常用机制给出可套用的应用模式(语法示意为通用 SQL 语义)。

5.1 条件更新(轻量乐观,优先考虑)

把不变量写进 WHERE,利用单条更新的原子性:

UPDATE inventory
SET stock = stock - :qty
WHERE sku_id = :sku_id
AND stock >= :qty;
-- 影响行数 = 1 才算成功

策略要点:

  • 计数器、库存、次数、余额扣减的首选之一。
  • 无需单独 version 字段也能防超卖。
  • 冲突高时要配合重试、限流或热点治理,而不是无限重试打爆库。

5.2 版本号乐观锁(CAS)

UPDATE orders
SET status = :new_status,
version = version + 1
WHERE id = :id
AND version = :old_version;

更稳妥时,把当前合法状态一并写进条件:

UPDATE orders
SET status = 'paid', version = version + 1
WHERE id = :id
AND version = :old_version
AND status = 'pending';

策略要点:

  • 适合状态机、配置、文档型整行更新。
  • 读—改—写路径必须带着读到的 version 回来。
  • 冲突即「有人抢先改过」,按业务选择重试或返回冲突。

5.3 事务内排他读(悲观行锁)

BEGIN;
SELECT ... FROM account WHERE id = :id FOR UPDATE;
-- 基于锁定行计算并 UPDATE / INSERT
COMMIT;

策略要点:

  • 多步读写依赖同一行最新值时使用。
  • 多资源时按稳定顺序加锁(例如按 id 升序)。
  • 锁内禁止外部 IO;需要外部结果时,先准备数据再开短事务,或改用状态机 + 幂等。

5.4 唯一约束(隐式互斥)

-- 同一用户同一活动只能一条
UNIQUE (user_id, activity_id)

策略要点:

  • 「只允许存在一条」类问题,优先约束,少手写锁。
  • 配合幂等键:重试时插入冲突即视为已成功或已处理。
  • 约束保证最终互斥;若还要附带额度扣减,仍需条件更新或事务锁。

5.5 咨询锁 / 命名锁(应用层互斥令牌)

数据库提供的「按名字加锁」能力:不锁业务行本身,而是锁一个逻辑键。

策略要点:

  • 适合「同一业务键的一段代码全局互斥」,且临界区仍落在同一数据库会话/事务模型内。
  • 锁的是流程,不是行:行级不变量仍建议用条件更新或行锁表达。
  • 注意会话断开、超时释放与锁名称设计,避免误互斥或死锁式互相等待。

5.6 与分布式锁的分工

需求更合适的手段
同一库内行数据正确条件更新 / 行锁 / 唯一约束
跨进程任务互斥、防定时器重入分布式锁或任务租约
两者都有分布式锁包「进入临界区」,数据仍用库内机制保证

策略要点:

  • 分布式锁不能替代行级一致性。
  • 锁租约应覆盖最长临界区并保留续租余量;无法保证时必须配合 fencing token / 版本号。
  • 能单库事务解决的,不要引入跨中间件锁。

六、场景化配方(锁类型怎么组合)

6.1 库存 / 额度扣减

优先级策略
默认条件更新(余量 >= 扣减量
冲突升高有限重试;仍不够则热点分片或多桶库存
多步强一致短事务 + 行级排他读,再写流水

6.2 账户转账

  • 悲观:两行按固定顺序排他锁 → 校验 → 双边更新 + 流水。
  • 流水用业务唯一号做幂等。
  • 避免先锁 A 后锁 B、另一笔相反顺序。

6.3 订单 / 工单状态机

  • 乐观版本号 + 状态条件更新。
  • 非法迁移直接失败,不依赖「先读后改」的内存判断 alone。
  • 对外回调用幂等,防止重复通知造成二次迁移。

6.4 防重复领取 / 防重复下单

  • 唯一约束为主。
  • 处理中状态用 CAS:pending → processing → done
  • 客户端幂等键贯穿全链路。

6.5 任务抢占

  • 行级互斥 + 「跳过已被占用项」或租约字段(谁占有、过期回收)。
  • 避免长事务抱着大批任务行不放。
  • Worker 崩溃靠租约超时回收,而不是无限持锁。

6.6 配置 / 主数据更新

  • 默认版本号乐观锁。
  • 发布窗口若需整批一致,用短事务批量写,或「写新版本 + 原子切换指针」。

七、横切策略:死锁、超时与可观测

这些不对应某一种锁,但决定策略能否上线。

7.1 降低死锁

  1. 全局加锁顺序(资源 id 排序)。
  2. 缩小锁范围与持锁时间
  3. 避免锁内再触发大范围查询
  4. 热点从「硬碰硬抢锁」改为排队或分片。

死锁在并发系统中难以为零:把它当成可重试错误,整段事务有限次重试即可。

7.2 失败分类

类型处理
业务不满足(余额不足、状态非法)直接返回业务错误,不盲目重试
并发冲突(版本过期、影响 0 行)有限退避重试或让客户端刷新
死锁 / 锁等待超时整事务重试,并监控频率
唯一键冲突按幂等语义转为成功或明确冲突

7.3 上线检查清单

  • 不变量是否落在更新条件 / 约束 / 状态机里?
  • 乐观路径是否检查影响行数并限制重试?
  • 悲观路径是否短事务、固定顺序?
  • 是否误用表锁或大范围锁挡在线流量?
  • 是否用分布式锁代替了库内行级保证?
  • 接口是否幂等?死锁与冲突是否有指标?

八、类型与策略速查

锁 / 机制控制思路典型用途关键策略
条件更新乐观库存、额度、计数检查影响行数;冲突有限重试
版本号 CAS乐观状态机、资料更新版本 + 合法状态双条件
共享锁悲观读短时稳定只读尽快释放,少用于长流程
排他行锁悲观写多步一致扣减、转账短事务、顺序加锁
范围锁悲观区间稳定性能点查不扫范围
表锁悲观运维窗口、低频互斥远离在线热路径
唯一约束隐式互斥防重、占坑优先于手写锁
咨询锁逻辑互斥按业务键串行一段逻辑与行级约束配合
分布式锁跨进程互斥任务防重、跨服务临界区不替代库内一致性

九、小结

  1. 先定不变量,再选锁类型;乐观与悲观由冲突频率和可否重试决定。
  2. 粒度尽量小:行优于范围,范围优于表;点查优于宽条件。
  3. 条件更新与唯一约束往往比显式加锁更简单、更稳。
  4. 悲观锁的生命线是短事务和固定顺序;乐观锁的生命线是冲突处理与幂等。
  5. 分布式锁管互斥边界,数据库机制管数据正确——两者职责不要反转。

一句话:

正确性靠不变量与原子更新,并发度靠粒度和冲突形态,稳定性靠短临界区、固定顺序和可重试的失败处理。