后台项目里常见这种场面:两个同事几乎同时点保存,后写的把先写的悄悄盖掉;或者队列重投一次,订单状态被改回去了。这篇不讲并发模型理论,只给能落地的做法:先判断你属于哪类冲突,再挑锁、版本号还是队列,最后按顺序排查。选型会写明什么情况下不适用,别硬套。
后台项目里常见这种场面:两个同事几乎同时点保存,后写的把先写的悄悄盖掉;或者队列重投一次,订单状态被改回去了。这篇不讲并发模型理论,只给能落地的做法:先判断你属于哪类冲突,再挑锁、版本号还是队列,最后按顺序排查。
选型会写明什么情况下不适用,别硬套。挑选具体实现方案前,建议先到 /tools/speed-test 这类站内工具页确认你当前用的数据库与中间件版本支持哪些能力,再到 /guides 看对应的接入指南,避免把不兼容的写法硬塞进现有代码里。
先分清你是哪种冲突,再谈方案
读改写覆盖(丢更新)和写写冲突表现不一样。前者是后提交的静默盖掉,用户毫无感知;后者常见报错或脏读,至少能被发现。先按业务影响判断哪种更痛:如果这条记录是金额、库存、状态机这类算错就麻烦的字段,静默覆盖比报错更危险,得优先防。
同一用户重复提交和多用户真并发是两回事。前者是网络重试或手抖连点,本质是同一个逻辑请求被落了两次,优先做幂等;后者是两个人真的读到同一份旧数据同时改,才需要锁或版本控制。判断错了方向,加了锁也防不住重复提交带来的数据错乱。
| 冲突类型 | 典型表象 | 优先手段 |
|---|---|---|
| 读改写覆盖 | 后保存静默盖掉先保存 | 版本号条件更新 |
| 写写冲突 | 报错、脏读、状态跳变 | 行锁或串行队列 |
| 重复提交 | 同一请求落两次 | 幂等键 |
| 可合并改动 | 各改各的字段被打回 | 字段级合并或追加 |
判断依据是这条记录被谁改、多久改一次、改动能不能合并。改动可合并的(比如给同一条记录加标签、追加备注)走追加而不是整行覆盖,走这条路就不用上锁。什么情况下不适用:纯追加日志、每次只改自己那一行的场景,加锁只会拖慢吞吐,没必要。
锁、版本号、队列:三条路的取舍
悲观锁(行锁或数据库选主更新)适合冲突概率高、改动集中、事务短的场景。关键约束是事务里别夹着外部接口调用,否则锁会被长期占住,把别人的请求全堵死。这类场景下锁的作用域要提前说清,跨库跨服务时一个数据库锁兜不住所有写入。
乐观锁靠版本号或时间戳,更新时带上原值做条件,返回影响行数为零就是被人抢先,让调用方读最新值重试。它适合冲突少、重试代价低的操作。串行队列(同一业务键投到同一队列或单执行者)适合金额、库存这类算错就麻烦的字段,用吞吐换确定性。
- 冲突概率高、改动集中、事务短:优先悲观锁,但事务内不要调外部接口
- 冲突少、重试便宜、读多写少:优先乐观锁加有限重试
- 金额、库存等强一致字段:优先串行队列或单执行者
- 改动可合并的字段:走追加或字段级合并,不必上锁
- 团队规模小、并发低:乐观锁加有限重试通常够用
如果拿不准当前项目属于哪种量级,可以到 /rankings 和 /price-watch 看同类项目的公开对比数据,再结合自己的冲突率决定方案,而不是照抄别人的配置。
落地步骤:从接口到数据库走一遍
- 给记录加版本字段所有更新语句都带上版本条件,写不到就当作冲突处理,而不是静默成功。上线前先在测试库跑一遍并发对撞,确认条件生效。
- 定义冲突后的动作重试、合并、还是直接提示用户刷新,这一步不定义清楚,加了版本号照样丢数据。动作要写在接口文档里,别留给调用方猜。
- 写死重试次数和退避重试有限次仍失败就返回明确错误;重试必须让客户端重新拉取数据,用旧值重试等于白做。
- 对重复提交做幂等键同一个键只落一次,避免把同一个请求当成两次并发修改。幂等键要和业务键一起进日志。
这套步骤的适用前提是你能改到写入链路。如果你面对的是一个不能改的下游系统,那就只能在调用层做排队或去重,别指望在数据库侧加条件。
怎么验证改动真的生效了,以及出错时看哪里
日志至少记这几项:业务键、请求幂等键、操作前后的版本号、更新影响行数、重试次数。出了覆盖事故,靠这几项能还原是谁盖了谁。缺了任何一项,排查时只能靠猜,效率会差很多。
做一次压测或双人手工对撞:两个人各自读一条记录、同时改不同字段、各自保存,看后保存的人是否被正确拦下。这个测试比单元测试更接近真实事故,建议放进每次改写入逻辑后的回归清单。
- 线上计数对不上:先看影响行数为零的更新有没有被忽略
- 重试后数据仍不对:检查是不是重试时用了旧数据
- 冲突率突然上升:往往是接口被重复调用或前端重复提交
- 状态莫名其妙回退:先查队列重投有没有幂等保护
- 日志缺版本号:先补日志,再谈定位
把每次冲突当成一次可观测事件统计,冲突率是判断方案是否够用的直接指标。如果冲突率长期偏高,说明选型或重试策略需要调整,可以到 /guides 对照排查顺序逐一核对。
下一步把上面的清单跑一遍,再对照站内的并发与写入实践指南查漏补缺。→常见问题
乐观锁冲突了就一定要重试吗?
不一定。读到的数据如果可以做合并(比如各自只改不同字段),合并后再更新比整条重试更划算;只有整条记录互斥、且重试代价低时才自动重试。涉及金额、库存这类结果敏感的字段,建议直接把冲突抛给用户确认,而不是后台悄悄重放。
同一用户连点两下保存,算并发问题吗?
更准确说这是重复提交。并发控制挡不住它,因为两次请求内容一样,版本号冲突可能被当成正常重试。做法是前端禁用按钮加后端幂等键,同一个键第二次直接返回上一次结果,不要落到业务逻辑里再判断。
加了锁还是出现覆盖,通常错在哪?
常见有三处:更新语句忘了带版本条件;事务里调了外部接口导致锁提前释放;读和写在不同的连接或不同实例上,锁作用不到一起。排查顺序是先看更新语句是否带条件,再看事务边界,最后核对是不是多数据源写了同一份数据。