SQL metadata edge concurrency race analysis
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
- 当前状态:未修复
- 影响版本:
main、release-1.4、release-1.3 - 关联 PR:暂无
为什么发生
SQL sequence 或 auto-increment 的 ID 分配顺序不等于事务提交顺序。
一个事务可以先取得 ACL ID,但尚未提交。另一个客户端可能已经观察到更大的 ACL ID,却无法在自己的数据库快照中读取前一个未提交的 ACL。
当前实现会把暂时无法读取的 ACL ID 当成永久空洞,并缓存为 EmptyRule。第一个事务后续即使提交,错误缓存仍然存在。
此外,insertACL 在事务 callback 中直接更新进程内 aclCache。如果事务随后回滚或重试,数据库中并不存在该 ACL,但缓存已经对其他请求可见。
复现步骤
- T1 插入 ACL,数据库分配 ID 100,但事务暂不提交。
- T2 插入或读取到更大的 ACL ID。
- T2 在自己的快照中查询不到 ID 100。
- T2 把 ID 100 缓存为
EmptyRule。 - T1 提交,并让某个 inode 引用 ACL 100。
- T2 后续继续使用错误的
EmptyRule,不再查询数据库。
影响
- 不同客户端可能对同一个 inode 得出不同权限结果。
EmptyRule可能把本应失败的权限检查变成允许,形成权限绕过。- 事务回滚后可能留下只存在于缓存中的幽灵 ACL。
- 错误状态可能持续到进程重启或缓存失效。
推荐解决方案
- ACL 缓存只能在数据库事务成功提交后发布。
- 增加 after-commit 或 attempt-local outcome 机制。
- 不要把暂时查询不到的 ACL ID 永久缓存为
EmptyRule。 - inode 引用了不存在的 ACL 时返回
EIO,保持 fail closed。 - 增加乱序提交、事务回滚和事务重试的确定性测试。
SQL-C02:BatchUnlink 在事务重试间泄漏删除状态
- 严重级别:Critical
- 当前状态:main 已修复
- 影响版本:
main、release-1.4 - 修复 PR:PR #7460
为什么发生
数据库 rollback 只能回滚数据库状态,不能回滚事务 callback 对外部 Go map 的修改。
原实现把 delNodes 等状态创建在 m.txn 外部。第一次事务 attempt 即使失败并回滚,已经写入 map 的 inode 仍会保留。后续 attempt 成功后,旧状态可能被当成最终结果消费。
复现步骤
- BatchUnlink 第一次 attempt 把 inode 100 的
nlink计算为 0。 - 第一次 attempt 把 inode 100 写入
delNodes。 - 后续 SQL 返回可重试错误,事务回滚。
- 并发 namespace 操作使 inode 100 在第二次 attempt 中仍然存活。
- 第二次 attempt 成功提交,没有删除 inode 100。
- 第一次 attempt 留下的
delNodes[100]仍被消费。 - 后台删除仍存活 inode 100 的数据块。
影响
仍具有有效 node 和 namespace entry 的文件可能失去数据块,属于不可恢复的数据丢失风险。
解决方案
PR #7460 已将删除候选、缓存失效和统计结果改成 attempt-local 状态,只发布最终成功提交的 attempt。
可以进一步在异步删除数据块前验证:
- inode 已经不存在;
- 存在匹配的 deletion marker 或 claim;
- 删除任务属于当前有效 generation。
SQL-C03:多个 GC 重复处理同一 delayed slice
- 严重级别:Critical
- 当前状态:main 已修复
- 影响版本:
main、release-1.4、release-1.3 - 修复 PR:PR #7449
为什么发生
两个 GC 可以在 delayed slice 记录被删除前同时读取它。
虽然 chunk_ref 行锁能把两个引用扣减操作串行执行,但它不能证明当前 GC 仍然拥有 delayed row。第二个 GC 即使删除 delayed row 时影响行数为 0,原实现仍可能提交引用扣减。
复现步骤
- 一个 slice 有一个活引用和一个 delayed 引用,
refs = 2。 - GC1 和 GC2 同时读取同一 delayed row。
- GC1 把
refs从 2 减到 1,删除 delayed row 并提交。 - GC2 等待行锁后继续把
refs从 1 减到 0。 - GC2 删除 delayed row 时影响行数为 0。
- 如果仍提交引用扣减,活对象会被当成无引用对象删除。
影响
仍被有效 chunk 引用的数据对象可能被 GC 删除,造成真实数据丢失。
解决方案
PR #7449 增加 delayed row 的原子 claim,只有成功 claim 的 GC 才能扣减引用并继续清理。
仍需评估稳定分支回移,并保留多 GC 并发执行的确定性回归测试。
SQL-C04:Session cleanup 缺少 generation 和 fencing
- 严重级别:Critical
- 当前状态:未修复
- 影响版本:
main、release-1.4、release-1.3 - 关联 PR:暂无
为什么发生
Session stale 扫描和实际清理不是同一个原子状态转换。
Cleaner 扫描到一个 SID 过期后,客户端可能已经恢复并 refresh session,但 Cleaner 仍会根据旧扫描结果删除锁、sustained inode 和 session。
相同 SID 还可以被重新创建,因此 SID 无法区分旧 session generation 和新 generation。
复现步骤
- 客户端暂时暂停,被 Cleaner 扫描为 stale。
- 客户端恢复并 refresh 相同 SID。
- Cleaner 根据旧扫描结果删除客户端的 flock/plock。
- 客户端继续使用或创建新的锁。
- Cleaner 又删除 sustained inode 和 session row。
- 客户端下一次 refresh 重新创建同一个 SID。
影响
- 活跃 POSIX 锁可能被错误释放。
- open-unlinked 文件可能被当作 stale sustained inode 删除。
- 旧 Cleaner 可以删除新 session generation 创建的资源。
- 混合版本客户端会增加协议不一致风险。
推荐解决方案
- Session 增加不可复用的 generation。
- 增加
Active、Cleaning、Closed等状态。 - 使用数据库时间和条件 UPDATE 原子 claim 过期 generation。
- session-scoped 数据关联 SID 和 generation。
- Refresh 不允许复活已经失去 lease 的 generation。
- Cleanup 每一步都验证 SID、generation、state 和 owner。
- 作为混合版本协议变更评估
MinClientVersion。
SQL-C05:Namespace 缺少完整的 edge/parent 并发不变量
- 严重级别:Critical
- 当前状态:未完全修复;#7472 暂时关闭
- 影响范围:SQL metadata namespace mutation
- 关联 PR:
原 Issue 正文中的 10 项问题统一归入本组。
子问题一:读取旧 edge 后误删或误改新 edge
为什么发生
doUnlink、doRmdir、doRename 和 doBatchUnlink 会先读取 edge(parent,name),随后等待 inode 行锁。
等待期间,另一个事务可能删除或替换同名 edge。原来的 DELETE/UPDATE 主要按 (parent,name) 定位,没有验证当前记录仍然是之前读取的 inode、type 和 ID。
在 MySQL Repeatable Read 下,SELECT 可能读取旧快照,而 DELETE/UPDATE 会作用于最新已提交记录,因此可能误删另一个事务刚创建的新 edge。
复现步骤
- T1 读取
/a -> inode 100。 - T1 等待 inode 100 的行锁。
- T2 删除
/a,重新创建/a -> inode 300并提交。 - T1 执行
DELETE WHERE parent=? AND name='a'。 - T1 删除的是新 edge
/a -> inode 300。 - T1 随后仍按旧 inode 100 更新 nlink、统计和回收状态。
影响
- 并发 unlink/rename 下误删其他客户端新创建的文件。
- nlink、目录统计和 quota 按错误 inode 结算。
- 产生悬挂 edge、孤儿 inode 或 namespace 丢失。
推荐解决方案
由 PR #7476 实现:
- 使用完整身份
(id,parent,name,inode,type)定位旧 edge。 - DELETE/UPDATE 必须检查 affected rows 恰好为 1。
- 身份不一致时返回可重试错误,让事务在新快照上重跑。
- 覆盖 unlink、rmdir、rename replace/exchange 和 BatchUnlink。
- BatchUnlink 对重复 name 去重,避免恒定 affected-row mismatch。
子问题二:rmdir 与并发 child 插入没有共同锁
为什么发生
rmdir 会锁定目标目录 inode,并检查目录中没有 child edge。
但 Mknod、Link、rename-in、Clone 和 BatchClone 原来不一定锁定目标 parent。尤其在 SkipDirMtime 窗口内,创建普通文件可能不会更新 parent,也无法通过 parent UPDATE 的 affected rows 发现目录已被删除。
复现步骤
/d是空目录。- T1 执行
rmdir /d,锁定node(d)并确认没有 child edge。 - T2 执行
create /d/f。 - T2 没有参与 parent 生命周期锁,插入 child edge。
- 因为
SkipDirMtime未到期,T2 跳过 parent update。 - T1 删除
/d的 edge 和node(d)。 - 最终留下 parent inode 不存在的
/d/fedge。
影响
- 产生无法从 root 遍历的孤儿文件或孤儿子树。
- 用户无法通过正常路径访问或删除。
- inode、目录统计和 quota 不一致。
- 对象数据可能长期无法回收。
推荐解决方案
由 PR #7472 尝试实现:
- 建立统一的 parent 生命周期锁协议。
- 向目录插入 child edge 时锁定目标 parent。
- rmdir 判空并删除目录时持有排他 parent/target 锁。
- 多 inode 操作按稳定顺序加锁,避免交叉死锁。
- 加锁后重新读取并验证 node 状态。
- 大小写不敏感模式中的“检查后插入”应使用排他 parent 锁。
- 批量路径不能退化为每个 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。
复现步骤
- T1 按
(inode A, inode B)获取锁。 - T2 按
(inode B, inode A)获取锁。 - T1 持有 A 等待 B。
- T2 持有 B 等待 A。
- 数据库检测到死锁并回滚其中一个事务。
- 高并发下持续发生死锁和重试。
影响
- namespace 操作延迟明显增加。
- 大量死锁回滚形成重试风暴。
- 数据库压力升高,操作最终可能以 EIO 失败。
推荐解决方案
- 收集事务涉及的全部 inode。
- 去重并合并锁强度。
- 共享锁与排他锁冲突时升级为排他锁。
- 按稳定的 inode 顺序获取锁。
- 对 BatchClone/BatchUnlink 进行真实数据库性能测试,避免 N+1 查询。
子问题四:trash 决策在事务重试和多客户端之间失效
为什么发生
trash 值在事务外通过 checkTrash 得到,事务内又可能被降级为 0。如果事务重试,第一次 attempt 修改过的变量可能泄漏到下一次 attempt。
subTrash 还会在客户端进程内缓存。另一个客户端删除对应 trash 目录后,当前客户端可能继续把 edge 挂到已经不存在的 parent inode 下。
复现步骤
场景 A:事务重试状态泄漏
- 第一次事务 attempt 选择把文件移入 trash。
- attempt 内某个修复分支把
trash改成 0。 - 事务遇到冲突并重试。
- 第二次 attempt 继承
trash=0。 - 本应进入回收站的文件被直接永久删除。
场景 B:失效 trash 缓存
- Client A 缓存当前
subTrashinode。 - Client B 清理并删除该 trash 子目录。
- Client A 继续使用旧 inode。
- 文件 edge 被挂到已经不存在的 parent 下。
影响
- 文件绕过回收站,被意外永久删除。
- 产生无法正常访问和清理的孤儿元数据。
- SQL、Redis 和 TKV 都可能受相关状态一致性问题影响。
推荐解决方案
由 PR #7477 尝试实现:
- 保存
requestedTrash,每次事务 attempt 开始时重新初始化trash。 - 在事务内验证 trash node 仍然存在。
- Redis 使用 WATCH,TKV 使用事务读取。
- SQL 与 PR #7472 合并后使用锁定读取。
- 删除缓存对应的 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
- 当前状态:未修复
- 影响版本:
main、release-1.4、release-1.3 - 关联 PR:暂无
为什么发生
Attach 和 Cleanup 都把 detached marker 的存在当作所有权,但没有对同一 marker generation 执行原子 claim。
Cleanup 会跨多个事务递归清理。Attach 只锁 destination parent,无法阻止已经开始的 Cleanup 继续删除刚刚 Attach 成功的 subtree。
复现步骤
- Cleanup 扫描到 detached inode 500 的过期 marker。
- Cleanup 开始处理 inode 500。
- Clone 完成,Attach 发布指向 inode 500 的 edge。
- Attach 删除 marker 并提交。
- Cleanup 没有重新验证 marker 所有权。
- Cleanup 删除 inode 500 及相关元数据。
- 新发布的 edge 指向不存在的 inode。
影响
- 已成功 Attach 的目录或 Clone subtree 可能消失。
- 产生 dangling edge。
- 多个 Cleaner 可能重复扣减 counter。
- Cleaner 崩溃后留下无法安全恢复的半清理状态。
推荐解决方案
- Detached marker 增加 state、generation、owner 和 lease expiration。
- Attach 和 Cleanup 原子 claim 同一 generation。
- 发布 edge 或删除 node 前重新验证 claim。
- Cleanup 设计为可恢复、可重试、幂等。
- 只有最终状态转换成功后才能更新统计。
SQL-H02:Dump/Backup 没有使用同一个一致性快照
- 严重级别:High
- 当前状态:未修复
- 影响版本:
main、release-1.4、release-1.3 - 关联 PR:暂无
为什么发生
namespace tree、node、chunk、xattr 和 ACL 可能由不同数据库事务读取,因此来自不同快照。
Fast dump 还把一次调用的 snapshot 状态保存在共享的 dbMeta.snap 字段中,并发 DumpMeta 调用可能相互覆盖或清空状态。
多线程 binary dump 已明确警告:数据库可写时不能保证一致性,而 CLI 默认会使用多个线程。
复现步骤
- 外层 dump transaction 读取
/a -> inode 100。 - 并发客户端删除或替换 inode 100。
- dump worker 使用另一个 transaction 读取 inode 100 的 metadata。
- worker 观察到与 namespace tree 不同的数据库状态。
- 输出中可能存在没有 node/chunk 的 edge,或者缺少必要元数据。
并发 fast dump 场景:
- Dump A 设置
m.snap = snapshotA。 - Dump B 设置
m.snap = snapshotB。 - Dump A 或 B 清空
m.snap。 - 另一个 dump 继续读取错误或空 snapshot。
影响
- 格式合法的备份不一定能够一致恢复。
- 恢复后可能缺少 node、chunk、xattr 或 ACL。
- 并发 dump 可能出现 data race、panic 或不完整输出。
- 自动备份可能在没有明确失败的情况下生成错误备份。
推荐解决方案
- 一次 dump 调用使用同一个 Repeatable Read transaction/snapshot。
- Snapshot 状态作为调用级参数传递,不能存放在共享
dbMeta字段。 - 在共享快照方案完成前,SQL dump 和自动备份强制使用单线程。
- 使用统一的 error/cancel owner,任意 worker 失败都让整个 dump 失败。
- 在持续 namespace mutation 下执行 dump/load,并验证恢复后的元数据不变量。
SQL-M01:在线 repair/sync 会覆盖并发增量
- 严重级别:Medium
- 当前状态:未修复
- 影响版本:
main、release-1.4;release-1.3部分路径 - 关联 PR:暂无
为什么发生
Repair 使用“先扫描、再写入绝对值”的方式。
扫描过程中正常 writer 仍然可以更新 counter、quota 和目录统计。Repair 最终写入扫描得到的旧绝对值时,会覆盖扫描期间产生的新增量。
只锁定最终 counter 行不能保护之前的全量扫描。
复现步骤
- Repair 扫描得到
usedSpace = 100。 - 客户端创建文件,把真实值增加到 110。
- Repair 完成扫描。
- Repair 把旧值 100 写回数据库。
- 文件仍存在,但对应 usage delta 永久丢失。
影响
- Volume counter 不准确。
- Directory statistics 不准确。
- Quota usage 与真实数据不一致。
- 目录 nlink 在 repair 成功后仍可能错误。
推荐解决方案
- 短期:repair 期间获取全局维护写锁,阻止正常 mutation。
- 长期:增加 maintenance epoch、write fence 或 mutation watermark。
- Repair 写入时使用 revision/epoch CAS。
- 合并扫描开始后产生的增量,而不是直接覆盖绝对值。
- 发现并发 mutation 时放弃结果并重新扫描。
SQL-M02:多客户端 quota 没有原子 reservation
- 严重级别:Medium
- 当前状态:未修复
- 影响版本:
main、release-1.4;release-1.3的 filesystem/directory quota - 关联 PR:暂无
为什么发生
每个客户端使用自己的 quota usage 缓存进行检查。
多个客户端可以同时通过同一个 hard limit 检查,各自提交 metadata mutation,之后再异步 flush quota delta。
最终数据库 usage 可能正确,但检查发生得太早,无法实现严格 hard limit。
复现步骤
- Quota limit 为 100,当前 usage 为 90。
- Client A 申请使用 8。
- Client B 同时申请使用 8。
- 两个客户端都根据本地缓存计算得到 98。
- 两个 mutation 都成功提交。
- 后续 delta flush 后 usage 变为 106。
影响
Filesystem、directory、user 和 group quota 都可能突破声明的 hard limit。
推荐解决方案
- 在 metadata mutation transaction 内执行权威 reservation。
- 多个 quota 维度按稳定顺序锁定 quota rows。
- Metadata mutation 回滚时 reservation 一并回滚。
- 本地缓存只用于展示和快速拒绝,不能作为最终 enforcement。
- 如果不提供严格 reservation,应把文档改为 eventually enforced limit。
- 旧客户端可能绕过新协议,需要升级门禁或
MinClientVersion。
SQL-M03:Format/config 更新没有 CAS,并发布未提交状态
- 严重级别:Medium
- 当前状态:未修复
- 影响版本:
main、release-1.4、release-1.3核心路径 - 关联 PR:暂无
为什么发生
多个客户端可以读取同一份 Format JSON,分别修改不同字段,然后无 revision 条件地覆盖整个 value。
进程内 Format 对象还可能在数据库事务提交前发布。如果事务失败,当前进程使用的配置与数据库配置不一致。
部分代码还会直接修改共享 Format 指针,存在 data race 风险。
复现步骤
- Client A 和 Client B 同时读取 Format F0。
- A 修改
TrashDays。 - B 启用
DirStats。 - A 写入 FA。
- B 基于旧 F0 写入 FB。
- B 的整段 JSON 覆盖 A 的修改。
未提交发布场景:
- 事务内调用
setFormat更新内存对象。 - 后续数据库操作失败并回滚。
- 当前进程仍然暴露未提交配置。
影响
- 并发 config 命令静默丢失更新。
- 不同客户端使用不同配置。
- Feature cleanup 与 flag 持久化可能只完成一半。
- 共享 Format 修改可能产生 Go data race。
推荐解决方案
- 为 setting 增加 revision,并使用 CAS。
- 或在同一个写事务内锁定、重新读取和更新 Format。
- 已发布的 Format 对象保持不可变,使用 copy-on-write。
- 只有数据库提交成功后才能发布新对象。
- Feature cleanup、setting update 和 changelog 放在同一原子边界。
- 增加混合版本保护,阻止旧客户端继续整段覆盖 JSON。
SQL-M04:Xattr mutation 没有参与 inode 生命周期锁
- 严重级别:Medium
- 当前状态:main 已修复
- 影响版本:
main、release-1.4、release-1.3 - 修复 PR:PR #7457
为什么发生
Xattr 表没有指向 node 表的外键。
SetXattr 可以在没有锁定和重新验证 inode 生命周期的情况下插入 xattr。只锁定一条不存在的 xattr 记录,也不能证明对应 inode 仍然存在。
复现步骤
- inode 100 存在,但
user.axattr 不存在。 - T1 开始 SetXattr,准备插入
user.a。 - T2 删除 inode 100 的最后一个 edge、node 和现有 xattr。
- T1 在 inode 删除后插入
user.a。 - 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 已修复
- 影响版本:
main、release-1.4、release-1.3 - 修复 PR:PR #7451
为什么发生
多个 goroutine 可以同时执行只读事务能力探测,并无同步地读取和写入 noReadOnlyTxn。
复现步骤
- 两个 goroutine 同时进入
roTxn。 - 两者同时读取
noReadOnlyTxn == false。 - 数据库返回不支持只读事务。
- 两个 goroutine 同时写入
noReadOnlyTxn = true。 - Go race detector 报告 data race。
影响
- 存在 Go data race。
- 只读事务能力可能被重复探测。
- 通常不直接造成元数据损坏,但违反并发安全要求。
解决方案
PR #7451 已把该字段改为原子布尔值,并使用原子 Load/Store。
仍需评估稳定分支回移。
Source: juicedata/juicefs