1. 先分清矛盾类型:编辑撞编辑、导出撞编辑、导出撞导出
很多人一想到"两个用户同时操作订单,后端怎么办",第一反应就是加锁。但锁是分类型的,盲目加锁只会让系统从"偶发冲突"变成"日常卡死"。我当年在处理这类问题时,第一个动作不是写代码,而是把并发场景拆开,确认到底是谁在跟谁抢。
1.1 写-写冲突:两个用户同时编辑同一张订单
这是最典型的一种冲突。用户A和用户B都打开了同一个订单详情页,页面上显示的都是"金额=100、状态=待审核"。A修改了金额为120并提交,B修改了备注"加急处理"并提交。
从数据库层面看,如果没有任何保护,B的提交会把A的修改覆盖掉。因为B的会话里没有A这次修改的信息,B执行的是一条普普通通的 UPDATE order_info SET remark = '加急处理' WHERE id = 12345,这条语句根本不关心金额字段当前是什么值。最后订单变成了"金额=100、备注=加急处理",A的金额修改彻底丢了。
这就是经典的"丢失更新"问题。它本质上是两个读改写操作交错执行导致的,和时间顺序无关,哪怕A和B的提交只差1毫秒,也一样可能发生。
1.2 读-写冲突:一个在编辑,一个在导出
编辑和导出的冲突稍微隐蔽一点。编辑不会直接破坏导出文件,但会让导出结果"不可信"。
举个例子:财务在10:00:00开始导出当天所有订单,导出程序第一次查询拿到了订单A的金额=100,然后继续查其他订单。就在这个过程中,客服在10:00:05修改了订单A的金额为120并提交。导出程序第二次查询订单B时并不受影响,但如果导出逻辑里还有一条关联查询的SQL,比如查订单A的明细,这时拿到的数据可能就是新的。于是导出的报表里,订单A的主表金额可能是100,明细表数据可能是修改后的,整个文件逻辑上是对不上的。
更严重的情况是,导出程序用了 SELECT ... WHERE status = '待审核' 这一类条件,而编辑操作刚好把某个订单的状态从"待审核"改成了"已审核"。那导出中途和导出结束时的结果集本身就不同,用户拿到文件时会疑惑:为什么这个订单的状态和我系统里看到的不一样?
1.3 读-读冲突:两个用户同时导出
读-读其实是天然并发的,两个用户同时导出同一个订单不会产生数据损坏。但如果导出这个操作本身带着"副产物"——比如导出后有更新订单状态的行为,比如"标记为已导出",那它就不再是纯粹的读操作,而是变成了一个"读-改-写"过程,需要按写冲突来处理。
我遇到过这样的业务:需求文档写着"导出后把订单标记为已导出,防止重复导出"。第一次上线时没多想,结果两个运营同时点导出,后一个导出的文件里根本没有订单数据,因为前一个已经把所有符合条件的订单全部标记成"已导出"了。
所以我的建议是:先按"是否有写操作"来划分,而不是按"操作名称"来划分。编辑、导出后改状态、审核、作废,这些只要有写,就都归类到并发写冲突的处理框架里去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方案选型:乐观锁、悲观锁、分布式锁各自的边界在哪
理清了冲突类型,下一步就是选方案。后端处理并发冲突,逃不开三大类:乐观锁、悲观锁、分布式锁。没有哪个方案是万能的,选错方案比不选方案更致命。
2.1 乐观锁:用版本号做CAS,适合高并发编辑
乐观锁的核心思路是"不主动锁,更新时校验版本"。业务表里加一个version字段,每次更新都把期望版本号放到WHERE条件里,只有版本号匹配才更新成功,同时版本号加1。
sql复制UPDATE order_info
SET amount = 120, version = version + 1
WHERE id = 12345 AND version = 0;
这条SQL的巧妙之处在于,数据库的行锁机制天然保证了原子性。两个用户同时执行同样的语句时,只有一个人能匹配到version=0,另一个人匹配不到,影响行数是0。程序通过判断 updateCount == 0 就能知道这次更新冲突了。
乐观锁的优点是并发性能好,不阻塞其他事务,特别适合订单编辑这种高频、短事务的操作。缺点是需要业务代码配合处理冲突——你不能假装没发生,必须返回提示让用户知道"你正在基于旧数据修改"。
2.2 悲观锁:SELECT FOR UPDATE,强一致但代价高
悲观锁的思路是"先锁住,别人别想碰"。在事务里执行 SELECT ... FOR UPDATE,数据库会给命中的行加排他锁,其他事务的读-写操作会被阻塞,直到当前事务提交或回滚。
sql复制BEGIN;
SELECT * FROM order_info WHERE id = 12345 FOR UPDATE;
-- 业务处理,修改金额、备注等
UPDATE order_info SET amount = 120 WHERE id = 12345;
COMMIT;
这个方案适合对一致性要求极高、写冲突概率很大的场景,比如库存扣减、账户余额变动。用在订单编辑上当然也行,但代价是并发能力急剧下降。想象一个热门订单同时被客服、财务、运营三个人打开操作,悲观锁会让另外两个人一直等待,如果其中一个事务因为别的原因迟迟不提交,后面的人全部卡住。
我的判断标准是:如果你们的订单编辑并发量确实很大,而且大多数编辑操作只是改个备注、改个地址,直接用悲观锁会白白牺牲吞吐量;乐观锁加友好提示已经足够了。
2.3 分布式锁:多实例部署时的重型武器
前面两种锁都建立在"单库单事务"的假设上。一旦系统拆分成多个微服务实例,或者订单库做了分库分表,JVM锁和数据库本地锁就失效了。
