数据库锁的应用策略
本文按锁的类型整理各自的适用场景、用法要点与避坑,偏应用策略,不绑定具体数据库方言。
一、先想清楚:锁保护的是什么
加锁之前,先写出要保住的业务不变量,再选机制:
| 场景 | 不变量 | 失败表现 |
|---|---|---|
| 库存 / 额度 | 余量不为负,扣减量正确 | 超卖、透支 |
| 状态机 | 只允许合法迁移 | 已完成被改回初始态 |
| 唯一资源 | 同一资源只被占用一次 | 重复领券、重复占座 |
| 多字段一致更新 | 读到的是同一版完整数据 | 半更新、丢更新 |
动手前再问三件事:
- 冲突频率:热点行还是偶尔撞车?
- 失败可否重试:能退避重试,还是必须当场串行成功?
- 临界区有多长:只改几行,还是中间还要调外部服务?
锁策略由冲突形态决定,不由「感觉要更安全」决定。
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)
- 把不变量写进更新条件(条件更新)
适合:
- 读多写少、冲突率低
- 状态机、资料整行更新
- 调用方可接受短暂失败并重试
不适合:
- 同一热点行高频争用(重试风 暴)
- 必须「拿到资源再往下走」的串行流程
应用要点:
- 更新必须检查影响行数(或等价返回值),不能只看「语句没报错」。
- 冲突后要有有限重试 + 退避,并区分业务失败与并发冲突。
- 可重入、可幂等的接口才能放心重试。
2.2 悲观锁
假设冲突很可能发生,先获取锁再读算写,持锁期间他人无法并发改同一资源。
典型手段:
- 事务内的排他读(如
SELECT ... FOR UPDATE一类语义) - 显式表/行锁、咨询锁
适合:
- 热点行、强一致扣减
- 多步读写必须基于同一快照串行完成
- 事务可以做得很短
不适合:
- 持锁期间做远程调用、人工确认、长计算
- 冲突其实很低却全盘加锁(白白降低吞吐)
应用要点:
- 持锁短:锁内只做必要计算与写库。
- 顺序固定:多行/多资源加锁时全局统一排序,降低死锁。
- 范围可控:只锁真正要改的数据,避免扫大片无关行。
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 咨询锁 / 命名锁(应用层互斥令牌)
数据库提供的「按名字加锁」能力:不锁业务行本身,而是锁一个逻辑键。
策略要点:
- 适合「同一业务键的一段代码全局互斥」,且临界区仍落在同一数据库会话/事务模型内。