乐观锁与悲观锁 — 概念、原理与实践
2026/8/5 23:45:00 网站建设 项目流程

乐观锁与悲观锁 — 概念、原理与实践


一、基本概念

1.1 什么是锁

在并发场景下,多个操作同时修改同一条数据会产生冲突。锁是一种机制,用来协调并发访问,保证数据一致性。

1.2 核心区别

维度乐观锁悲观锁
哲学“先干活,提交时再检查有没有冲突”“先占坑,干活期间别人不许动”
加锁时机不加锁,更新时检查读取时就加锁
冲突假设假设冲突很少发生假设冲突经常发生
冲突处理检测到冲突后重试或报错让其他人等待,避免冲突
类比自助结账:扫完商品发现价格变了,重新扫收银台排队:一次只服务一个人

1.3 生活类比

乐观锁 = 在线编辑文档: 你和同事同时编辑同一段文字 你先提交 → 成功 同事提交 → 系统提示"内容已被修改,请刷新后重试" 悲观锁 = Word 文件共享锁: 你打开文件 → 文件被锁定为"只读"给其他人 其他人想编辑 → 提示"文件已被XXX锁定,请等待" 你保存关闭 → 其他人才能编辑

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、悲观锁(Pessimistic Locking)

2.1 原理

“先锁后操作”— 在读取数据时就加排他锁,其他事务无法读取(共享锁)或修改(排他锁)同一行数据,直到当前事务提交或回滚。

2.2 数据库底层实现

SELECT … FOR UPDATE
-- 事务ABEGIN;SELECT*FROMstockWHEREitem_id=100FORUPDATE;-- 此时数据库在 item_id=100 这一行加了排他锁(X锁)-- 事务A可以读写这行数据-- 事务B(同时执行)BEGIN;SELECT*FROMstockWHEREitem_id=100FORUPDATE;-- 阻塞!等待事务A释放锁...-- 直到事务A执行 COMMIT 或 ROLLBACK-- 事务A提交UPDATEstockSETqty=qty-1WHEREitem_id=100;COMMIT;-- 释放锁-- 此时事务B的 SELECT FOR UPDATE 返回结果(拿到最新数据)-- 事务B继续执行...
为什么叫"悲观"?

因为它悲观地认为"别人一定会来修改我正在操作的数据",所以提前加锁把别人挡住。

MySQL InnoDB 锁的层级
┌──────────────────────────────────────────┐ │ MySQL InnoDB 锁体系 │ ├──────────────────────────────────────────┤ │ 表级锁(Table Lock) │ │ - 意向共享锁(IS) │ │ - 意向排他锁(IX) │ ├──────────────────────────────────────────┤ │ 行级锁(Row Lock)← FOR UPDATE 使用的 │ │ - 共享锁(S Lock):允许多个事务同时读 │ │ - 排他锁(X Lock):只允许一个事务读写 │ ├──────────────────────────────────────────┤ │ 间隙锁(Gap Lock) │ │ - 锁住索引范围间的"间隙" │ │ - 防止幻读 │ └──────────────────────────────────────────┘
FOR UPDATE 加的是什么锁?
SELECT * FROM stock WHERE item_id = 100 FOR UPDATE; - 如果 item_id 是主键/唯一索引 → 加【行锁】(只锁这一行) - 如果 item_id 是普通索引 → 加【行锁 + 间隙锁】 - 如果没有索引 → 加【表锁】(全表扫描,锁所有行)!!! ⚠️ 重要:WHERE 条件必须命中索引,否则退化为表锁,性能灾难!

2.3 共享锁 vs 排他锁

锁类型SQL含义兼容性
共享锁(S Lock)SELECT ... LOCK IN SHARE MODE允许多个事务同时读S 与 S 兼容
排他锁(X Lock)SELECT ... FOR UPDATE只允许一个事务操作X 与任何锁都不兼容
事务A加了S锁:事务B可以加S锁(一起读),不能加X锁(不能写) 事务A加了X锁:事务B不能加S锁也不能加X锁(不能读也不能写)

2.4 死锁问题

-- 事务ABEGIN;SELECT*FROMstockWHEREitem_id=1FORUPDATE;-- 锁住行1-- 尝试锁行2...SELECT*FROMstockWHEREitem_id=2FORUPDATE;-- 等待事务B释放-- 事务B(同时)BEGIN;SELECT*FROMstockWHEREitem_id=2FORUPDATE;-- 锁住行2-- 尝试锁行1...SELECT*FROMstockWHEREitem_id=1FORUPDATE;-- 等待事务A释放-- A等B,B等A → 死锁!-- MySQL 检测到死锁后会自动杀掉一个事务(回滚代价小的那个)

预防死锁:多表/多行操作时,所有事务按相同顺序加锁。


三、乐观锁(Optimistic Locking)

3.1 原理

“不加锁,提交时检查”— 读取数据时不加锁,更新时检查数据是否被其他人修改过。如果被修改过,则拒绝本次更新。

3.2 实现方式

方式1:版本号(Version)
数据表中增加一个 version 字段,每次更新时 version + 1 读取:SELECT id, qty, version FROM stock WHERE item_id = 100 → 得到 qty=100, version=5 更新:UPDATE stock SET qty=90, version=6 WHERE item_id = 100 AND version = 5 → 如果影响行数 = 1 → 更新成功 → 如果影响行数 = 0 → 数据已被修改,更新失败

时序分析:

时刻T1:线程A 读取 → version=5, qty=100 时刻T2:线程B 读取 → version=5, qty=100 时刻T3:线程A 更新 → SET qty=90, version=6 WHERE version=5 → 成功(影响1行) 时刻T4:线程B 更新 → SET qty=80, version=6 WHERE version=5 → 失败(影响0行,version已经是6) → 线程B 知道数据被修改过,可以重试或报错
方式2:时间戳(Timestamp)
-- 用更新时间代替版本号UPDATEstockSETqty=90,update_time=NOW()WHEREitem_id=100ANDupdate_time='2026-08-05 10:00:00';

缺点:时间精度问题,并发极高时可能多个操作在同一毫秒。

方式3:CAS(Compare And Swap)
-- 用旧值本身作为条件UPDATEstockSETqty=90WHEREitem_id=100ANDqty=100;-- 只有当 qty 仍然是我读到的 100 时才更新

3.3 为什么叫"乐观"?

因为它乐观地认为"冲突不会经常发生",所以不加锁,让大家都能读。只在提交时才检查。如果真的冲突了(概率低),再处理。


四、底层原理对比

4.1 悲观锁的执行过程(数据库层面)

┌─────────────────────────────────────────────────────┐ │ 事务A:SELECT * FROM stock WHERE id=1 FOR UPDATE │ │ │ │ 1. InnoDB 在内存中找到 id=1 的行 │ │ 2. 检查该行是否已有排他锁 → 没有 │ │ 3. 在该行加上排他锁(X Lock),记录锁持有者=事务A │ │ 4. 返回查询结果给事务A │ │ │ │ 此时另一个事务B执行 FOR UPDATE 同一行: │ │ 1. InnoDB 找到 id=1 的行 │ │ 2. 检查该行是否已有排他锁 → 有(事务A持有) │ │ 3. 将事务B放入该行的等待队列 │ │ 4. 事务B线程挂起(阻塞) │ │ │ │ 事务A COMMIT: │ │ 1. 释放 id=1 行上的排他锁 │ │ 2. 唤醒等待队列中的事务B │ │ 3. 事务B获得锁,继续执行 │ └─────────────────────────────────────────────────────┘

4.2 乐观锁的执行过程(应用层面)

┌─────────────────────────────────────────────────────┐ │ 线程A:SELECT id, qty, version FROM stock WHERE id=1 │ │ → 结果:qty=100, version=5 │ │ → 不加任何锁!其他线程可以自由读写 │ │ │ │ 线程A处理业务逻辑(计算新数量等)... │ │ │ │ 线程A:UPDATE stock SET qty=90, version=6 │ │ WHERE id=1 AND version=5 │ │ │ │ 数据库执行 UPDATE: │ │ 1. 找到 id=1 的行 │ │ 2. 检查 WHERE 条件:version=5 → 匹配! │ │ 3. 执行更新:qty=90, version=6 │ │ 4. 返回 affected_rows = 1 │ │ │ │ 应用层检查:affected_rows > 0 → 更新成功 │ └─────────────────────────────────────────────────────┘ 如果被其他线程修改过: ┌─────────────────────────────────────────────────────┐ │ 线程B:UPDATE stock SET qty=80, version=6 │ │ WHERE id=1 AND version=5 │ │ │ │ 数据库执行 UPDATE: │ │ 1. 找到 id=1 的行 │ │ 2. 检查 WHERE 条件:version=5 → 不匹配!当前是6 │ │ 3. 不执行更新 │ │ 4. 返回 affected_rows = 0 │ │ │ │ 应用层检查:affected_rows == 0 → 更新失败,冲突! │ │ → 重新读取最新数据,再次尝试(或直接报错) │ └─────────────────────────────────────────────────────┘

五、开发中常用的实现方式

5.1 JPA 乐观锁(@Version)

/** * 实体基类,包含乐观锁版本号. */@MappedSuperclasspublicabstractclassBaseEntity{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateIntegerid;@Version// JPA 乐观锁核心注解privateIntegerversion;privateDatecreateTime;privateDateupdateTime;}/** * 库存实体. */@Entity@Table(name="stock")publicclassStockextendsBaseEntity{privateIntegeritemSkuId;privateIntegerqty;privateIntegermemberId;// getter/setter...}

JPA @Version 的底层行为:

// 当执行 repository.save(stock) 时,JPA 自动生成的 SQL:// UPDATE stock SET qty=?, version=version+1 WHERE id=? AND version=?// ↑ 带上当前version作为条件// 如果 WHERE 条件不匹配(version已变)→ 影响行数为0// → JPA 抛出 javax.persistence.OptimisticLockException// → Spring 包装为 org.springframework.orm.ObjectOptimisticLockingFailureException

5.2 JPA 悲观锁(@Lock)

@RepositorypublicinterfaceStockRepositoryextendsJpaRepository<Stock,Integer>{/** * 悲观锁查询:SELECT ... FOR UPDATE. */@Lock(LockModeType.PESSIMISTIC_WRITE)@Query("SELECT s FROM Stock s WHERE s.itemSkuId = :itemSkuId AND s.memberId = :memberId")StockfindForUpdate(@Param("itemSkuId")IntegeritemSkuId,@Param("memberId")IntegermemberId);/** * 共享锁查询:SELECT ... LOCK IN SHARE MODE. */@Lock(LockModeType.PESSIMISTIC_READ)@Query("SELECT s FROM Stock s WHERE s.id = :id")StockfindForShare(@Param("id")Integerid);}

5.3 MyBatis 悲观锁

<!-- mapper.xml --><selectid="selectForUpdate"resultType="Stock">SELECT * FROM stock WHERE item_sku_id = #{itemSkuId} AND member_id = #{memberId} FOR UPDATE</select><updateid="deductStock">UPDATE stock SET qty = qty - #{qty} WHERE item_sku_id = #{itemSkuId} AND member_id = #{memberId} AND qty >= #{qty}</update>

5.4 MyBatis 乐观锁

<!-- mapper.xml --><updateid="deductStockWithVersion">UPDATE stock SET qty = qty - #{qty}, version = version + 1 WHERE item_sku_id = #{itemSkuId} AND member_id = #{memberId} AND version = #{version} AND qty >= #{qty}</update>// Java 代码 int affected = stockMapper.deductStockWithVersion(itemSkuId, memberId, qty, currentVersion); if (affected == 0) { throw new OptimisticLockException("库存已被修改,请刷新重试"); }

六、完整业务示例代码

6.1 乐观锁 + 重试机制

@ServicepublicclassStockService{@ResourceprivateStockRepositorystockRepository;privatestaticfinalintMAX_RETRIES=3;/** * 扣减库存(乐观锁 + 自动重试). * * 流程: * 1. 查询当前库存(不加锁) * 2. 业务校验 * 3. 尝试更新(带 version 条件) * 4. 如果 version 冲突 → 重试(最多3次) */publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){for(intattempt=1;attempt<=MAX_RETRIES;attempt++){// 1. 查询最新数据Stockstock=stockRepository.findByItemSkuIdAndMemberId(itemSkuId,memberId);if(stock==null){thrownewBusinessException("库存记录不存在");}// 2. 业务校验if(stock.getQty()<qty){thrownewBusinessException("库存不足,当前库存: "+stock.getQty());}// 3. 修改并保存(JPA 自动带 version 条件)stock.setQty(stock.getQty()-qty);try{stockRepository.saveAndFlush(stock);return;// 成功退出}catch(ObjectOptimisticLockingFailureExceptione){// 4. 乐观锁冲突if(attempt==MAX_RETRIES){thrownewBusinessException("操作冲突,请刷新后重试");}log.warn("乐观锁冲突,第{}次重试, itemSkuId={}",attempt,itemSkuId);// 短暂等待后重试sleep(50*attempt);}}}privatevoidsleep(longmillis){try{Thread.sleep(millis);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}

6.2 悲观锁实现

@ServicepublicclassStockPessimisticService{@ResourceprivateStockRepositorystockRepository;/** * 扣减库存(悲观锁). * * 流程: * 1. FOR UPDATE 锁定行(其他事务在此阻塞等待) * 2. 业务校验 * 3. 直接更新(无需担心并发冲突) * 4. 事务提交后自动释放锁 * * 注意:必须在事务内使用!锁的生命周期 = 事务的生命周期 */@Transactional(rollbackFor=Exception.class)publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){// 1. 悲观锁查询(SELECT FOR UPDATE)// 其他事务对同一行的 FOR UPDATE 会阻塞在这里Stockstock=stockRepository.findForUpdate(itemSkuId,memberId);if(stock==null){thrownewBusinessException("库存记录不存在");}// 2. 业务校验(此时确定没有其他线程在操作这行数据)if(stock.getQty()<qty){thrownewBusinessException("库存不足,当前库存: "+stock.getQty());}// 3. 直接更新(无需乐观锁检查,因为已经加了排他锁)stock.setQty(stock.getQty()-qty);stockRepository.save(stock);// 4. 事务提交时自动释放行锁}}

6.3 悲观锁 + 超时控制

@ServicepublicclassStockPessimisticWithTimeoutService{@ResourceprivateStockRepositorystockRepository;/** * 悲观锁 + 锁等待超时. * 避免长时间阻塞(如前一个事务执行很慢). */@Transactional(rollbackFor=Exception.class,timeout=10)// 事务超时10秒publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){try{Stockstock=stockRepository.findForUpdate(itemSkuId,memberId);if(stock==null){thrownewBusinessException("库存记录不存在");}if(stock.getQty()<qty){thrownewBusinessException("库存不足");}stock.setQty(stock.getQty()-qty);stockRepository.save(stock);}catch(PessimisticLockingFailureExceptione){// 获取锁超时thrownewBusinessException("系统繁忙,请稍后重试");}}}

6.4 CAS 方式乐观锁(无 version 字段)

@RepositorypublicinterfaceStockRepositoryextendsJpaRepository<Stock,Integer>{/** * CAS 式更新:用旧值作为条件. * 不依赖 version 字段,直接用业务字段做条件. */@Modifying@Query("UPDATE Stock s SET s.qty = s.qty - :deductQty "+"WHERE s.itemSkuId = :itemSkuId AND s.memberId = :memberId AND s.qty >= :deductQty")intdeductByCondition(@Param("itemSkuId")IntegeritemSkuId,@Param("memberId")IntegermemberId,@Param("deductQty")IntegerdeductQty);}@ServicepublicclassStockCasService{@ResourceprivateStockRepositorystockRepository;/** * CAS 扣减库存(一条SQL搞定,不需要先查后改). * SQL 本身保证原子性:qty >= deductQty 是条件判断 + 更新 一步完成. */@Transactional(rollbackFor=Exception.class)publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){intaffected=stockRepository.deductByCondition(itemSkuId,memberId,qty);if(affected==0){thrownewBusinessException("库存不足或数据冲突");}}}

七、如何选择

场景推荐方案原因
读多写少(如商品详情)乐观锁冲突概率低,不加锁性能高
写多读少(如秒杀库存)悲观锁 或 Redis 分布式锁冲突频繁,乐观锁重试太多
并发度不高(如后台管理)乐观锁简单可靠
单表单行高并发悲观锁(FOR UPDATE)数据库行锁效率高
跨服务/跨表并发分布式锁(Redis)超出数据库锁的范围
金融/资金操作悲观锁 + 重试绝对不能出错
批量处理乐观锁 + 重试避免大量行被锁住

八、关键设计总结

维度乐观锁悲观锁
锁实现应用层(version/CAS)数据库层(行锁)
并发性能高(无锁竞争)低(有阻塞等待)
冲突代价高(重试/报错)低(等待即可)
死锁风险有(需要注意加锁顺序)
适用场景低冲突高冲突
代码复杂度中(需处理重试)低(加锁后直接操作)
事务时长影响无影响事务越长锁持有越久
索引要求无特殊要求WHERE 必须命中索引

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询