导读:本期聚焦于小白龙创作的《SQL并发更新导致数据被覆盖怎么办?乐观锁与悲观锁实战对比》,敬请观看详情。两个用户同时修改同一条订单记录,后提交的数据把先提交的覆盖掉了,这是并发更新中最常见的丢失更新问题。本文从问题成因入手,分析数据库事务隔离级别对并发的影响,重点讲解乐观锁与悲观锁两种主流解决方案。乐观锁通过版本号或时间戳机制在更新时校验数据是否被篡改,适合读多写少场景;悲观锁则利用SELECT FOR UPDATE直接锁住行,适合写冲突频繁的业务。文中给出MySQL和PostgreSQL的完整SQL示例,对比两种方案的适用场景、性能差异和常见踩坑点,帮助你在库存扣减、订单状态流转等实际业务中选对并发控制策略。

并发更新冲突是数据库开发里绕不开的坑。典型场景是:库存表只剩一件商品,两个用户同时下单,两个事务都读到库存为1,各自判断够扣减,然后分别执行UPDATE,最终库存变成负数。又或者后台管理系统中,运营A和运营B同时编辑同一条商品信息,A先保存,B后保存,A修改的字段被B的旧数据悄悄覆盖,这种问题叫丢失更新,等发现时往往已经造成实际损失。本文就来系统地聊聊这类问题怎么处理。

SQL并发更新导致数据被覆盖怎么办?乐观锁与悲观锁实战对比

为什么会发生并发更新冲突

要理解冲突的根源,得先看数据库的隔离机制。在MySQL默认的REPEATABLE READ隔离级别下,事务读取到的数据是一致性快照。也就是说,事务A在9点开启时读到的库存是10,那么即使其他事务在这期间把它改成5,A在这个事务里读到的依然是10。快照读保证了读的一致性,但也意味着你基于旧数据做的判断,在真正提交时可能已经不成立了。

更关键的是,普通的UPDATE语句本身不会校验数据有没有被别人改过。你执行UPDATE product SET stock = stock - 1 WHERE id = 1时,数据库只会机械地执行减一操作,它不知道你在应用层曾经读过stock=10并基于这个值做过业务判断。所以如果应用层的逻辑是先SELECT读出来,在代码里判断,再UPDATE写回一个计算后的值,中间这一段时间窗口就是冲突的温床。

还有一种容易被忽视的情况是字段级覆盖。比如A修改了商品的标题,B修改了商品的价格,两个人各自提交了一个包含全部字段的UPDATE语句,结果B把A改的标题又改回旧值了。这不是数据库层面的并发问题,而是应用层的更新粒度太粗导致的,但同样属于并发更新冲突的范畴,解决思路也离不开下面要讲的锁机制。

方案一:用乐观锁避免丢失更新

乐观锁的核心假设是:冲突发生的概率不高,所以不在读取时加锁,而是在更新提交的那一刻校验数据有没有被别人动过。最常用的实现是版本号机制:表里加一个version字段,每次更新时版本号加一,同时UPDATE语句的WHERE条件里带上读取时的版本号。如果数据在这期间被别的事务改过,版本号对不上,这条UPDATE会影响0行,应用层据此感知到冲突并做重试或报错。

首先给表加上版本字段:

ALTER TABLE product ADD COLUMN version INT NOT NULL DEFAULT 0;

读取和更新的完整流程如下:

-- 第一步:读取数据,记下当前版本号
SELECT stock, version FROM product WHERE id = 1;
-- 假设读到 stock = 10, version = 3

-- 第二步:更新时校验版本号,只有版本没变才允许更新
UPDATE product
SET stock = 9,
    version = version + 1
WHERE id = 1
  AND version = 3;

-- 如果上面这条语句影响行数为 0,说明数据已被其他事务修改,
-- 应用层需要重新读取数据并重试整个流程

也可以用时间戳代替版本号,原理完全一样,但版本号是纯整数比较,性能更好,而且不怕时钟回拨,实践中更推荐版本号方案。如果不想加字段,某些场景下可以用业务字段本身做校验,比如UPDATE product SET stock = 9 WHERE id = 1 AND stock = 10,这其实是条件更新的一种简化形式,适合字段本身就能判断新旧的场景。

乐观锁的优点很明显:数据库层面没有真正的锁开销,读操作完全不受影响,吞吐量高。缺点是冲突频繁时应用层要不断重试,反而降低性能,所以它适合读多写少、冲突概率低的业务,比如用户资料编辑、后台内容管理等。写重试逻辑时要注意设置最大重试次数,避免在高并发下打转。另外,扣库存这种热点行场景,如果用乐观锁配合重试,失败率可能高得离谱,这时就该考虑悲观锁了。

方案二:用悲观锁直接锁住数据行

悲观锁的思路相反:它假设冲突一定会发生,所以读取数据时就直接加锁,别的事务想读这行数据的锁定版本都得排队等着。在MySQL和PostgreSQL中,标准做法是在SELECT语句后面加上FOR UPDATE,这样读到的行会被加上排他锁,直到事务提交或回滚才释放。

-- 事务一:锁定要修改的行
BEGIN;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
-- 这一行被当前事务独占,其他事务的 FOR UPDATE 查询会阻塞等待

-- 在应用层判断库存是否充足
UPDATE product SET stock = stock - 1 WHERE id = 1;

COMMIT;

使用FOR UPDATE有几个必须注意的点。第一,它必须在事务中才生效,如果没有显式开启事务,单条语句的锁随语句结束就释放了,起不到保护作用。第二,WHERE条件尽量命中主键或唯一索引,如果走了全表扫描,在InnoDB下可能把扫描过的行都锁住,甚至升级为锁表,严重拖垮并发性能。第三,锁的粒度要控制好,只锁真正要修改的行,不要在一个大事务里锁定大量数据长时间不放。

悲观锁适合写冲突频繁的场景,典型例子就是秒杀扣库存。因为冲突概率高,乐观锁的重试成本反而更大,直接锁行排队虽然牺牲了一点吞吐,但逻辑简单清晰,不会有失败重试的复杂度。需要注意的是,等待锁的事务如果长时间拿不到锁会超时报错,所以要合理设置锁等待超时时间,MySQL里可以通过innodb_lock_wait_timeout参数调整,默认50秒,线上业务一般建议调低一些,快速失败快速重试比长时间阻塞更健康。

两种方案怎么选以及其他补充手段

选型的核心依据是冲突概率和性能要求的权衡。简单总结:读多写少、冲突概率低,用乐观锁;写多冲突频繁、数据一致性要求极高,用悲观锁。下面这个表格可以帮你快速判断:

对比维度乐观锁悲观锁
实现方式版本号或条件更新,无真实锁SELECT FOR UPDATE 加排他锁
读性能不受影响锁定读会阻塞其他锁定读
写冲突处理失败后应用层重试数据库排队等待
适用场景后台编辑、用户资料等低冲突业务库存扣减、余额支付等高冲突业务
主要风险高并发下重试风暴锁等待超时、死锁

除了这两种主流方案,还有一些补充手段值得一提。其一是原子更新,也就是把业务判断完全下推到SQL里,让数据库单条语句完成读和写的原子操作,比如UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock >= 1,通过影响行数判断是否成功,这其实是数据库内部的行锁在起作用,天然避免了应用层的竞态窗口,特别适合简单的增减类操作。其二是提高事务隔离级别到SERIALIZABLE,让数据库自动把读操作加共享锁,理论上能杜绝丢失更新,但并发性能损失太大,一般不推荐在互联网业务中使用。其三是热点数据缓冲方案,比如秒杀场景把库存放到Redis里用原子操作扣减,异步落库,从根本上绕开了单行热点锁竞争。

最后提醒一点,悲观锁使用中要警惕死锁。两个事务分别以不同顺序锁定两行数据就可能互相等待。虽然InnoDB有死锁检测机制会主动回滚其中一个事务,但应用层最好保证加锁顺序一致,比如统一按主键升序加锁,从源头减少死锁发生的概率。乐观锁则要注意重试的幂等性,重试前必须重新读取最新数据再计算,而不是拿旧值反复提交。掌握了这些原则,绝大多数并发更新冲突问题都能妥善解决。

SQL并发更新乐观锁悲观锁修改时间:2026-09-06 05:10:37

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51359.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。