乐观锁的业务场景及实现方式
乐观锁(Optimistic Lock)
- 每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
- 比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
定义与适用面清楚了,落地还差三块:两种主流实现(version 与 CAS 字段)的 SQL 形态、重试策略的工程细节、以及乐观/悲观的量化选型依据。本文补齐成可直接落库的方案。
实现一:版本号(version)
表加 version 列,读时带出,写时校验并自增:
-- 读 |
要点:
- 校验与更新在同一条 UPDATE 内原子完成,不存在”检查后、更新前”的窗口
- 影响行数为 0 即冲突:业务选择重试(重新读-改-写)或报错给用户
- version 单调递增,天然防 ABA
- ORM 支持:JPA
@Version、MyBatis-Plus@Version自动织入校验,手写 SQL 需自行维护
实现二:条件字段(状态前置 / 业务 CAS)
不引入 version,用业务字段本身做预期值:
-- 库存扣减:以"当前库存"为预期值 |
count > 0 与 status = 'CREATED' 就是乐观校验:并发下只有一个 UPDATE 能匹配到行。状态机流转天然适合这种写法——前置状态即版本号,语义更内聚,还省一列。
重试策略的工程细节
冲突后重试不是简单循环,要注意:
- 重试上限与退避:3 次左右 + 随机退避(避免冲突风暴下同步重试互相踩踏)
- 重读再改:重试必须重新 SELECT 最新数据再计算,不能拿旧快照反复提交(否则永远冲突)
- 事务边界:version 校验 UPDATE 与同事务其他写操作一起提交;长事务持旧 version 会放大冲突率,保持事务短小
- 用户体验:面向用户的编辑冲突(表单提交)通常不自动重试,而是返回”数据已被他人修改,请刷新”——自动重试会用旧表单覆盖新数据,反而错
- 监控:冲突率是健康指标;持续高冲突说明热点行,应考虑拆分(如库存分桶)或改排队串行化
乐观 vs 悲观的量化选型
| 维度 | 乐观锁 | 悲观锁(SELECT … FOR UPDATE) |
|---|---|---|
| 冲突频率假设 | 低 | 高 |
| 读多写少 | 优(读无锁) | 差(读也加锁) |
| 写密集/热点行 | 差(重试风暴) | 优(排队串行) |
| 死锁风险 | 无 | 有(多锁顺序) |
| 实现复杂度 | 低(一列/一条件) | 中(事务与锁顺序管理) |
经验阈值:冲突率 <5% 乐观锁收益明显;>20% 重试开销反超加锁,转悲观或串行化(单队列消费、分片热点)。热点账户扣款这类”写写必冲突”场景,乐观锁是最差选择。
与分布式场景的衔接
跨服务的并发更新(微服务各持一部分状态)数据库 version 管不到,需上移:
- 接口幂等键 + 状态前置校验(业务 CAS 的 API 版:
if-match语义) - 分布式锁串行化热点(代价是吞吐)
- 事件溯源/CRDT 等最终一致模型(彻底放弃同步校验)
乐观锁的精髓是**把冲突检测从”读时加锁”推迟到”写时校验”**,用极低概率的重试换取读路径的零开销。version 与条件字段是它的两种 SQL 形态,重试策略与冲突率监控是它的工程配套;选型时先估冲突率,再决定乐观还是悲观。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus











