← 返回题目列表

数据库乐观锁 version 字段怎么设计?适合解决什么问题?

高频 中等 第 13 / 33 题 更新于 2026/07/29
数据库设计乐观锁version并发控制

简化版

乐观锁通常通过 version 字段或更新时间字段实现,更新时带上旧版本条件,只有版本匹配才允许修改。它适合冲突不高但需要防止并发覆盖的场景,如库存、配置、资料编辑;冲突很高时要考虑悲观锁、队列化或原子扣减。

详细版

乐观锁的核心是“先读版本,更新时校验版本”。典型 SQL:

update product
set stock = stock - 1,
    version = version + 1
where id = 1
  and version = 10
  and stock > 0;

如果影响行数为 1,更新成功;如果为 0,说明数据被别人改过或库存不足,业务要重试或返回失败。

设计要点:

  • version 用整数递增,简单可靠。
  • 更新必须带旧版本条件。
  • 失败后不能无脑无限重试。
  • 乐观锁防并发覆盖,不等于解决所有一致性问题。
  • 高热点扣减要结合库存预占、队列或缓存原子操作。

完整版教学

一、乐观锁解决的是并发覆盖问题

并发覆盖最典型的例子是两个用户同时编辑同一条资料。A 读到姓名和地址,B 也读到同一份数据;B 先保存,A 后保存,如果 A 的更新没有校验版本,就可能把 B 的修改覆盖掉。

乐观锁的思路是:读取时拿到版本号,写回时要求版本号仍然没变。如果版本已经变化,说明中间有人改过,这次更新不能直接覆盖。

T1 读取 version=10
T2 读取 version=10
T2 更新成功,version=11
T1 按 version=10 更新,影响行数=0,发现冲突

记忆钩子:乐观锁不是锁住数据,而是在提交时问一句“我读到的版本还是最新版吗”。

二、version 字段是最常见实现

最常见设计是在表里加一个整数 version 字段,默认从 01 开始。每次更新时把 version1,并在 where 条件里带上旧版本。

update user_profile
set nickname = ?,
    avatar = ?,
    version = version + 1
where id = ?
  and version = ?;

这个 SQL 的关键不是 version = version + 1,而是 where version = ?。没有旧版本条件,就只是普通更新。

如果影响行数是 0,业务要告诉用户“数据已被修改,请刷新后重试”,或者自动重新读取、合并再提交。

三、updated_at 也能做版本,但不如整数稳定

有些系统用 updated_at 做乐观锁条件,例如 where updated_at = ?。这在简单系统里可行,但要注意时间精度和自动更新策略。

如果数据库时间精度只有秒,同一秒内两次更新可能拿到相同时间;如果批量修复数据会改变 updated_at,也可能制造额外冲突。

方案优点风险
version bigint简单、单调、精确需要多一个字段
updated_at少字段,可读精度和自动更新时间可能带来误判
哈希摘要能发现内容变化计算成本高,字段多时复杂

面试中推荐优先说整数 version,再补充 updated_at 可以用于简单场景。

四、乐观锁更新要和业务条件一起写

很多并发问题不是只看版本,还要看业务条件。比如扣库存时,除了版本匹配,还要保证库存大于 0。

update sku_stock
set stock = stock - 1,
    version = version + 1
where sku_id = 100
  and version = 27
  and stock >= 1;

如果只判断版本,不判断库存,可能出现负库存。如果只判断库存,不判断版本,某些读改写场景又可能覆盖其他字段。

影响行数为 0 时,要区分是版本冲突还是库存不足。可以重新查一次当前数据,再决定提示用户、重试或走补偿。

五、乐观锁不适合所有高并发热点

乐观锁适合冲突概率不高的场景。因为一旦冲突很多,大量请求都会失败重试,数据库压力反而更大。

假设 100 个请求同时抢最后 10 件库存,如果都读取同一个版本,可能只有少数成功,其他请求反复重试。重试次数过多时,吞吐会下降,延迟会升高。

冲突低:100 次请求,2 次冲突 -> 乐观锁很划算
冲突高:100 次请求,90 次冲突 -> 重试成本很高

高热点场景可以考虑库存分段、队列串行化、Redis 原子扣减、预占库存、限流等方案,乐观锁只是其中一层保护。

六、失败处理是乐观锁设计的一部分

乐观锁失败不是异常情况,而是设计内的正常结果。系统必须明确失败后怎么处理。

常见策略:

  • 用户编辑资料:提示刷新后重试。
  • 后台配置修改:展示差异,让用户选择覆盖或合并。
  • 库存扣减:有限重试,超过次数返回售罄或繁忙。
  • 定时任务抢占:影响行数为 0 就跳过。

不要无限重试。比如每次冲突后最多重试 3 次,每次退避 20ms、50ms、100ms,避免把数据库打爆。

七、常见误区与追问

  • 误区:加了 version 字段就是乐观锁。 真正生效的是更新时带旧版本条件,并检查影响行数。
  • 误区:乐观锁能解决所有并发问题。 它主要防覆盖,高热点资源争抢还需要限流、队列、拆分或原子操作。
  • 误区:乐观锁失败就是系统错误。 失败是预期分支,业务要设计重试、提示或放弃。
  • 追问:version 应该用什么类型? 通常用 intbigint,长期高频更新表更推荐 bigint
  • 追问:乐观锁和悲观锁怎么选? 冲突低用乐观锁,冲突高且必须串行时考虑悲观锁或队列化。
  • 追问:为什么要检查影响行数? 因为影响行数是版本匹配和业务条件是否成立的最终结果。

八、面试中可以这样落地

可以用“商品库存扣减”回答。表里有 stockversion,扣减时 SQL 同时判断库存和版本,成功后提交,失败后重新读取决定是否重试。

update sku_stock
set stock = stock - ?,
    version = version + 1
where sku_id = ?
  and version = ?
  and stock >= ?;

这个方案还可以继续扩展:如果热点很高,就把库存拆成多个库存桶,或者先在缓存中预扣,再异步落库并对账。这样回答不会把乐观锁讲成万能钥匙。

九、加强记忆

乐观锁记住三个动作:读版本、带版本更新、看影响行数。它的价值是防止并发覆盖,适合低冲突场景;它的边界是高热点下重试成本高。面试时再补一句“失败处理也是设计的一部分”,就能体现工程完整性。