#7431·juicefs

SQL metadata edge concurrency race analysis

Author: jiefenghuangCreated Aug 21, 2026Updated Sep 3, 2026

SQL metadata 并发一致性问题跟踪

状态更新时间:2026-09-03

问题总览

ID 严重级别 问题 当前状态 关联 PR
SQL-C01 Critical ACL 在事务提交前发布,并错误负缓存序列空洞 未修复 暂无
SQL-C02 Critical BatchUnlink 在事务重试间泄漏删除状态 main 已修复 #7460
SQL-C03 Critical 多个 GC 重复处理同一 delayed slice main 已修复 #7449
SQL-C04 Critical Session cleanup 缺少 generation 和 fencing 未修复 暂无
SQL-C05 Critical Namespace 缺少完整的 edge/parent 并发不变量 未完全修复;#7472 暂时关闭 #7472#7476#7477
SQL-H01 High Detached Attach 和 Cleanup 没有共同的原子 claim 未修复 暂无
SQL-H02 High Dump/Backup 没有使用同一个一致性快照 未修复 暂无
SQL-M01 Medium 在线 repair/sync 会覆盖并发增量 未修复 暂无
SQL-M02 Medium 多客户端 quota 没有原子 reservation 未修复 暂无
SQL-M03 Medium Format/config 更新没有 CAS,并发布未提交状态 未修复 暂无
SQL-M04 Medium Xattr mutation 没有参与 inode 生命周期锁 main 已修复 #7457
SQL-L01 Low noReadOnlyTxn 存在无同步读写 main 已修复 #7451

已合并 PR

PR 修复问题 合并时间 状态
#7449 SQL-C03 2026-08-26 已合并,检查通过
#7451 SQL-L01 2026-08-26 已合并,检查通过
#7460 SQL-C02 2026-08-26 已合并,检查通过
#7457 SQL-M04 2026-08-27 已合并,检查通过

以上修复目前只确认进入 main,尚未确认对应稳定分支已经回移。

问题详情

SQL-C01:ACL 在事务提交前发布,并错误负缓存序列空洞

  • 严重级别:Critical
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3
  • 关联 PR:暂无

为什么发生

SQL sequence 或 auto-increment 的 ID 分配顺序不等于事务提交顺序。

一个事务可以先取得 ACL ID,但尚未提交。另一个客户端可能已经观察到更大的 ACL ID,却无法在自己的数据库快照中读取前一个未提交的 ACL。

当前实现会把暂时无法读取的 ACL ID 当成永久空洞,并缓存为 EmptyRule。第一个事务后续即使提交,错误缓存仍然存在。

此外,insertACL 在事务 callback 中直接更新进程内 aclCache。如果事务随后回滚或重试,数据库中并不存在该 ACL,但缓存已经对其他请求可见。

复现步骤

  1. T1 插入 ACL,数据库分配 ID 100,但事务暂不提交。
  2. T2 插入或读取到更大的 ACL ID。
  3. T2 在自己的快照中查询不到 ID 100。
  4. T2 把 ID 100 缓存为 EmptyRule
  5. T1 提交,并让某个 inode 引用 ACL 100。
  6. T2 后续继续使用错误的 EmptyRule,不再查询数据库。

影响

  • 不同客户端可能对同一个 inode 得出不同权限结果。
  • EmptyRule 可能把本应失败的权限检查变成允许,形成权限绕过。
  • 事务回滚后可能留下只存在于缓存中的幽灵 ACL。
  • 错误状态可能持续到进程重启或缓存失效。

推荐解决方案

  1. ACL 缓存只能在数据库事务成功提交后发布。
  2. 增加 after-commit 或 attempt-local outcome 机制。
  3. 不要把暂时查询不到的 ACL ID 永久缓存为 EmptyRule
  4. inode 引用了不存在的 ACL 时返回 EIO,保持 fail closed。
  5. 增加乱序提交、事务回滚和事务重试的确定性测试。

SQL-C02:BatchUnlink 在事务重试间泄漏删除状态

  • 严重级别:Critical
  • 当前状态:main 已修复
  • 影响版本:mainrelease-1.4
  • 修复 PR:PR #7460

为什么发生

数据库 rollback 只能回滚数据库状态,不能回滚事务 callback 对外部 Go map 的修改。

原实现把 delNodes 等状态创建在 m.txn 外部。第一次事务 attempt 即使失败并回滚,已经写入 map 的 inode 仍会保留。后续 attempt 成功后,旧状态可能被当成最终结果消费。

复现步骤

  1. BatchUnlink 第一次 attempt 把 inode 100 的 nlink 计算为 0。
  2. 第一次 attempt 把 inode 100 写入 delNodes
  3. 后续 SQL 返回可重试错误,事务回滚。
  4. 并发 namespace 操作使 inode 100 在第二次 attempt 中仍然存活。
  5. 第二次 attempt 成功提交,没有删除 inode 100。
  6. 第一次 attempt 留下的 delNodes[100] 仍被消费。
  7. 后台删除仍存活 inode 100 的数据块。

影响

仍具有有效 node 和 namespace entry 的文件可能失去数据块,属于不可恢复的数据丢失风险。

解决方案

PR #7460 已将删除候选、缓存失效和统计结果改成 attempt-local 状态,只发布最终成功提交的 attempt。

可以进一步在异步删除数据块前验证:

  • inode 已经不存在;
  • 存在匹配的 deletion marker 或 claim;
  • 删除任务属于当前有效 generation。

SQL-C03:多个 GC 重复处理同一 delayed slice

  • 严重级别:Critical
  • 当前状态:main 已修复
  • 影响版本:mainrelease-1.4release-1.3
  • 修复 PR:PR #7449

为什么发生

两个 GC 可以在 delayed slice 记录被删除前同时读取它。

虽然 chunk_ref 行锁能把两个引用扣减操作串行执行,但它不能证明当前 GC 仍然拥有 delayed row。第二个 GC 即使删除 delayed row 时影响行数为 0,原实现仍可能提交引用扣减。

复现步骤

  1. 一个 slice 有一个活引用和一个 delayed 引用,refs = 2
  2. GC1 和 GC2 同时读取同一 delayed row。
  3. GC1 把 refs 从 2 减到 1,删除 delayed row 并提交。
  4. GC2 等待行锁后继续把 refs 从 1 减到 0。
  5. GC2 删除 delayed row 时影响行数为 0。
  6. 如果仍提交引用扣减,活对象会被当成无引用对象删除。

影响

仍被有效 chunk 引用的数据对象可能被 GC 删除,造成真实数据丢失。

解决方案

PR #7449 增加 delayed row 的原子 claim,只有成功 claim 的 GC 才能扣减引用并继续清理。

仍需评估稳定分支回移,并保留多 GC 并发执行的确定性回归测试。


SQL-C04:Session cleanup 缺少 generation 和 fencing

  • 严重级别:Critical
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3
  • 关联 PR:暂无

为什么发生

Session stale 扫描和实际清理不是同一个原子状态转换。

Cleaner 扫描到一个 SID 过期后,客户端可能已经恢复并 refresh session,但 Cleaner 仍会根据旧扫描结果删除锁、sustained inode 和 session。

相同 SID 还可以被重新创建,因此 SID 无法区分旧 session generation 和新 generation。

复现步骤

  1. 客户端暂时暂停,被 Cleaner 扫描为 stale。
  2. 客户端恢复并 refresh 相同 SID。
  3. Cleaner 根据旧扫描结果删除客户端的 flock/plock。
  4. 客户端继续使用或创建新的锁。
  5. Cleaner 又删除 sustained inode 和 session row。
  6. 客户端下一次 refresh 重新创建同一个 SID。

影响

  • 活跃 POSIX 锁可能被错误释放。
  • open-unlinked 文件可能被当作 stale sustained inode 删除。
  • 旧 Cleaner 可以删除新 session generation 创建的资源。
  • 混合版本客户端会增加协议不一致风险。

推荐解决方案

  1. Session 增加不可复用的 generation。
  2. 增加 ActiveCleaningClosed 等状态。
  3. 使用数据库时间和条件 UPDATE 原子 claim 过期 generation。
  4. session-scoped 数据关联 SID 和 generation。
  5. Refresh 不允许复活已经失去 lease 的 generation。
  6. Cleanup 每一步都验证 SID、generation、state 和 owner。
  7. 作为混合版本协议变更评估 MinClientVersion

SQL-C05:Namespace 缺少完整的 edge/parent 并发不变量

原 Issue 正文中的 10 项问题统一归入本组。

子问题一:读取旧 edge 后误删或误改新 edge

为什么发生

doUnlinkdoRmdirdoRenamedoBatchUnlink 会先读取 edge(parent,name),随后等待 inode 行锁。

等待期间,另一个事务可能删除或替换同名 edge。原来的 DELETE/UPDATE 主要按 (parent,name) 定位,没有验证当前记录仍然是之前读取的 inode、type 和 ID。

在 MySQL Repeatable Read 下,SELECT 可能读取旧快照,而 DELETE/UPDATE 会作用于最新已提交记录,因此可能误删另一个事务刚创建的新 edge。

复现步骤
  1. T1 读取 /a -> inode 100
  2. T1 等待 inode 100 的行锁。
  3. T2 删除 /a,重新创建 /a -> inode 300 并提交。
  4. T1 执行 DELETE WHERE parent=? AND name='a'
  5. T1 删除的是新 edge /a -> inode 300
  6. T1 随后仍按旧 inode 100 更新 nlink、统计和回收状态。
影响
  • 并发 unlink/rename 下误删其他客户端新创建的文件。
  • nlink、目录统计和 quota 按错误 inode 结算。
  • 产生悬挂 edge、孤儿 inode 或 namespace 丢失。
推荐解决方案

PR #7476 实现:

  1. 使用完整身份 (id,parent,name,inode,type) 定位旧 edge。
  2. DELETE/UPDATE 必须检查 affected rows 恰好为 1。
  3. 身份不一致时返回可重试错误,让事务在新快照上重跑。
  4. 覆盖 unlink、rmdir、rename replace/exchange 和 BatchUnlink。
  5. BatchUnlink 对重复 name 去重,避免恒定 affected-row mismatch。

子问题二:rmdir 与并发 child 插入没有共同锁

为什么发生

rmdir 会锁定目标目录 inode,并检查目录中没有 child edge。

MknodLink、rename-in、Clone 和 BatchClone 原来不一定锁定目标 parent。尤其在 SkipDirMtime 窗口内,创建普通文件可能不会更新 parent,也无法通过 parent UPDATE 的 affected rows 发现目录已被删除。

复现步骤
  1. /d 是空目录。
  2. T1 执行 rmdir /d,锁定 node(d) 并确认没有 child edge。
  3. T2 执行 create /d/f
  4. T2 没有参与 parent 生命周期锁,插入 child edge。
  5. 因为 SkipDirMtime 未到期,T2 跳过 parent update。
  6. T1 删除 /d 的 edge 和 node(d)
  7. 最终留下 parent inode 不存在的 /d/f edge。
影响
  • 产生无法从 root 遍历的孤儿文件或孤儿子树。
  • 用户无法通过正常路径访问或删除。
  • inode、目录统计和 quota 不一致。
  • 对象数据可能长期无法回收。
推荐解决方案

PR #7472 尝试实现:

  1. 建立统一的 parent 生命周期锁协议。
  2. 向目录插入 child edge 时锁定目标 parent。
  3. rmdir 判空并删除目录时持有排他 parent/target 锁。
  4. 多 inode 操作按稳定顺序加锁,避免交叉死锁。
  5. 加锁后重新读取并验证 node 状态。
  6. 大小写不敏感模式中的“检查后插入”应使用排他 parent 锁。
  7. 批量路径不能退化为每个 inode 一次远程数据库 RTT。

当前决定

暂不合入 #7472。该 PR 试图通过统一 parent 生命周期锁和多 inode 有序加锁,解决 rmdir 与并发 create/link/rename/clone 产生孤儿 edge,以及部分死锁问题。但方案会侵入多个 namespace 热路径,并涉及数据库共享锁兼容、额外数据库往返、锁竞争和混合版本协议,复杂度及验证成本较高,当前收益有限。

当前重点讨论的孤儿 edge 风险主要发生在 SkipDirMtime > 0 时:普通文件 create/link 可能跳过 parent 更新,无法通过事务冲突或 affected rows 发现 parent 已被并发删除。设置 --skip-dir-mtime=0 可以显著降低这类风险,但不能解决多 inode 加锁顺序导致的死锁,也不等价于完整的 parent 生命周期保护。

现阶段将其保留为已知问题,待出现明确的生产影响,或找到更小、更容易验证的修复方案后再重新评估。

子问题三:多 inode 加锁顺序不一致

为什么发生

rename、BatchUnlink 和 BatchClone 等操作涉及多个 inode。不同路径按照业务角色顺序逐个加锁,两个事务可能以相反顺序锁定相同 inode。

复现步骤
  1. T1 按 (inode A, inode B) 获取锁。
  2. T2 按 (inode B, inode A) 获取锁。
  3. T1 持有 A 等待 B。
  4. T2 持有 B 等待 A。
  5. 数据库检测到死锁并回滚其中一个事务。
  6. 高并发下持续发生死锁和重试。
影响
  • namespace 操作延迟明显增加。
  • 大量死锁回滚形成重试风暴。
  • 数据库压力升高,操作最终可能以 EIO 失败。
推荐解决方案
  1. 收集事务涉及的全部 inode。
  2. 去重并合并锁强度。
  3. 共享锁与排他锁冲突时升级为排他锁。
  4. 按稳定的 inode 顺序获取锁。
  5. 对 BatchClone/BatchUnlink 进行真实数据库性能测试,避免 N+1 查询。

子问题四:trash 决策在事务重试和多客户端之间失效

为什么发生

trash 值在事务外通过 checkTrash 得到,事务内又可能被降级为 0。如果事务重试,第一次 attempt 修改过的变量可能泄漏到下一次 attempt。

subTrash 还会在客户端进程内缓存。另一个客户端删除对应 trash 目录后,当前客户端可能继续把 edge 挂到已经不存在的 parent inode 下。

复现步骤

场景 A:事务重试状态泄漏

  1. 第一次事务 attempt 选择把文件移入 trash。
  2. attempt 内某个修复分支把 trash 改成 0。
  3. 事务遇到冲突并重试。
  4. 第二次 attempt 继承 trash=0
  5. 本应进入回收站的文件被直接永久删除。

场景 B:失效 trash 缓存

  1. Client A 缓存当前 subTrash inode。
  2. Client B 清理并删除该 trash 子目录。
  3. Client A 继续使用旧 inode。
  4. 文件 edge 被挂到已经不存在的 parent 下。
影响
  • 文件绕过回收站,被意外永久删除。
  • 产生无法正常访问和清理的孤儿元数据。
  • SQL、Redis 和 TKV 都可能受相关状态一致性问题影响。
推荐解决方案

PR #7477 尝试实现:

  1. 保存 requestedTrash,每次事务 attempt 开始时重新初始化 trash
  2. 在事务内验证 trash node 仍然存在。
  3. Redis 使用 WATCH,TKV 使用事务读取。
  4. SQL 与 PR #7472 合并后使用锁定读取。
  5. 删除缓存对应的 trash 目录时同步失效 subTrash

当前未完成项

截至 2026-09-03:

  • #7472 决定暂时关闭;#7476、#7477 仍是 Open、Review required。
  • 三个 PR 各自检查通过,但尚未验证合并后的最终代码。
  • 三个 PR 修改同一批函数,合并时存在文本冲突。
  • #7472 仍有以下未解决评审问题:
    • 回归测试没有真实复现双事务交错;
    • 大小写不敏感模式的共享 parent 锁不充分;
    • BatchClone/BatchUnlink 可能产生大量串行数据库 RTT。
  • 仍需 MySQL、MariaDB、PostgreSQL、TiDB 双 session barrier 测试。
  • 仍需评估新旧客户端混合运行时旧客户端绕过新锁协议的问题。

SQL-H01:Detached Attach 和 Cleanup 没有共同的原子 claim

  • 严重级别:High
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3
  • 关联 PR:暂无

为什么发生

Attach 和 Cleanup 都把 detached marker 的存在当作所有权,但没有对同一 marker generation 执行原子 claim。

Cleanup 会跨多个事务递归清理。Attach 只锁 destination parent,无法阻止已经开始的 Cleanup 继续删除刚刚 Attach 成功的 subtree。

复现步骤

  1. Cleanup 扫描到 detached inode 500 的过期 marker。
  2. Cleanup 开始处理 inode 500。
  3. Clone 完成,Attach 发布指向 inode 500 的 edge。
  4. Attach 删除 marker 并提交。
  5. Cleanup 没有重新验证 marker 所有权。
  6. Cleanup 删除 inode 500 及相关元数据。
  7. 新发布的 edge 指向不存在的 inode。

影响

  • 已成功 Attach 的目录或 Clone subtree 可能消失。
  • 产生 dangling edge。
  • 多个 Cleaner 可能重复扣减 counter。
  • Cleaner 崩溃后留下无法安全恢复的半清理状态。

推荐解决方案

  1. Detached marker 增加 state、generation、owner 和 lease expiration。
  2. Attach 和 Cleanup 原子 claim 同一 generation。
  3. 发布 edge 或删除 node 前重新验证 claim。
  4. Cleanup 设计为可恢复、可重试、幂等。
  5. 只有最终状态转换成功后才能更新统计。

SQL-H02:Dump/Backup 没有使用同一个一致性快照

  • 严重级别:High
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3
  • 关联 PR:暂无

为什么发生

namespace tree、node、chunk、xattr 和 ACL 可能由不同数据库事务读取,因此来自不同快照。

Fast dump 还把一次调用的 snapshot 状态保存在共享的 dbMeta.snap 字段中,并发 DumpMeta 调用可能相互覆盖或清空状态。

多线程 binary dump 已明确警告:数据库可写时不能保证一致性,而 CLI 默认会使用多个线程。

复现步骤

  1. 外层 dump transaction 读取 /a -> inode 100
  2. 并发客户端删除或替换 inode 100。
  3. dump worker 使用另一个 transaction 读取 inode 100 的 metadata。
  4. worker 观察到与 namespace tree 不同的数据库状态。
  5. 输出中可能存在没有 node/chunk 的 edge,或者缺少必要元数据。

并发 fast dump 场景:

  1. Dump A 设置 m.snap = snapshotA
  2. Dump B 设置 m.snap = snapshotB
  3. Dump A 或 B 清空 m.snap
  4. 另一个 dump 继续读取错误或空 snapshot。

影响

  • 格式合法的备份不一定能够一致恢复。
  • 恢复后可能缺少 node、chunk、xattr 或 ACL。
  • 并发 dump 可能出现 data race、panic 或不完整输出。
  • 自动备份可能在没有明确失败的情况下生成错误备份。

推荐解决方案

  1. 一次 dump 调用使用同一个 Repeatable Read transaction/snapshot。
  2. Snapshot 状态作为调用级参数传递,不能存放在共享 dbMeta 字段。
  3. 在共享快照方案完成前,SQL dump 和自动备份强制使用单线程。
  4. 使用统一的 error/cancel owner,任意 worker 失败都让整个 dump 失败。
  5. 在持续 namespace mutation 下执行 dump/load,并验证恢复后的元数据不变量。

SQL-M01:在线 repair/sync 会覆盖并发增量

  • 严重级别:Medium
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3 部分路径
  • 关联 PR:暂无

为什么发生

Repair 使用“先扫描、再写入绝对值”的方式。

扫描过程中正常 writer 仍然可以更新 counter、quota 和目录统计。Repair 最终写入扫描得到的旧绝对值时,会覆盖扫描期间产生的新增量。

只锁定最终 counter 行不能保护之前的全量扫描。

复现步骤

  1. Repair 扫描得到 usedSpace = 100
  2. 客户端创建文件,把真实值增加到 110。
  3. Repair 完成扫描。
  4. Repair 把旧值 100 写回数据库。
  5. 文件仍存在,但对应 usage delta 永久丢失。

影响

  • Volume counter 不准确。
  • Directory statistics 不准确。
  • Quota usage 与真实数据不一致。
  • 目录 nlink 在 repair 成功后仍可能错误。

推荐解决方案

  1. 短期:repair 期间获取全局维护写锁,阻止正常 mutation。
  2. 长期:增加 maintenance epoch、write fence 或 mutation watermark。
  3. Repair 写入时使用 revision/epoch CAS。
  4. 合并扫描开始后产生的增量,而不是直接覆盖绝对值。
  5. 发现并发 mutation 时放弃结果并重新扫描。

SQL-M02:多客户端 quota 没有原子 reservation

  • 严重级别:Medium
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3 的 filesystem/directory quota
  • 关联 PR:暂无

为什么发生

每个客户端使用自己的 quota usage 缓存进行检查。

多个客户端可以同时通过同一个 hard limit 检查,各自提交 metadata mutation,之后再异步 flush quota delta。

最终数据库 usage 可能正确,但检查发生得太早,无法实现严格 hard limit。

复现步骤

  1. Quota limit 为 100,当前 usage 为 90。
  2. Client A 申请使用 8。
  3. Client B 同时申请使用 8。
  4. 两个客户端都根据本地缓存计算得到 98。
  5. 两个 mutation 都成功提交。
  6. 后续 delta flush 后 usage 变为 106。

影响

Filesystem、directory、user 和 group quota 都可能突破声明的 hard limit。

推荐解决方案

  1. 在 metadata mutation transaction 内执行权威 reservation。
  2. 多个 quota 维度按稳定顺序锁定 quota rows。
  3. Metadata mutation 回滚时 reservation 一并回滚。
  4. 本地缓存只用于展示和快速拒绝,不能作为最终 enforcement。
  5. 如果不提供严格 reservation,应把文档改为 eventually enforced limit。
  6. 旧客户端可能绕过新协议,需要升级门禁或 MinClientVersion

SQL-M03:Format/config 更新没有 CAS,并发布未提交状态

  • 严重级别:Medium
  • 当前状态:未修复
  • 影响版本:mainrelease-1.4release-1.3 核心路径
  • 关联 PR:暂无

为什么发生

多个客户端可以读取同一份 Format JSON,分别修改不同字段,然后无 revision 条件地覆盖整个 value。

进程内 Format 对象还可能在数据库事务提交前发布。如果事务失败,当前进程使用的配置与数据库配置不一致。

部分代码还会直接修改共享 Format 指针,存在 data race 风险。

复现步骤

  1. Client A 和 Client B 同时读取 Format F0。
  2. A 修改 TrashDays
  3. B 启用 DirStats
  4. A 写入 FA。
  5. B 基于旧 F0 写入 FB。
  6. B 的整段 JSON 覆盖 A 的修改。

未提交发布场景:

  1. 事务内调用 setFormat 更新内存对象。
  2. 后续数据库操作失败并回滚。
  3. 当前进程仍然暴露未提交配置。

影响

  • 并发 config 命令静默丢失更新。
  • 不同客户端使用不同配置。
  • Feature cleanup 与 flag 持久化可能只完成一半。
  • 共享 Format 修改可能产生 Go data race。

推荐解决方案

  1. 为 setting 增加 revision,并使用 CAS。
  2. 或在同一个写事务内锁定、重新读取和更新 Format。
  3. 已发布的 Format 对象保持不可变,使用 copy-on-write。
  4. 只有数据库提交成功后才能发布新对象。
  5. Feature cleanup、setting update 和 changelog 放在同一原子边界。
  6. 增加混合版本保护,阻止旧客户端继续整段覆盖 JSON。

SQL-M04:Xattr mutation 没有参与 inode 生命周期锁

  • 严重级别:Medium
  • 当前状态:main 已修复
  • 影响版本:mainrelease-1.4release-1.3
  • 修复 PR:PR #7457

为什么发生

Xattr 表没有指向 node 表的外键。

SetXattr 可以在没有锁定和重新验证 inode 生命周期的情况下插入 xattr。只锁定一条不存在的 xattr 记录,也不能证明对应 inode 仍然存在。

复现步骤

  1. inode 100 存在,但 user.a xattr 不存在。
  2. T1 开始 SetXattr,准备插入 user.a
  3. T2 删除 inode 100 的最后一个 edge、node 和现有 xattr。
  4. T1 在 inode 删除后插入 user.a
  5. SetXattr 返回成功,但数据库留下孤儿 xattr。

影响

  • SetXattr 可能对已删除 inode 返回成功。
  • 孤儿 xattr 无法通过正常 namespace 操作清理。
  • SQL、Redis、TKV 的 inode 生命周期语义不一致。

解决方案

PR #7457 已统一采用 node-first 生命周期验证:

  • SQL:先锁定并验证 node,再修改 xattr。
  • Redis:WATCH inode 和 xattr。
  • TKV:在同一个 transaction 中验证 inode。
  • inode 不存在时返回 ENOENT

仍需评估稳定分支回移,并补充真实 MySQL、PostgreSQL、TiKV 双客户端交错验证。


SQL-L01:noReadOnlyTxn 存在无同步读写

  • 严重级别:Low
  • 当前状态:main 已修复
  • 影响版本:mainrelease-1.4release-1.3
  • 修复 PR:PR #7451

为什么发生

多个 goroutine 可以同时执行只读事务能力探测,并无同步地读取和写入 noReadOnlyTxn

复现步骤

  1. 两个 goroutine 同时进入 roTxn
  2. 两者同时读取 noReadOnlyTxn == false
  3. 数据库返回不支持只读事务。
  4. 两个 goroutine 同时写入 noReadOnlyTxn = true
  5. Go race detector 报告 data race。

影响

  • 存在 Go data race。
  • 只读事务能力可能被重复探测。
  • 通常不直接造成元数据损坏,但违反并发安全要求。

解决方案

PR #7451 已把该字段改为原子布尔值,并使用原子 Load/Store。

仍需评估稳定分支回移。