AI写代码的并发安全关:7个线上bug死于同一个“读—算—写”结构(附行锁/乐观锁规则文件)

📅 2026/8/11 6:47:28
AI写代码的并发安全关:7个线上bug死于同一个“读—算—写”结构(附行锁/乐观锁规则文件)
AI写代码的并发安全关7个线上bug死于同一个“读—算—写”结构附行锁/乐观锁规则文件摘要本文深入剖析了 AI 生成代码中常见的并发安全问题指出 7 个线上 bug 均源于相同的“读—算—写”结构。文章分析了 AI 产生此类问题的根本原因训练语料局限和人工审查疏忽并提供了完整的解决方案通过决策树区分不同并发场景无竞争、低并发、高并发给出行锁和乐观锁的具体代码实现最后介绍了如何将规则集成到 AI 审查 Skill 中实现自动化防护。上一篇我把团队的 AI 审查 Skill 开源了——五关清单41 条规则仓库地址在文末。有朋友问41 条规则会一条条讲吗讲。把最硬的几关一篇一篇拆开今天从并发关开始。它在五关里条数不是最多但战果最大那 11 个线上 bug 里7 个死在它手里。之前系列第二篇里给过一份人审清单但当时只给了审法没给规则全文和代码。而且有个数字没说透那 7 个并发 bug其实是同一个 bug。场景各不相同代码结构一模一样。这篇把审法翻译成 AI 能自动执行的规则文件代码补齐。同 6 行代码炸了我们 7 次把 7 个并发 bug 的代码叠在一起看全是这个结构Transactional public void deductPoints(Long userId, Integer amount) { User user userMapper.selectById(userId); // 读 if (user.getPoints() amount) { // 算 throw new BusinessException(积分不足); } user.setPoints(user.getPoints() - amount); userMapper.updateById(user); // 写 }读一次算一下写回去。就这三步。单线程下这段代码完美——逻辑通顺命名规范测试全绿。我们本地跑了三天没出过一次错。上线之后两个请求同时读到 points 100各扣 10 分都写回了 90。实际应该是 80。MySQL 默认 RR 隔离级别下普通 SELECT 和 UPDATE 之间有一个窗口并发一起来这个窗口就是抽奖。7 个 bug 全死在这个窗口上。积分、库存、计数类字段换着花样死。为什么 AI 每次都写出这个结构复盘到第 3 个的时候我就不骂 AI 了因为我弄明白了一件事这不是失手这是它的标准答案。它训练语料里的教科书示例、博客教程、高票回答绝大多数扣减积分的代码就长这样——教科书在单线程语境里教书这段代码在单线程语境里是对的。它不知道你的接口 QPS 上千不知道这个字段每天被并发读写多少次。你不告诉它它就给你教科书答案。还有一个原因在人这边。AI 30 秒写完我就下意识地 30 秒审完——逻辑一眼就通通到我把 review 从推演一遍并发路径退化成了看一眼通不通。7 个 bug 里有好几个我 review 的时候看了两遍没看出来。不是逻辑复杂是逻辑太顺了这两个原因凑在一起结论只有一个**规则必须写成文件让 AI 每次动笔前先读。靠 prompt 会忘靠人肉 review 会漏靠文件不会。**决策树三句话分完所有场景并发关规则文件的核心是一棵决策树。碰到读—算—写结构先问数据归属1.不同用户操作不同数据各扣各的分→ 没有数据竞争不需要锁但要防重复提交——同一个请求不能因为用户双击或网络重试被执行两次。2.同一份数据、并发量低→ SELECT FOR UPDATE 行锁简单可靠。3.同一份数据、并发量高秒杀、抢券→ Redis DECR / Lua 原子预扣异步落库。这种场景上行锁会把自己拖死。外加一条兜底并发量不确定的时候先上行锁加监控看重试率再决定要不要换方案。不确定时不许裸奔。顺带修正一个旧口径之前的系列里我写过不确定就先上乐观锁。五个月跑下来这条改了——乐观锁在冲突率上来之后会引发重试风暴比行锁更难收拾。现在规则库的默认是行锁。规则是活的以仓库最新版为准。正确写法直接抄姿势一行锁低并发首选Select(SELECT * FROM account WHERE id #{id} FOR UPDATE) Account lockAndSelect(Param(id) Long id); Transactional public void deductPoints(Long userId, Integer amount) { Account account accountMapper.lockAndSelect(userId); if (account.getPoints() amount) { throw new BusinessException(积分不足); } accountMapper.deductPoints(userId, amount); }注意FOR UPDATE 必须在 Transactional 方法内调用否则锁不会持有到事务结束——锁提前释放等于没锁。这种细节是踩出来的不是看文档看出来的。姿势二乐观锁冲突少的场景Update(UPDATE account SET points points - #{amount}, version version 1 WHERE id #{id} AND version #{version}) int deductWithVersion(Param(id) Long id, Param(amount) Integer amount, Param(version) Integer version); Transactional public void deductPoints(Long userId, Integer amount) { for (int i 0; i 3; i) { Account account accountMapper.selectById(userId); if (account.getPoints() amount) { throw new BusinessException(积分不足); } int affected accountMapper.deductWithVersion( userId, amount, account.getVersion()); if (affected 1) return; } throw new BusinessException(扣减冲突请重试); }三个要点UPDATE 语句本身完成判断 扣减两步原子执行version 字段做 CAS失败重试三次不行就抛业务异常让上游重试。怎么选并发量低、冲突少 → 乐观锁没有锁开销并发量不确定 → 先行锁加监控高并发秒杀 → Redis 原子预扣。这条规则在 Skill 里怎么生效规则文件 references/concurrency.md 的结构适用条件代码里出现读—算—写就触发→决策树→禁止写法→正反例。装上之后的效果AI 生成代码时碰到积分扣减这类场景第一稿就直接给出带锁的版本并在注释里标明守住了哪几关review 已有代码时看到裸的 SELECT UPDATE 会直接拦下附上修改后的代码。团队版里并发关攒到了 9 条读—算—写主规则、防重复提交、缓存并发、批量场景的豁免条款……每条后面标着来源——谁加的、踩了什么坑、什么时候加的。还有一条元规则我认为比所有具体规则都重要AI 不确定项目的外部调用范围或并发量级时必须向人提问禁止猜测。方案选错可以改猜错了没人知道。最后仓库地址[github.com/wangheng19901021/skills](https://github.com/wangheng19901021/skills)。并发关规则全文在 references/concurrency.md正反例在 examples/select-for-update.md复制进你项目的 .claude/skills/ 就能用。文中说的 11 个 bug 完整复盘和五关清单速查表都收进了《AI 代码审查避坑手册AI 写代码的并发安全关7 个线上 bug 死于同一个“读—算—写”结构附行锁/乐观锁规则文件关事务里调 RPCAI 每次都踩——afterCommit一条规则的事。—— 硅基书斋主理人十年 Java 后端