ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

乐观锁与悲观锁 — 概念、原理与实践 乐观锁与悲观锁 — 概念、原理与实践一、基本概念1.1 什么是锁在并发场景下多个操作同时修改同一条数据会产生冲突。锁是一种机制用来协调并发访问保证数据一致性。1.2 核心区别维度乐观锁悲观锁哲学“先干活提交时再检查有没有冲突”“先占坑干活期间别人不许动”加锁时机不加锁更新时检查读取时就加锁冲突假设假设冲突很少发生假设冲突经常发生冲突处理检测到冲突后重试或报错让其他人等待避免冲突类比自助结账扫完商品发现价格变了重新扫收银台排队一次只服务一个人1.3 生活类比乐观锁 在线编辑文档 你和同事同时编辑同一段文字 你先提交 → 成功 同事提交 → 系统提示内容已被修改请刷新后重试 悲观锁 Word 文件共享锁 你打开文件 → 文件被锁定为只读给其他人 其他人想编辑 → 提示文件已被XXX锁定请等待 你保存关闭 → 其他人才能编辑注博客https://blog.csdn.net/badao_liumang_qizhi二、悲观锁Pessimistic Locking2.1 原理“先锁后操作”— 在读取数据时就加排他锁其他事务无法读取共享锁或修改排他锁同一行数据直到当前事务提交或回滚。2.2 数据库底层实现SELECT … FOR UPDATE-- 事务ABEGIN;SELECT*FROMstockWHEREitem_id100FORUPDATE;-- 此时数据库在 item_id100 这一行加了排他锁X锁-- 事务A可以读写这行数据-- 事务B同时执行BEGIN;SELECT*FROMstockWHEREitem_id100FORUPDATE;-- 阻塞等待事务A释放锁...-- 直到事务A执行 COMMIT 或 ROLLBACK-- 事务A提交UPDATEstockSETqtyqty-1WHEREitem_id100;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 LockSELECT ... LOCK IN SHARE MODE允许多个事务同时读S 与 S 兼容排他锁X LockSELECT ... FOR UPDATE只允许一个事务操作X 与任何锁都不兼容事务A加了S锁事务B可以加S锁一起读不能加X锁不能写 事务A加了X锁事务B不能加S锁也不能加X锁不能读也不能写2.4 死锁问题-- 事务ABEGIN;SELECT*FROMstockWHEREitem_id1FORUPDATE;-- 锁住行1-- 尝试锁行2...SELECT*FROMstockWHEREitem_id2FORUPDATE;-- 等待事务B释放-- 事务B同时BEGIN;SELECT*FROMstockWHEREitem_id2FORUPDATE;-- 锁住行2-- 尝试锁行1...SELECT*FROMstockWHEREitem_id1FORUPDATE;-- 等待事务A释放-- A等BB等A → 死锁-- MySQL 检测到死锁后会自动杀掉一个事务回滚代价小的那个预防死锁多表/多行操作时所有事务按相同顺序加锁。三、乐观锁Optimistic Locking3.1 原理“不加锁提交时检查”— 读取数据时不加锁更新时检查数据是否被其他人修改过。如果被修改过则拒绝本次更新。3.2 实现方式方式1版本号Version数据表中增加一个 version 字段每次更新时 version 1 读取SELECT id, qty, version FROM stock WHERE item_id 100 → 得到 qty100, version5 更新UPDATE stock SET qty90, version6 WHERE item_id 100 AND version 5 → 如果影响行数 1 → 更新成功 → 如果影响行数 0 → 数据已被修改更新失败时序分析时刻T1线程A 读取 → version5, qty100 时刻T2线程B 读取 → version5, qty100 时刻T3线程A 更新 → SET qty90, version6 WHERE version5 → 成功影响1行 时刻T4线程B 更新 → SET qty80, version6 WHERE version5 → 失败影响0行version已经是6 → 线程B 知道数据被修改过可以重试或报错方式2时间戳Timestamp-- 用更新时间代替版本号UPDATEstockSETqty90,update_timeNOW()WHEREitem_id100ANDupdate_time2026-08-05 10:00:00;缺点时间精度问题并发极高时可能多个操作在同一毫秒。方式3CASCompare And Swap-- 用旧值本身作为条件UPDATEstockSETqty90WHEREitem_id100ANDqty100;-- 只有当 qty 仍然是我读到的 100 时才更新3.3 为什么叫乐观因为它乐观地认为冲突不会经常发生所以不加锁让大家都能读。只在提交时才检查。如果真的冲突了概率低再处理。四、底层原理对比4.1 悲观锁的执行过程数据库层面┌─────────────────────────────────────────────────────┐ │ 事务ASELECT * FROM stock WHERE id1 FOR UPDATE │ │ │ │ 1. InnoDB 在内存中找到 id1 的行 │ │ 2. 检查该行是否已有排他锁 → 没有 │ │ 3. 在该行加上排他锁X Lock记录锁持有者事务A │ │ 4. 返回查询结果给事务A │ │ │ │ 此时另一个事务B执行 FOR UPDATE 同一行 │ │ 1. InnoDB 找到 id1 的行 │ │ 2. 检查该行是否已有排他锁 → 有事务A持有 │ │ 3. 将事务B放入该行的等待队列 │ │ 4. 事务B线程挂起阻塞 │ │ │ │ 事务A COMMIT │ │ 1. 释放 id1 行上的排他锁 │ │ 2. 唤醒等待队列中的事务B │ │ 3. 事务B获得锁继续执行 │ └─────────────────────────────────────────────────────┘4.2 乐观锁的执行过程应用层面┌─────────────────────────────────────────────────────┐ │ 线程ASELECT id, qty, version FROM stock WHERE id1 │ │ → 结果qty100, version5 │ │ → 不加任何锁其他线程可以自由读写 │ │ │ │ 线程A处理业务逻辑计算新数量等... │ │ │ │ 线程AUPDATE stock SET qty90, version6 │ │ WHERE id1 AND version5 │ │ │ │ 数据库执行 UPDATE │ │ 1. 找到 id1 的行 │ │ 2. 检查 WHERE 条件version5 → 匹配 │ │ 3. 执行更新qty90, version6 │ │ 4. 返回 affected_rows 1 │ │ │ │ 应用层检查affected_rows 0 → 更新成功 │ └─────────────────────────────────────────────────────┘ 如果被其他线程修改过 ┌─────────────────────────────────────────────────────┐ │ 线程BUPDATE stock SET qty80, version6 │ │ WHERE id1 AND version5 │ │ │ │ 数据库执行 UPDATE │ │ 1. 找到 id1 的行 │ │ 2. 检查 WHERE 条件version5 → 不匹配当前是6 │ │ 3. 不执行更新 │ │ 4. 返回 affected_rows 0 │ │ │ │ 应用层检查affected_rows 0 → 更新失败冲突 │ │ → 重新读取最新数据再次尝试或直接报错 │ └─────────────────────────────────────────────────────┘五、开发中常用的实现方式5.1 JPA 乐观锁Version/** * 实体基类包含乐观锁版本号. */MappedSuperclasspublicabstractclassBaseEntity{IdGeneratedValue(strategyGenerationType.IDENTITY)privateIntegerid;Version// JPA 乐观锁核心注解privateIntegerversion;privateDatecreateTime;privateDateupdateTime;}/** * 库存实体. */EntityTable(namestock)publicclassStockextendsBaseEntity{privateIntegeritemSkuId;privateIntegerqty;privateIntegermemberId;// getter/setter...}JPA Version 的底层行为// 当执行 repository.save(stock) 时JPA 自动生成的 SQL// UPDATE stock SET qty?, versionversion1 WHERE id? AND version?// ↑ 带上当前version作为条件// 如果 WHERE 条件不匹配version已变→ 影响行数为0// → JPA 抛出 javax.persistence.OptimisticLockException// → Spring 包装为 org.springframework.orm.ObjectOptimisticLockingFailureException5.2 JPA 悲观锁LockRepositorypublicinterfaceStockRepositoryextendsJpaRepositoryStock,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 --selectidselectForUpdateresultTypeStockSELECT * FROM stock WHERE item_sku_id #{itemSkuId} AND member_id #{memberId} FOR UPDATE/selectupdateiddeductStockUPDATE stock SET qty qty - #{qty} WHERE item_sku_id #{itemSkuId} AND member_id #{memberId} AND qty #{qty}/update5.4 MyBatis 乐观锁!-- mapper.xml --updateiddeductStockWithVersionUPDATE 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_RETRIES3;/** * 扣减库存乐观锁 自动重试. * * 流程 * 1. 查询当前库存不加锁 * 2. 业务校验 * 3. 尝试更新带 version 条件 * 4. 如果 version 冲突 → 重试最多3次 */publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){for(intattempt1;attemptMAX_RETRIES;attempt){// 1. 查询最新数据StockstockstockRepository.findByItemSkuIdAndMemberId(itemSkuId,memberId);if(stocknull){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(attemptMAX_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(rollbackForException.class)publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){// 1. 悲观锁查询SELECT FOR UPDATE// 其他事务对同一行的 FOR UPDATE 会阻塞在这里StockstockstockRepository.findForUpdate(itemSkuId,memberId);if(stocknull){thrownewBusinessException(库存记录不存在);}// 2. 业务校验此时确定没有其他线程在操作这行数据if(stock.getQty()qty){thrownewBusinessException(库存不足当前库存: stock.getQty());}// 3. 直接更新无需乐观锁检查因为已经加了排他锁stock.setQty(stock.getQty()-qty);stockRepository.save(stock);// 4. 事务提交时自动释放行锁}}6.3 悲观锁 超时控制ServicepublicclassStockPessimisticWithTimeoutService{ResourceprivateStockRepositorystockRepository;/** * 悲观锁 锁等待超时. * 避免长时间阻塞如前一个事务执行很慢. */Transactional(rollbackForException.class,timeout10)// 事务超时10秒publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){try{StockstockstockRepository.findForUpdate(itemSkuId,memberId);if(stocknull){thrownewBusinessException(库存记录不存在);}if(stock.getQty()qty){thrownewBusinessException(库存不足);}stock.setQty(stock.getQty()-qty);stockRepository.save(stock);}catch(PessimisticLockingFailureExceptione){// 获取锁超时thrownewBusinessException(系统繁忙请稍后重试);}}}6.4 CAS 方式乐观锁无 version 字段RepositorypublicinterfaceStockRepositoryextendsJpaRepositoryStock,Integer{/** * CAS 式更新用旧值作为条件. * 不依赖 version 字段直接用业务字段做条件. */ModifyingQuery(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(rollbackForException.class)publicvoiddeductStock(IntegeritemSkuId,IntegermemberId,Integerqty){intaffectedstockRepository.deductByCondition(itemSkuId,memberId,qty);if(affected0){thrownewBusinessException(库存不足或数据冲突);}}}七、如何选择场景推荐方案原因读多写少如商品详情乐观锁冲突概率低不加锁性能高写多读少如秒杀库存悲观锁 或 Redis 分布式锁冲突频繁乐观锁重试太多并发度不高如后台管理乐观锁简单可靠单表单行高并发悲观锁FOR UPDATE数据库行锁效率高跨服务/跨表并发分布式锁Redis超出数据库锁的范围金融/资金操作悲观锁 重试绝对不能出错批量处理乐观锁 重试避免大量行被锁住八、关键设计总结维度乐观锁悲观锁锁实现应用层version/CAS数据库层行锁并发性能高无锁竞争低有阻塞等待冲突代价高重试/报错低等待即可死锁风险无有需要注意加锁顺序适用场景低冲突高冲突代码复杂度中需处理重试低加锁后直接操作事务时长影响无影响事务越长锁持有越久索引要求无特殊要求WHERE 必须命中索引
返回列表