Skip to Content
面试题库Juc[面试题] 怎么使用MySQL实现一个乐观锁?

[面试题] 怎么使用MySQL实现一个乐观锁?

📖 版块 A:知识讲解版 (温故知新)

  • 核心理念:乐观锁(Optimistic Locking)假设冲突不经常发生。在操作数据时不会直接加锁,而是在更新提交的那一刻,通过对比版本号(Version)或时间戳(Timestamp)来检测数据是否被其他事务修改过。

  • 实现步骤

    1. 添加版本字段:在数据库表中增加一个 version 字段(通常是整数类型,初始值为 0)。
    2. 读取数据:查询当前记录时,连同 version 字段一起查出。
    3. 业务处理:在内存中完成业务逻辑计算。
    4. 提交更新:执行 SQL 更新语句,其核心逻辑是 UPDATE ... SET version = version + 1, ... WHERE id = #{id} AND version = #{old_version}
    5. 判断结果:根据更新受影响的行数(Rows Affected)来判断是否更新成功。如果返回 0,说明数据已被他人修改。
  • 优势与局限

    • 优势:由于不需要数据库层面的物理排他锁(行锁),在读多写少的场景下性能极高,避免了死锁风险。
    • 局限:在高并发写、竞争激烈的场景下,更新失败率会大幅上升,导致频繁重试。

🎤 版块 B:面试实战版 (高手逐字稿)

Q:怎么使用 MySQL 实现一个乐观锁?

💬 高手逐字稿

在 MySQL 中实现乐观锁,最标准也是最常用的做法是基于“版本号(Version)”机制。

具体的实现流程是这样的:首先,我们需要在业务表中增加一个 version 字段。当我们需要修改一条记录时,先查询出这条数据的当前版本号。在执行更新 SQL 时,我们会带上一个过滤条件,即 WHERE 子句里必须包含 version = #{刚才查出的版本号},同时在 SET 语句中让 version = version + 1

由于数据库本身会保证单行更新的原子性,如果有两个线程同时拿到了同一个版本号,只有一个线程能更新成功并返回受影响行数为 1,另一个线程由于版本号已经变了,会匹配不到记录,返回受影响行数为 0。

这里有一个细节需要注意,就是对于更新失败的情况,我们通常需要设计“重试机制”。在 Java 代码中,如果感知到更新返回 0,可以采用自旋的方式重新拉取数据再执行一次更新,或者直接向前端抛出异常。

对比悲观锁(比如 FOR UPDATE),乐观锁最大的好处是在整个业务处理期间不需要持有数据库锁。如果是一个耗时很长的业务逻辑,悲观锁会导致其他线程长时间阻塞,而乐观锁只在最后更新的那一瞬间生效,极大地提高了系统的并发吞吐量。

💡 高手自问自答:“那么,如果业务场景对数据一致性要求极高,但又不想增加版本号字段,有替代方案吗?”

其实也是有的。我们可以使用“状态机锁”或者“全字段对比”。比如对于订单系统,我们可以执行 UPDATE orders SET status = 'FINISHED' WHERE id = 1 AND status = 'PAYED'。这种基于业务状态的流转,本质上也是一种乐观锁思想。

另外,还有一个容易被忽视的问题是“ABA 问题”。虽然在简单的 version 递增逻辑下,MySQL 乐观锁天然免疫 ABA 问题(因为版本号只会往上加,不会回滚),但在一些复杂的业务逻辑中,如果版本号可能会被重置,那么就需要结合时间戳或其他唯一标识来兜底。在写高并发系统时,我通常会建议只在读多写少的场景用乐观锁;如果写竞争非常激烈,乐观锁的重试开销反而会成为性能瓶颈,这时候切回悲观锁或分布式红锁反而是更明智的选择。

最后更新于:
⏱️

登录后开始阅读

当前 MVP 版本只开放邮箱密码登录注册。完成登录后即可进入面试题内容。

还没有账号?先去注册