1. Codex 写的并发更新为什么总在半夜报 Deadlock如果你用 Codex 生成过订单、库存、账户余额这类高并发业务代码大概率遇到过这种诡异现象本地测试全绿单用户操作毫无问题一上并发就偶尔蹦出一句Deadlock detected某个事务被数据库强制回滚接口报 500重试一下又成功了。数据库 CPU 不高慢查询也不多但就是隔三差五失败一次。这类问题几乎不是某条 SQL 写错了而是多个事务同时持有不同资源又互相等待对方释放形成了循环等待。数据库检测到环路后只能挑一个事务回滚来打破僵局。所以死锁不代表数据库卡死真正要问的是为什么业务事务会形成循环等待答案通常指向一个词——锁顺序不一致。Codex 在生成代码时往往按业务语义的自然顺序写转账先锁 A 再锁 B退款先锁 B 再锁 A两段代码单独看都没毛病并发跑起来就可能互相卡住。这篇就从这个切入点把统一锁顺序的改造思路、可复制的配置骨架、死锁复现与验证动作讲清楚帮你在 Codex 生成的并发代码里把死锁率压下来。2. 先理解死锁循环等待是怎么形成的2.1 一个最小转账案例假设有两个账户 A 和 B。事务 1 执行 A 转 B先锁 A 再锁 B事务 2 执行 B 转 A先锁 B 再锁 A。并发时可能出现事务 1 持有 A 等待 B事务 2 持有 B 等待 A谁都无法继续。数据库只能回滚其中一个。这就是死锁的本质不是资源不够而是获取顺序形成了环。2.2 为什么 Codex 容易写出这种代码Codex 是按你给的业务描述逐段生成的。你让它写「订单取消时恢复库存」它会先锁订单再锁库存你让它写「库存调整时检查关联订单」它会先锁库存再检查订单。两个函数各自合理但放在同一个系统里锁顺序就反了。Codex 不会主动帮你统一全局锁顺序除非你在提示词或规则文件里明确要求。2.3 死锁的三个必要条件要形成死锁必须同时满足互斥资源同一时刻只能被一个事务持有、持有并等待持有资源的同时等待其他资源、循环等待存在一个等待环。前两个是数据库并发控制的固有特性很难消除能主动打破的主要是循环等待。统一锁顺序就是直接掐断第三个条件。3. TaoToken 前置给 Codex 一个稳定的模型入口3.1 为什么排查死锁也需要模型辅助死锁日志往往很长包含两个事务的完整 SQL、锁等待关系、traceId。人工读一遍要十几分钟还容易漏掉关键行。让 Codex 帮你先画锁顺序图、列出每个事务访问表的顺序效率会高很多。但前提是模型调用要稳定不能排查到一半断流。我试过用 TaoToken 作为 Codex 的模型入口它的 API 兼容主流调用方式配置一次就能长期用。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。3.2 拿 Key 与接入文档先到控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建后复制 Key妥善保存。接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用的是 Claude Code 这类工具Anthropic 兼容接入说明在 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 。3.3 长期编码场景选 Coding Plan如果你每天都在用 Codex 处理并发事务、读死锁日志、重构锁顺序单次调用成本会累积。长期编码或 Agent 场景可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它更适合高频、持续的开发工作流。4. 统一锁顺序改造思路与可复制配置4.1 核心原则所有事务按同一顺序获取锁最实用的规则是多资源事务永远按资源 ID 从小到大加锁。无论业务调用顺序如何真正获取锁的顺序始终一致循环等待的概率就大幅下降。// 改造前按业务语义顺序加锁容易反序 await lockProduct(productA); await lockProduct(productB); // 改造后先排序再按统一顺序加锁 const ids [productA, productB].sort((a, b) a - b); for (const id of ids) { await lockProduct(id); }4.2 批量更新前先稳定排序处理多个订单时如果不同请求传入的 orderIds 顺序不同请求 A 是 1→2→3请求 B 是 3→2→1就可能增加死锁概率。先排序再处理const sortedIds [...orderIds].sort((a, b) a - b); for (const orderId of sortedIds) { await updateOrder(orderId); }这个模式特别适合库存扣减、多账户操作、批量订单更新、多行SELECT ... FOR UPDATE。4.3 跨表业务规定统一访问顺序死锁可以跨多张表。事务 A 更新 orders 再更新 inventory事务 B 更新 inventory 再更新 orders同样会形成环。团队可以规定统一业务顺序订单 → 库存 → 账户 → 日志。所有相关事务尽量遵循同样顺序不要让不同模块按自己的习惯随意访问。4.4 settings.json 配置骨架把锁规则写进 Codex 的配置文件让后续生成的代码自动遵守。以下是一个可复制的骨架{ codex: { database: { lockOrder: [orders, inventory, accounts, logs], batchSort: true, maxTransactionMs: 2000, forUpdatePolicy: explicit-only, deadlockRetry: { maxAttempts: 3, baseDelayMs: 100, jitter: true } } } }4.5 config.toml 配置骨架如果你用 TOML 管理配置可以这样写[codex.database] lock_order [orders, inventory, accounts, logs] batch_sort true max_transaction_ms 2000 for_update_policy explicit-only [codex.database.deadlock_retry] max_attempts 3 base_delay_ms 100 jitter true4.6 把规则写进 AGENTS.md配置文件之外再在项目根目录的 AGENTS.md 里写清楚锁规则Codex 每次生成并发代码时都会参考# Database Lock 规则 - 多资源事务必须保持统一锁顺序 - 批量更新前必须稳定排序资源 ID - 事务必须尽量短外部 HTTP 调用放在事务外 - 禁止无理由大量使用 SELECT FOR UPDATE - 可使用原子 UPDATE 时优先减少读后写流程 - 死锁重试必须设置最大次数和退避 - 自动重试前必须确认业务具备幂等性 - 死锁排查必须检查索引与锁范围 - 修改并发事务后必须进行真实并发测试5. 验证请求与成功结果5.1 用原子 UPDATE 替代读后写很多死锁来自「先读再判断再写」的模式。库存扣减可以直接用原子 SQLUPDATE products SET stock stock - 1 WHERE id ? AND stock 0;通过影响行数判断是否成功既减少竞态也缩短锁持有时间。Codex 生成代码时你可以在提示词里明确要求优先使用原子 UPDATE。5.2 死锁重试要有限制数据库检测到死锁后会主动回滚一个事务这类错误属于瞬时并发冲突重试一次很可能成功。但必须限制次数并加退避async function runWithRetry(fn, maxAttempts 3) { for (let attempt 0; attempt maxAttempts; attempt) { try { return await fn(); } catch (error) { if (!isDeadlock(error)) throw error; const delay 100 * 2 ** attempt Math.random() * 50; await sleep(delay); } } throw new Error(deadlock retry exhausted); }注意重试事务必须保证幂等。如果事务内部包含发短信、调支付接口、写外部系统回滚后自动重试可能造成副作用执行两次。自动重试更适合纯数据库事务跨系统副作用要单独设计。5.3 制造真实并发来复现死锁普通单元测试测不出死锁必须真正制造并发。可以用两个连接模拟-- 连接 1 BEGIN; SELECT * FROM orders WHERE id 1 FOR UPDATE; -- 暂停不提交 -- 连接 2 BEGIN; SELECT * FROM inventory WHERE id 1 FOR UPDATE; -- 暂停不提交 -- 连接 1 继续 SELECT * FROM inventory WHERE id 1 FOR UPDATE; -- 等待 -- 连接 2 继续 SELECT * FROM orders WHERE id 1 FOR UPDATE; -- 死锁触发修复后再用 10、50、100 并发压测观察死锁率和锁等待时间是否下降。5.4 验证锁顺序是否统一改造完成后让 Codex 帮你检查所有涉及多资源的事务确认加锁顺序一致。可以要求它输出一张表事务名访问表顺序是否符合统一顺序订单取消orders → inventory是库存调整inventory → orders否需改造账户转账accounts → logs是6. 本篇常见错排查6.1 死锁日志只看应用层一句 Transaction failed应用层报错信息通常只有一句Transaction failed看不到真正的死锁图。要追到数据库的 deadlock graph 或锁等待详情。MySQL 可以用SHOW ENGINE INNODB STATUS查看最近一次死锁PostgreSQL 可以查pg_locks和日志里的 deadlock detected 详情。6.2 索引缺失扩大锁范围UPDATE orders SET status done WHERE user_id 1001;如果 user_id 没有索引数据库可能扫描更多记录锁冲突范围随之扩大。遇到死锁时不能只检查业务代码还要看 SQL 是否命中索引、实际扫描多少行、锁住多少记录。6.3 事务里调用第三方 APIawait db.transaction(async tx { await tx.order.update(...); await callThirdPartyApi(); // 危险锁持有时间被拉长 await sleep(3000); await tx.inventory.update(...); });事务期间锁一直不释放其他请求更容易冲突。外部请求尽量放在事务外事务内只做必要的数据库操作快速提交。6.4 一次锁太多数据SELECT * FROM orders WHERE status pending FOR UPDATE;如果 pending 有 5 万条一个事务可能锁住大量记录。更合理的方式是分批读取每批 100 到 500 条短事务处理SELECT id FROM orders WHERE status pending ORDER BY id LIMIT 100 FOR UPDATE;6.5 到处乱加 SELECT FOR UPDATECodex 遇到并发问题很容易建议加FOR UPDATE。它确实能解决部分一致性问题但如果所有查询都升级成行锁并发度会明显下降甚至制造更多锁等待。使用前要明确为什么需要锁、锁哪几行、持有多久、有没有原子 UPDATE 可以替代。6.6 隔离级别直接拉到最高把隔离级别提到 SERIALIZABLE 可能让大量原本可以并发的事务开始互相等待。不要为了「解决并发问题」直接把整个系统隔离级别拉到最高先确认业务需要什么一致性、当前数据库默认行为是什么、哪类异常真正需要避免。6.7 用全表大锁简单解决死锁发现死锁后把并发彻底压成串行确实几乎不会死锁了但吞吐量也没了。真正目标应该是只锁必要资源而不是锁整个系统。两个完全不同用户的订单本来就应该能并发执行。6.8 监控只看 Deadlock 次数死锁只是最严重的一种锁冲突。在真正死锁之前系统可能已经有大量 Lock Wait。建议同时观察锁等待数量、锁等待耗时、事务平均时长、长事务数量、死锁次数。平均锁等待从 5ms 涨到 500ms 时虽然死锁还没爆发接口已经越来越慢。6.9 死锁日志缺少业务上下文给事务日志加 traceId{ event: transaction_deadlock, traceId: trace_1001, operation: transfer_balance, attempt: 2 }这样可以追踪哪个用户请求、进入哪个业务、执行哪些 SQL、最后发生死锁。仅靠数据库线程 ID 通常很难快速定位。6.10 让 Codex 先画锁顺序图再改代码遇到死锁时可以这样要求 Codex请先不要修改代码分析当前死锁流程列出涉及哪些事务、每个事务按什么顺序访问表、每一步可能锁哪些记录、哪两个事务形成循环等待、是否存在长事务、是否有无索引 UPDATE 扩大锁范围、是否可以统一锁顺序、哪些操作可以改成原子 UPDATE。先把锁关系画清楚再改代码比简单加 retry 3 次更能解决根本问题。7. 把死锁率降下来之后统一锁顺序不是银弹但它是最容易落地、收益最直接的一步。配合短事务、原子更新、有限重试、锁等待监控Codex 生成的并发代码可以从「偶尔死锁」变成「极少死锁且可安全恢复」。如果你在排查过程中需要模型帮你读死锁日志、画锁顺序图、重构事务代码可以用 TaoToken 的模型对话入口 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。先拿 Key再按接入文档配置就能把 Codex 接到稳定的模型服务上。长期做并发重构和压测的话Coding Plan 会更划算 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。真正可靠的并发事务设计不是保证系统永远没有锁而是做到锁得少、锁得短、顺序一致并且即使偶尔发生死锁也能安全恢复。
