乐观锁与悲观锁:从原理到选型,并发控制实战指南
1. 并发控制第一课先分清两种锁的底层思路做后端开发这些年只要涉及多线程或高并发基本绕不开“锁”这个话题。面试中问烂了的乐观锁与悲观锁真正在工作中能讲透、选对、用对的人其实并不多。原因很简单大多数人背了概念却没有建立“锁的本质是牺牲某种资源换取数据一致性”的底层认知遇到具体业务时自然不知道选哪把锁。先给个直接的定义。悲观锁的逻辑是我笃定并发冲突一定会发生所以在操作数据之前干脆先把资源锁住别人想碰就得等我用完。乐观锁的逻辑相反我默认大部分时候没人跟我抢所以我不加锁只在最后提交修改时检查一下——如果期间数据被别人动过那我就放弃本次修改或者重试。这两种思路没有绝对的优劣它们是两种完全不同的并发控制策略背后对应着不同的性能开销、实现复杂度、适用场景。我见过不少团队在并发量不算高的系统里强行上乐观锁结果冲突重试率居高不下反而拖垮了接口响应也见过在低频管理后台里用悲观锁锁行锁等待把简单操作变得又慢又堵。选锁本质上是一道权衡题核心就两个问题冲突概率高不高冲突后的代价大不大如果冲突概率很低选乐观锁——不需要加锁天然没有死锁、锁等待、上下文切换的代价提交时发现冲突重试几次就成功了。如果冲突概率很高或者冲突后的代价无法承受比如金融转账、库存扣减选悲观锁——哪怕牺牲一点吞吐量也要保证操作一次成功而不是反复失败重试。下面我会把两种锁的实现方式、适用业务类型、代码示例、踩坑经验全部拆开来讲。这篇文章不会只停留在概念层面所有示例代码都是可直接落地的写法看完你能直接拿去做技术选型和方案评审。2. 悲观锁用串行化换取强一致性2.1 悲观锁的本质与实现方式悲观锁的核心思想就是“先拿锁再办事”。它假设并发冲突是常态所以在真正执行业务逻辑之前先把目标资源锁住让其他线程或事务排队等待。在关系型数据库中悲观锁最典型的实现就是SELECT ... FOR UPDATE。这条 SQL 的含义是在查询结果集的同时对命中的行记录加排他锁。事务 A 锁住了这几行事务 B 再来执行同样的查询加锁操作时就会阻塞直到事务 A 提交或回滚释放锁。这种锁数据库引擎本身就能保证互斥性不用我们在应用层做额外的判断。除了数据库层面的行锁悲观锁在应用层也有实现方式比如 Java 里的synchronized、ReentrantLock以及分布式场景下的 Redis 分布式锁。核心逻辑都一样多个执行单元竞争同一把锁谁拿到谁执行其他人阻塞等待。悲观锁的优点非常突出数据一致性由锁机制强保证实现简单业务代码里只需要包一层事务、加一行 SQL不需要设计冲突检测和重试策略。缺点也很明显并发能力受限于锁的粒度锁等待会拖慢响应时间并且可能出现死锁——两个事务各自持有一把锁又在等对方释放另一把锁最终谁也无法推进只能靠数据库超时机制来破局但这个“破局”往往意味着其中一个事务被强制回滚。还有一个容易被忽略的开销锁的持有期间如果业务逻辑里有远程调用、外部接口请求、网络 IO 等耗时操作这把锁的持有时间会被拉得很长整个链路的吞吐量都会断崖式下降。后面我会专门说这个问题的优化思路。2.2 什么业务必须用悲观锁悲观锁不是万能的但在某些业务场景下是唯一稳妥的选择。我总结了三类必须优先考虑悲观锁的情况。第一类是金融级强一致性场景。比如账户余额扣减、转账、积分扣减这些操作不允许出现任何超扣、错账。乐观锁的冲突重试机制在最坏情况下会出现多次重试都失败甚至需要业务方决定“是否放弃本次操作”——这个“放弃”在金融场景里是无法接受的。使用悲观锁事务一旦获取锁本次操作要么成功提交要么异常回滚没有中间态语义清晰便于对账。第二类是热点资源的高冲突场景。典型如秒杀系统的库存扣减、火车票余票扣减。这类场景里同一个 SKU 或同一条余票记录在瞬间会被海量请求命中。如果使用乐观锁极端情况下大部分请求都会因版本号冲突而失败随之而来的大量重试会进一步放大数据库压力可能直接把数据库打挂。相比之下悲观锁把并发请求变成串行队列数据库行锁本身有队列管理机制反而更可控。第三类是操作本身就具备“独占性”的业务。比如任务的领取与执行、订单的状态流转、文件的编辑与上传。这些业务有一个共同特征同一时间只允许一个执行者操作第二个执行者要么等待要么直接拒绝。比如任务调度系统里同一批次任务不能被两台机器同时执行这时候用分布式锁悲观锁的一种形态做互斥是最直观的模型。2.3 悲观锁实战示例库存扣减接口这里我给出一个最典型的库存扣减示例方便对比后面乐观锁的实现。下面是用 SQL 实现的悲观锁扣减库存代码。-- 开启事务 BEGIN; -- 对库存记录加排他锁 SELECT stock FROM product WHERE product_id #{productId} FOR UPDATE; -- 在应用层判断库存是否充足这里假设查询结果为 stock -- 若 stock 需要扣减的数量则抛异常回滚 -- 执行扣减 UPDATE product SET stock stock - #{count} WHERE product_id #{productId}; -- 提交事务释放锁 COMMIT;这里最关键的一行是SELECT ... FOR UPDATE。执行完这行之后事务就持有了该商品库存记录的排他锁。此后其他事务再对这个商品执行SELECT ... FOR UPDATE或UPDATE都会阻塞直到本事务提交。有人可能会问为什么不直接执行UPDATE product SET stock stock - 1 WHERE product_id ? AND stock 0用受影响行数来判断是否超卖这确实是一种常见的免锁方案但它有一个前提条件库存扣减只是简单的数值更新并且不需要读取库存的当前值来做后续逻辑。在复杂的业务场景里比如扣减库存的同时需要记录操作流水、检查限购数量、判断历史累计购买量就必须先把当前库存和用户购买记录查出来判断这时候UPDATE ... AND stock 0就无能为力了需要先加锁保证读取到的数据不被其他事务篡改。悲观锁在实现时还有几个细节需要注意。首先是FOR UPDATE一定要走主键或唯一索引否则 InnoDB 会退化为表锁把整个表的写操作全部锁住性能堪称灾难。其次事务一定要短小精悍不要在锁内做远程调用、发消息、写日志等耗时操作锁的持有时间直接决定系统的并发上限。最后要设置合理的锁等待超时时间MySQL 默认是 50 秒这个值在业务上往往太长了建议根据接口的预期耗时进行调优。3. 乐观锁用版本号检测冲突3.1 乐观锁的实现原理与几种常见形态乐观锁的实现不需要数据库锁机制完全在应用层完成。最经典的实现方式就是版本号控制在数据表中增加一个version字段每次更新数据时把 SQL 写成“更新前先检查版本号是否和读取时一致”如果一致则更新成功同时将版本号加 1如果不一致说明数据已经被其他人改过当前更新失败。UPDATE product SET stock stock - #{count}, version version 1 WHERE product_id #{productId} AND version #{oldVersion};这段 SQL 执行后如果返回的影响行数为 1说明更新成功如果返回 0说明version已经不匹配也就是期间有人改过这条数据需要业务层决定重试或者放弃。整个过程没有加锁、没有阻塞多个线程可以同时执行更新由数据库的行锁机制在最后一步保证原子性。除了版本号乐观锁还有两种常见形态。第一种是 CASCompare And Swap方式直接比较某个业务字段的旧值和新值比如“库存扣减前库存必须是 100”用UPDATE ... WHERE stock 100来更新但这种方式在字段会被重复修改回原值的场景下会出现 ABA 问题值从 A 变成 B 又变回 A检查时误以为没被改过。第二种是时间戳方式用update_time字段代替版本号同样存在时间精度不足导致的误判问题。在实际项目中我强烈建议使用专门的version字段而不是用业务字段或时间戳。原因很简单业务字段可能被改回原值ABA 问题时间戳精度可能不够而version字段只负责并发控制单调递增永远不回退语义清晰、无歧义。3.2 乐观锁实战示例带重试机制的库存扣减乐观锁在实际使用中光有更新 SQL 还不够通常需要配合重试机制。否则第一次冲突就直接返回失败在高并发场景下用户体验会很差。这里给出一个带重试的完整示例。Transactional public boolean deductStock(Long productId, Integer count, int maxRetry) { for (int attempt 0; attempt maxRetry; attempt) { // 第一步查询当前版本号和库存 Product product productMapper.selectById(productId); if (product.getStock() count) { throw new BizException(库存不足); } // 第二步尝试用版本号作为条件更新 int rows productMapper.deductStockWithVersion( productId, count, product.getVersion()); // 第三步根据影响行数判断是否成功 if (rows 1) { return true; } // 第四步更新失败说明有冲突短暂避让后重试 Thread.sleep(10L * (attempt 1)); } throw new BizException(系统繁忙请稍后重试); }这段代码有几点值得说明。查询和更新分离是本方案的特点也是潜在风险点。查询时拿到的version是旧值更新时拿旧version作为条件。两步之间存在时间窗口因此可能出现多个请求都读到同一个version但最终只有一个人更新成功这正是乐观锁的工作原理。重试之间的间隔使用递增退避策略避免重试请求聚集在同一时间点打过去造成瞬时压力。10ms 的基准值可以根据业务情况调整但不要让重试风暴成为新的性能问题。Transactional注解在这里的作用是保证更新操作本身的事务性但要注意由于先查后改整个事务的隔离级别会影响并发行为。在默认的 RR可重复读隔离级别下乐观锁的两次查询之间其他事务的提交不会影响当前事务的快照读取——这意味着“查”到的版本号可能是旧的但“更新”用的 SQL 是直接作用于当前行的不受快照影响所以最终一致性仍然由UPDATE ... WHERE version ?来保证。这里面的细节比较多后面我会单独讲一个深坑。重试次数要根据业务容忍度来设置。库存类操作我一般设置 3 次最多 5 次超过直接返回“系统繁忙”。如果重试太多次单个请求的耗时会被拉得很长接口平均响应时间会变得很难看。3.3 乐观锁的适用场景与边界乐观锁最适合的是并发冲突概率低但系统整体并发量又很高的场景。典型如文章点赞数、浏览量、评论数这类计数器的累加操作。大部分情况下用户点赞不会同时落到同一条数据上就算偶尔冲突重试一次就成功了完全没必要为了极低概率的冲突去加悲观锁。内容管理系统中的文档编辑也适合乐观锁。多人同时编辑同一篇文档的概率本来就低用乐观锁可以在编辑提交时检测出版本过期提示用户“文档已被他人修改”让用户决定是覆盖还是刷新后合并体验上反而更合理。但是乐观锁也有明显的边界不适合冲突率高、热点集中的场景。比如前面提到的秒杀场景同一商品瞬时千万级请求乐观锁会导致大量请求失败重试数据库的更新操作和重试查询会形成双重压力。在这种情况下悲观锁加队列比乐观锁更适用。还有一个很容易被忽略的问题乐观锁只适合单行或少量行更新的场景。如果一个业务操作要同时更新多张表、多行记录用乐观锁会让复杂度急剧上升——你需要在同一个事务里校验多张表各自的版本号并且任何一个版本号冲突都会导致整体回滚冲突概率成倍放大重试代价也难以接受。这种情况直接用悲观锁更合理。4. 两种锁的硬核对比性能、并发度、一致性全方位拆解4.1 常规认知 vs 实际情况一张表看清差异关于两种锁的对比很多人脑子里只有一句“乐观锁性能比悲观锁好”这个认知过于粗暴必须结合具体场景来分析。我整理了实际开发中最常被拿来对比的六个维度对比维度悲观锁乐观锁并发能力低锁等待导致吞吐量受限高无阻塞但冲突后需重试数据一致性强一致串行化执行最终一致依赖版本校验实现复杂度简单SQL 加锁即可较高需版本字段加重试机制死锁风险存在需设置超时与规避策略无死锁但存在活锁重试不成功冲突开销锁等待期间线程阻塞冲突后立即失败重试有额外开销适用场景写多读少、冲突率高、强一致读多写少、冲突率低、追求吞吐量这个表格里的结论和很多文章里“乐观锁性能更好”的笼统说法有很大差异。真正决定性能的不是锁本身而是冲突率和锁竞争程度。在低冲突场景下乐观锁省去了加锁解锁和线程阻塞的开销性能确实优于悲观锁但在高冲突场景下乐观锁的重试风暴会放大系统负载反而是悲观锁的串行队列更稳定。换句话说不是“乐观锁更好”而是“低冲突场景更适合乐观锁高冲突场景更适合悲观锁”。想明白这一点选型就不会出错。4.2 锁粒度对比行锁、表锁、分布式锁选择锁的时候除了乐观和悲观这个维度还有一个同样重要的维度是锁的粒度。锁粒度越细并发能力越高但管理和实现的复杂度也越高。现实中我们经常见到的锁粒度有三种行锁、表锁、分布式锁。行锁是数据库提供的最细粒度锁SELECT ... FOR UPDATE就是典型的行锁。多个事务可以同时操作不同行的数据互不干扰并发能力好也是日常开发最常用的方式。表锁则是把整张表锁住在 MyISAM 引擎中常见或者当FOR UPDATE查询条件没走索引时InnoDB 也会退化成表锁并发能力极差实际项目中要坚决避免。分布式锁是跨服务场景下的锁典型实现有 Redis 分布式锁和 ZooKeeper 分布式锁。它的本质是在多个服务实例之间共享一把互斥锁实现方式通常是用 Redis 的SETNX命令加一个过期时间或者用 ZK 的临时顺序节点。分布式锁的粒度通常是“业务资源”比如“订单号 12345 的支付锁”而不是数据库的某一行。选锁粒度时有一个基本判断单体应用优先选数据库行锁分布式系统必须选分布式锁。还有一种常见方案是数据库悲观锁 Redis 分布式锁并用前者保证数据一致性后者保证跨服务的互斥逻辑。这里必须提醒一个很多人踩过的坑分布式锁的过期时间设置。如果你设置的过期时间是 3 秒但业务执行了 5 秒那么第 3 秒时锁自动释放另一个线程拿到锁进入临界区此时两个线程同时在执行同一业务逻辑分布式锁完全失效。解决方案有几类把过期时间设置得足够长并配合续期机制比如用 Redisson 看门狗自动续期或者使用 ZK 的临时顺序节点客户端断开时锁自动释放不会出现锁过期但业务未结束的尴尬。4.3 在 Spring Boot 项目里实践两种锁如果是 Spring Boot 项目悲观锁的实现非常直接。在 Repository 层的查询方法上加Lock注解并在 Service 层加Transactional即可。Lock(LockModeType.PESSIMISTIC_WRITE) Query(SELECT p FROM Product p WHERE p.productId :productId) Product findByIdForUpdate(Param(productId) Long productId);注意Lock注解必须配合事务使用因为锁的释放依赖事务的提交。如果方法没有加Transactional锁会在查询结束后立即释放后面的更新操作就失去了保护。乐观锁在 Spring Data JPA 里可以通过Version注解实现实体类加上这个注解后框架会在更新时自动拼上version version 1的条件更新失败时抛出OptimisticLockException。这个机制非常方便但也有一些隐蔽的问题。比如 JPA 在做save()时如果传入的是一个脱管实体detached entity版本号可能没有正确更新容易产生意外行为。我之前就踩过这个坑前端传回的 DTO 里带着旧版本号后端直接做save()更新导致版本号乱跳数据被错误覆盖。如果用 MyBatis乐观锁需要手动管理 SQL 中的version条件类似于 3.2 节的示例。MyBatis 的好处是 SQL 完全可控但坏处是每次写更新语句都要记得带上version条件。团队规范必须严格要求否则漏掉一个线上就出问题。5. 业务场景选型指南到底什么时候该用哪把锁5.1 高频业务场景的分类与匹配结合我自己的项目经验把常见的业务场景分成五类每类都给出明确的锁选型建议。电商库存扣减是最典型的场景需要根据业务量级区分。如果库存条目多、单条被同时点击的概率低比如普通商品详情页的加购减库存可以用乐观锁如果是单品秒杀、限量抢购这种热点资源必须用悲观锁或者 Redis 预扣减加异步对账方案。很多大厂秒杀系统的实际做法是先用 Redis 在缓存层做预扣减挡住大部分流量再用数据库悲观锁保证最终一致性。账户金额操作无论是余额查询后再扣减还是转账、提现、退款都强烈建议悲观锁。金额相关的数据不能用“重试”来弥补错误一个失败的转账请求如果重试两次就变成转了两笔钱这是绝对不允许的。银行核心系统的账务处理基本都是数据库行锁加串行化操作就是这个原因。订单状态流转待支付 → 已支付 → 已发货 → 已完成 → 已取消属于典型的独占性操作每个状态的变更都要求幂等。推荐使用分布式锁锁的 key 可以用订单号或者直接用数据库悲观锁锁住订单行。特别注意不要用乐观锁做状态流转因为状态机流转失败后重试的语义很难表达——你不知道当前状态是什么不知道该重试还是该报错。计数类操作点赞数、浏览数、播放数、评论数属于低冲突高并发场景强烈建议乐观锁。更优的方案是直接使用 Redis 的INCR命令做原子累加然后定时批量刷入数据库连乐观锁都可以省掉。这个做法我后面会重点讲。后台管理系统的编辑保存CMS 文章、商品信息、用户资料适合乐观锁。因为多人同时编辑同一个数据是低频事件发生冲突时给用户一个“数据已被他人修改”的提示就够了。但要注意如果编辑操作内部附带大量关联更新比如商品信息编辑会同时更新价格、库存、图片、分类关系等多个版本的并发控制复杂度会比想象中高建议在开启编辑时用悲观锁锁住主表记录避免后续一连串更新出问题。5.2 库存扣减场景的深度拆解从乐观锁到分段锁库存扣减作为并发控制最经典的教学案例我多花一点篇幅展开。很多团队第一次接触乐观锁时都会拿库存扣减来试验但实际效果往往不理想原因在于对场景的冲突率没有准确评估。假设一个商品有 100 件库存100 个用户同时抢理想情况下只有一个用户能抢到其余 99 个用户的更新操作都会失败。如果使用乐观锁这 99 个请求都会进入重试逻辑每个请求都重新查询版本号、重新尝试更新数据库的读写压力会翻几倍。如果重试次数设置不当数据库甚至会被打挂。悲观锁方案则不同100 个请求到达数据库后第一个请求拿到行锁操作完释放后面 99 个排队依次执行。每个请求拿到锁后重新查库存发现库存为 0直接返回失败。整个过程数据库的读写次数很少压力可控。高并发库存场景的更优方案其实是分段锁或预减库存。分段锁的思路是把 100 件库存分成 10 段每段 10 件每段对应数据库里的一行记录扣减时随机选一段进行操作降低同一行的竞争。预减库存的思路是在 Redis 中完成扣减操作流量削峰后异步同步到数据库。两种方案都规避了单一热点的并发竞争也同时绕开了乐观锁和悲观锁各自的局限。这里想表达的观点是乐观锁和悲观锁只是并发控制的两种基本手法而不是并发问题的最终解。真正优秀的架构往往是在缓存层用原子操作挡掉大部分请求在数据库层用精确锁保证最终一致。5.3 分布式系统下如何做锁选型当项目从单体架构演进到分布式架构时数据库行锁仍然可以用于保护单行数据的一致性但“操作幂等性”和“跨服务互斥”这类需求数据库锁已经无法覆盖必须引入分布式锁。分布式锁的常见实现方案包括基于 Redis 的SET NX EX命令、基于 ZooKeeper 的临时顺序节点、基于数据库的唯一索引比如用INSERT INTO lock_table (resource_id) VALUES (...) ON DUPLICATE KEY UPDATE的方式来模拟锁。三种方案各有优劣Redis 方案性能最好但有锁过期和主从切换带来的可靠性问题ZK 方案可靠性高但性能较差对依赖 ZK 集群稳定性的要求高数据库唯一索引方案实现简单、绝对可靠但并发能力有限且拿不到锁时只能靠轮询或数据库异常来判断。在分布式的锁选型上我的建议是不需要绝对的强一致性的场景用 Redis 分布式锁必须绝对可靠、绝对不能出现两个客户端同时拿到锁的场景用 ZK 或数据库唯一索引。分布式锁之外幂等性设计同样不可忽视。用唯一订单号、唯一流水号作为数据库唯一键让重复请求直接插入失败是一种比“不断加锁”更优雅的并发控制方式。锁是控制并发的手段幂等是应对重试的手段两者配合才能构建出真正健壮的分布式业务系统。6. 实操中的坑与经验教训这些雷我替你踩过了6.1 悲观锁的隐藏开销索引失效与长事务悲观锁最常见的一个坑就是SELECT ... FOR UPDATE的查询条件没有走索引导致行锁升级为表锁。一旦升级为表锁整个表的写操作全部被阻塞数据库性能急剧恶化。排查方法很简单用EXPLAIN查看执行计划key字段如果没有使用索引就要优化查询条件或强制走主键。第二个坑是长事务。悲观锁的锁释放时机是事务提交或回滚所以事务执行时间越长锁持有时间越长并发能力越差。很多团队在事务内调用外部接口、发 MQ 消息、甚至做文件上传导致一个事务跑了好几秒甚至几十秒系统整体吞吐量直接崩盘。优化方向有两个一是尽量缩小事务边界把远程调用和消息发送移到事务外使用“本地事务 发件表 定时扫描发消息”的模式二是把锁粒度放细锁查询条件尽量定位到单行而不是一次性锁多行。还有一个细节FOR UPDATE的锁是阻塞等待模式等待时间超过innodb_lock_wait_timeout后会抛出超时异常。默认 50 秒太长了建议在配置里调低到 3~5 秒宁可让请求快速失败也不要让用户干等几十秒。6.2 乐观锁的深坑重试机制与 ABA 问题乐观锁的坑也不少最要命的就是无限重试导致的活锁。在高冲突场景下多个线程反复竞争同一行数据每个线程都失败、都重试最后都拿不到锁请求耗时无限拉长。解决办法是设置最大重试次数和退避时间超过阈值直接返回失败。第二个坑是忽略更新结果。有些同学写了UPDATE ... WHERE version ?但用rows 0时不做额外判断直接把失败当作正常返回导致调用方以为更新成功了最后数据悄悄丢失。我见过不止一次这种线上事故排查到凌晨才发现是“乐观锁更新失败被吞掉了”。强烈建议影响行数为 0 时必须抛出异常或明确标记失败让上层感知冲突。第三个坑是前面提到的 ABA 问题。版本号方式天然免疫 ABA 问题版本号只递增不回退但 CAS 方式如果比较字段值本身可能被 ABA 坑到。举例线程 A 读到余额 100 元线程 B 扣减 10 元再增加 10 元余额回到 100 元线程 A 再更新时发现余额还是 100 元CAS 判断成功但实际上余额已经被动过两次。这个坑在用时间戳或业务字段做比较时也可能发生用专门的版本号字段是规避 ABA 最简单的方法。6.3 分布式锁的常见失效问题与规避方案分布式锁的常见问题有三个锁过期、误删锁、主从切换。锁过期问题前面提过解决思路是给锁加上自动续期机制。如果用的是 Redis Redisson它的看门狗机制默认每 10 秒续期一次直到业务执行完成锁才真正释放。如果用的是自研的 Redis SETNX 方案就需要自己实现续期逻辑或者把过期时间设置成业务耗时的数倍。误删锁问题很典型线程 A 拿到锁执行期间锁过期自动释放线程 B 拿到锁开始执行此时 A 执行完毕顺手把锁给删了。由于 A 已经失去了锁的所有权它这一删等于把 B 的锁给释放了B 和 C 就会同时进入临界区。解决方案是删除锁时校验当前锁的持有标识可以用 UUID 作为 value只有 value 匹配时才执行删除。这个校验和删除动作要保证原子性必须用 Lua 脚本来实现。主从切换问题更难规避。Redis 主节点上的锁信息在主节点宕机、从节点晋升后可能丢失导致新的主节点上锁不存在多个线程同时拿到锁。严格意义上这个问题的标准解法是 RedLock 算法在多个独立 Redis 节点上同时加锁超过半数成功才算加锁成功。但 RedLock 本身的复杂性也备受争议对于大多数业务来说数据不要求极其严格的强一致Redis 分布式锁加 UUID 校验加合理过期时间已经够用。如果你是金融、支付等必须严格避免双写风险的场景建议直接上 ZK它的分布式锁在一致性上要可靠得多。6.4 实战中我认为最重要的一条经验最后分享一条我个人最有感触的经验也呼应一下开头提到的“锁的本质”。并发问题的本质是共享资源的竞争。所有锁的本质是给竞争中的一方开绿灯给其他方亮红灯以换取数据一致性。但红灯亮得太多、太久系统的并发能力就会归零。真正的高手不是比谁更会用锁而是比谁能让“灯”红得更少、更快。在实际架构里我通常按这样的顺序来解决问题能不能拆分资源消除竞争这是最快的方案。能不能让请求串行化走队列这是最稳的方案。能不能用乐观锁配合幂等重试搞定这是最轻的方案。最后才是悲观锁兜底保住强一致牺牲部分并发能力。技术选型都应该是从业务出发倒推技术方案而不是拿着锁去套业务。希望这篇文章能帮你建立起这套思考方式。如果你在实际项目中还遇到过哪些印象深刻的锁实践或者踩过我没提到的坑欢迎在评论区聊聊。你的经历很可能正好是别人在找的答案。