并发修改同一条数据如何解决

接入与开发

讲清多人同时改同一条记录时的并发控制做法,给出选型对照、步骤与排查顺序,避开覆盖写丢数据。

后台项目里常见这种场面:两个同事几乎同时点保存,后写的把先写的悄悄盖掉;或者队列重投一次,订单状态被改回去了。这篇不讲并发模型理论,只给能落地的做法:先判断你属于哪类冲突,再挑锁、版本号还是队列,最后按顺序排查。选型会写明什么情况下不适用,别硬套。

后台项目里常见这种场面:两个同事几乎同时点保存,后写的把先写的悄悄盖掉;或者队列重投一次,订单状态被改回去了。这篇不讲并发模型理论,只给能落地的做法:先判断你属于哪类冲突,再挑锁、版本号还是队列,最后按顺序排查。

选型会写明什么情况下不适用,别硬套。挑选具体实现方案前,建议先到 /tools/speed-test 这类站内工具页确认你当前用的数据库与中间件版本支持哪些能力,再到 /guides 看对应的接入指南,避免把不兼容的写法硬塞进现有代码里。

先分清你是哪种冲突,再谈方案

读改写覆盖(丢更新)和写写冲突表现不一样。前者是后提交的静默盖掉,用户毫无感知;后者常见报错或脏读,至少能被发现。先按业务影响判断哪种更痛:如果这条记录是金额、库存、状态机这类算错就麻烦的字段,静默覆盖比报错更危险,得优先防。

同一用户重复提交和多用户真并发是两回事。前者是网络重试或手抖连点,本质是同一个逻辑请求被落了两次,优先做幂等;后者是两个人真的读到同一份旧数据同时改,才需要锁或版本控制。判断错了方向,加了锁也防不住重复提交带来的数据错乱。

冲突类型典型表象优先手段
读改写覆盖后保存静默盖掉先保存版本号条件更新
写写冲突报错、脏读、状态跳变行锁或串行队列
重复提交同一请求落两次幂等键
可合并改动各改各的字段被打回字段级合并或追加

判断依据是这条记录被谁改、多久改一次、改动能不能合并。改动可合并的(比如给同一条记录加标签、追加备注)走追加而不是整行覆盖,走这条路就不用上锁。什么情况下不适用:纯追加日志、每次只改自己那一行的场景,加锁只会拖慢吞吐,没必要。

锁、版本号、队列:三条路的取舍

悲观锁(行锁或数据库选主更新)适合冲突概率高、改动集中、事务短的场景。关键约束是事务里别夹着外部接口调用,否则锁会被长期占住,把别人的请求全堵死。这类场景下锁的作用域要提前说清,跨库跨服务时一个数据库锁兜不住所有写入。

乐观锁靠版本号或时间戳,更新时带上原值做条件,返回影响行数为零就是被人抢先,让调用方读最新值重试。它适合冲突少、重试代价低的操作。串行队列(同一业务键投到同一队列或单执行者)适合金额、库存这类算错就麻烦的字段,用吞吐换确定性。

  • 冲突概率高、改动集中、事务短:优先悲观锁,但事务内不要调外部接口
  • 冲突少、重试便宜、读多写少:优先乐观锁加有限重试
  • 金额、库存等强一致字段:优先串行队列或单执行者
  • 改动可合并的字段:走追加或字段级合并,不必上锁
  • 团队规模小、并发低:乐观锁加有限重试通常够用

如果拿不准当前项目属于哪种量级,可以到 /rankings 和 /price-watch 看同类项目的公开对比数据,再结合自己的冲突率决定方案,而不是照抄别人的配置。

落地步骤:从接口到数据库走一遍

  1. 给记录加版本字段所有更新语句都带上版本条件,写不到就当作冲突处理,而不是静默成功。上线前先在测试库跑一遍并发对撞,确认条件生效。
  2. 定义冲突后的动作重试、合并、还是直接提示用户刷新,这一步不定义清楚,加了版本号照样丢数据。动作要写在接口文档里,别留给调用方猜。
  3. 写死重试次数和退避重试有限次仍失败就返回明确错误;重试必须让客户端重新拉取数据,用旧值重试等于白做。
  4. 对重复提交做幂等键同一个键只落一次,避免把同一个请求当成两次并发修改。幂等键要和业务键一起进日志。

这套步骤的适用前提是你能改到写入链路。如果你面对的是一个不能改的下游系统,那就只能在调用层做排队或去重,别指望在数据库侧加条件。

怎么验证改动真的生效了,以及出错时看哪里

日志至少记这几项:业务键、请求幂等键、操作前后的版本号、更新影响行数、重试次数。出了覆盖事故,靠这几项能还原是谁盖了谁。缺了任何一项,排查时只能靠猜,效率会差很多。

做一次压测或双人手工对撞:两个人各自读一条记录、同时改不同字段、各自保存,看后保存的人是否被正确拦下。这个测试比单元测试更接近真实事故,建议放进每次改写入逻辑后的回归清单。

  • 线上计数对不上:先看影响行数为零的更新有没有被忽略
  • 重试后数据仍不对:检查是不是重试时用了旧数据
  • 冲突率突然上升:往往是接口被重复调用或前端重复提交
  • 状态莫名其妙回退:先查队列重投有没有幂等保护
  • 日志缺版本号:先补日志,再谈定位

把每次冲突当成一次可观测事件统计,冲突率是判断方案是否够用的直接指标。如果冲突率长期偏高,说明选型或重试策略需要调整,可以到 /guides 对照排查顺序逐一核对。

下一步把上面的清单跑一遍,再对照站内的并发与写入实践指南查漏补缺。

常见问题

乐观锁冲突了就一定要重试吗?

不一定。读到的数据如果可以做合并(比如各自只改不同字段),合并后再更新比整条重试更划算;只有整条记录互斥、且重试代价低时才自动重试。涉及金额、库存这类结果敏感的字段,建议直接把冲突抛给用户确认,而不是后台悄悄重放。

同一用户连点两下保存,算并发问题吗?

更准确说这是重复提交。并发控制挡不住它,因为两次请求内容一样,版本号冲突可能被当成正常重试。做法是前端禁用按钮加后端幂等键,同一个键第二次直接返回上一次结果,不要落到业务逻辑里再判断。

加了锁还是出现覆盖,通常错在哪?

常见有三处:更新语句忘了带版本条件;事务里调了外部接口导致锁提前释放;读和写在不同的连接或不同实例上,锁作用不到一起。排查顺序是先看更新语句是否带条件,再看事务边界,最后核对是不是多数据源写了同一份数据。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。