题目:乐观锁与悲观锁如何实现?
难度:medium
标签:db/mysql/transaction, mysql/optimistic-lock, difficulty/medium
评分:⭐⭐⭐☆(3/5)
MySQL 中乐观锁与悲观锁。乐观锁其实就是比较交换,先比较一下,比如设一个版本号,版本比较之前跟比较之后,如果是比较之前是一致的,然后就可以更新,如果版本号不一致就不更新。所谓乐观锁就是觉得它没什么问题,不用加锁,然后如果发生了变动才判定这个锁失效了,就不再执行。悲观锁的话,就是直接强加锁,for update,update 这种直接就是本身的 MySQL 里边它自己的锁,它本身的锁就是一个悲观锁的执行,并不是乐观锁。比如它的行锁、列锁、间隙锁,这些其实全都是悲观锁。如何实现呢?悲观锁它实现的基于场景其实是在索引上加锁,把当前的要处理的行上加一个锁,或者是对应的一个空间,一个锁的,在区间内加锁,区间锁,锁住这部分数据,在其他的执行时候,其他数据执行来过来执行的时候,如果是更新的写锁,其他来读死都不能读,然后就会导致对应的数据,让其他的 SQL 来执行的话,事务是不可以查到的。
优点:
SELECT FOR UPDATE / UPDATE / DELETE 隐式加锁,MySQL 原生锁属于悲观锁不足:
UPDATE ... SET version=version+1 WHERE id=? AND version=?SELECT FOR UPDATE NOWAIT / SKIP LOCKED(MySQL 8.0+)LOCK IN SHARE MODE(8.0 起 FOR SHARE)innodb_deadlock_detect)、SHOW ENGINE INNODB STATUS 查看锁信息补充:
version 列 + CAS 更新,影响行数 0 即冲突SELECT ... FOR UPDATE(排他)、SELECT ... FOR SHARE(共享)、LOCK IN SHARE MODE(5.7 语法)FOR UPDATE NOWAIT 立即失败、FOR UPDATE SKIP LOCKED 跳过加锁行(做队列消费超常用)innodb_deadlock_detect=ON(默认),通过 wait-for graph 检测,回滚 undo log 少的核心考点定位:理解乐观锁与悲观锁的本质差异(冲突时机的假设不同),掌握 MySQL 中的具体实现方式与选型判断。
知识点拆解:
version 列: UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?;
影响行数 = 1 表示成功;影响行数 = 0 表示版本冲突,业务层选择重试或失败。这也是 CAS(Compare-And-Swap) 的数据库版本。
SELECT ... FOR UPDATE:加排他锁(X 锁),其他事务读/写都要等SELECT ... FOR SHARE(8.0+)/ LOCK IN SHARE MODE(5.7):加共享锁(S 锁),其他事务读可以,写要等UPDATE / DELETE:隐式加 X 锁(不需要显式声明)INSERT:隐式加 S 锁(gap + insert intention lock)FOR UPDATE NOWAIT:拿不到锁立即返回错误,不阻塞FOR UPDATE SKIP LOCKED:跳过已加锁的行,用于"队列消费"模式LOCKED_COLUMNS=NONE:锁表而不是锁行SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK 段information_schema.innodb_trx 查看当前事务information_schema.innodb_lock_waits(8.0 之前)/ performance_schema.data_lock_waits(8.0)看锁等待链innodb_lock_wait_timeout 控制等待超时(默认 50 秒)innodb_deadlock_detect=ON(默认)通过 wait-for graph 检测死锁,回滚 undo log 少的考察方向:
延伸递进面试题:
SELECT FOR UPDATE 在 RC 和 RR 隔离级别下锁的粒度有什么区别?FOR UPDATE SKIP LOCKED 可以用来做什么?version 列,遇到重试风暴怎么办?UPDATE ... WHERE old_val=? 代替 version。面试官您好,乐观锁和悲观锁本质上是两种对"冲突概率"的假设。悲观锁假设"我一定会冲突",操作前先加锁,别人只能等;乐观锁假设"大概率不会冲突",先不加锁直接干活,提交前再校验,冲突才失败重试。
悲观锁在 MySQL 里是原生支持的。核心语法是 SELECT ... FOR UPDATE,它会给读到的行加排他锁,其他事务读、写都得等。8.0 之后还有 SELECT ... FOR SHARE,加的是共享锁,其他事务读可以并行,只有写要等。UPDATE 和 DELETE 本身就会隐式加 X 锁,不需要显式声明。加锁粒度主要有行锁、临键锁(next-key lock)、间隙锁,RR 隔离级别下默认用临键锁防止幻读。
8.0 还有两个增强语法我觉得很实用:FOR UPDATE NOWAIT,拿不到锁立即报错不阻塞;FOR UPDATE SKIP LOCKED,跳过已经被加锁的行,特别适合任务队列场景——多个 worker 抢任务,跳过的行让下一个 worker 处理,互不干扰。
乐观锁 MySQL 没有原生实现,要在应用层做。标准做法是表里加一个 version 列,更新的时候带上版本条件:UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?。这条 SQL 的影响行数如果是 1,说明成功;如果是 0,说明别人在我读和写之间改过了,版本对不上,业务层决定是重试还是失败。这本质就是 CAS,Compare-And-Swap 的数据库版本。
选型上,悲观锁适合高冲突、强一致的场景,比如余额扣减、订单支付;乐观锁适合低冲突、能容忍重试的场景,比如点赞计数、普通库存扣减。悲观锁的代价是并发度低、有死锁风险、有超时问题,MySQL 默认 innodb_lock_wait_timeout=50s,超过就报错。死锁的话 innodb_deadlock_detect=ON(默认开启),InnoDB 会通过 wait-for graph 检测到死锁,回滚 undo log 少的那个事务。要排查线上死锁看 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段,或者查 performance_schema.data_lock_waits 看锁等待链。
https://chat.deepseek.com/share/1f4jfjd7lcbtnwc0hu