Table of Contents

VersionedMetadata 使用指南——版本链 N=2 轮转回收 / 单值元数据

给谁看:需要跨重启单值状态(水位/配置/锚点)的组件开发者 回答什么:VersionedMetadata 是什么 / Settings / 版本链 N=2 / 同步 vs 异步持久化 / 外部隔离传输 本篇只讲结构本身——Meta 横切协议(Disabled/Managed/Transport + IMetaTransport)见 meta.md 定位:Structures/ 6 子族之一,搭配件(与 Ring/Log 主结构搭配,跨重启单值状态)


0. 一句话总纲

VersionedMetadata = 版本链追加式单值 + N=2 轮转回收 + 崩溃安全(写一半 = 旧版本完好)

  • 单值:每次 Write 把整条 payload 当一个版本追加到磁盘版本链,链头即当前值;
  • 内存工作副本(N≥2 对齐槽)让 Read 零 IO、Abort 零 IO(回退到上一版本);
  • 落盘 = 引擎 Allocate + Write + Flush,崩溃写到一半 = 链头 magic/CRC 不匹配 = 旧版本完好(断链天然容错);
  • PayloadSize 须 ≥ 托管块上界(3a 外部隔离传输场景,见 §2、§8 反模式 1)。

1. 定位

维度 内容
与主结构的关系 与 Ring/Log 主结构搭配——承载跨重启的单值状态(如 Ring 的 CommittedTailAddress 锚点、Session 的检查点水位、TimeSeries 的 dense 块水位)
与 Meta 横切件的关系 VersionedMetadata 是 Meta 横切件在结构层的具体实现——结构自身的水位(HighestVersionAddress 等)经 Meta 横切件持久化;横切协议(三模式 + IMetaTransport)见 meta.md
与 Index 的差异 Index 派生数据(可重建、判等闭环、依赖真相源);VersionedMetadata 是单值状态本体(追加流版本链,N=2 轮转回收)
与 Mirror 的差异 Mirror 是字节镜像(不知尺寸的整流式 AppendChunk);VersionedMetadata 是定长/变长单值(每次 Write 一条 record)

两种消费形态

  1. 直接用——跨重启单值状态:构造 + Initialize + WaitForReady,按 Write/Read/Persist 用。
  2. 3a 外部隔离传输——把别的结构的 meta 块托管到 VersionedMetadata:用内置适配器 MetadataMetaTransport(源码 Meta/MetadataMetaTransport.cs),勿自写单槽文件(见 §8 反模式 2)。

为什么单值用版本链而不是单槽覆盖写

  • 崩溃安全免费——单槽覆盖写要自理 torn write(写一半崩 = 数据撕裂),版本链追加 + 链头 magic/CRC 校验天然容错;
  • Abort 零 IO——回退上一版本只切内存镜像,不触盘;
  • 空间有界——N=2 轮转回收(ReclaimOldVersions),不会无界增长;
  • 跨结构复用——VersionedMetadata 自身实现 ITransactionParticipant,可注册进 TransactionLog 跨结构原子提交(3a 场景直接用 ext.Storage)。

2. Settings 全参数

VersionedMetadataSettings(继承 MetadataSettingsSettings 基类公共 meta 配置):

字段 类型 默认 说明
MainEngine StorageEngineOptions "tc.metadata",单段 16MB,不分段,不预分配 主引擎选项。Metadata 版本链默认单段稀疏按需增长(元数据是结构体级小数据)
PayloadSize int 必填 元数据结构体字节数(写入 payload 大小,自动向上对齐到扇区)。3a 托管场景须 ≥ meta 块上界(12B 头 + 水位 struct + MetaOpaqueBytes + 4B 尾)
MaxPayloadSize int? null 变长档写入上限。null = 固定档(Write 截断/补零到 PayloadSize);非 null 且 > PayloadSize = 变长档(Write 允许 ≤ 上限任意长度,record 按实际长度落盘,超上限 fail-fast 抛)
MaxMemoryVersions int 2 内存多版本保留窗口(N≥2 是 Abort 零 IO 底线,可配更高支持 MVCC)。构造时强制 Math.Max(2, value)
MetaPolicyKind MetaPolicyKind Disabled meta 横切件三模式:Disabled(扫盘恢复)/ Managed(独立 .meta 引擎)/ Transport(外部 IMetaTransport)——见 meta.md §1
MetaOpaqueBytes int 0 外部 opaque 区容量(写侧约束,不参与盘上几何)。3a 托管场景按调用方需要配置

跨重启改 PayloadSize 合法:恢复载入的历史版本按其盘上真实 PayloadLength 完整交付(不补零、不截断);本次运行的新版本按新 PayloadSize 几何落盘。版本链混尺寸由各 record 头部自述几何支撑。

MaxPayloadSize 何时用:keyed 分区元数据(如 TimeSeries dense 水位块——n 随序列增长,每次 Write 的块长动态)。固定档消费者(Queue/Blob 等)逐字节行为不变。


3. 构造与生命周期

VersionedMetadata 无 Create 工厂——显式三步:构造(= 配置,零 IO)→ Initialize(同步启动后台恢复,立即返回)→ WaitForReady(观测等待恢复完成)。Dispose 幂等。

3.1 直接消费(最常见)

using TC.Tier.Core.IO;
using TC.Tier.Runtime.Structures.Metadata;

// ① 文件系统(介质平权——换介质 = 换一根 spec)
using var fs = TierFs.New("memory:");   // 或 "local:///path/to/dir"

// ② 配置(PayloadSize 必填;其余默认值即生产值)
var settings = new VersionedMetadataSettings(
    new StorageEngineOptions("session.ckpt", 16L * 1024 * 1024, enableSegmentation: false))
{
    PayloadSize = 64,                  // 单值结构体字节数
    MetaPolicyKind = MetaPolicyKind.Managed,   // O(1) 恢复水位;Disabled 走扫盘
};

// ③ 构造 + 启动 + 等就绪
using var md = new VersionedMetadata(fs, settings);
md.Initialize();                       // 同步 void,启动后台恢复后立即返回
md.WaitForReady();                    // 观测等待恢复完成(载入链头版本到内存镜像)

3.2 Write + Read + Abort(写读编排)

// ① 写——返回版本号(单调递增);本调用按落盘策略决定是否立即 flush
long v1 = md.Write(stackalloc byte[] { /* 64B 结构体 */ });
//   v1 == 1(首次 Write 推进到 1;恢复载入过历史版本则从载入版本号续推)

// ② 读——零 IO;首次 Write 前 = 加载版本(盘上真实大小),Write 后 = 现热区
Span<byte> buf = stackalloc byte[64];
int n = md.Read(buf);                  // n = min(当前内容长度, buf.Length)

// ③ 零拷贝视图——仅 epoch 存活期内有效;跨 await 必须改走 Read(dst)
Span<byte> view = md.AsSpan();         // 同步栈内合法
ref var head = ref md.GetRef<long>();  // 强类型首字段引用(热路径零校验)

// ④ 显式持久化点——不注入 SyncPersistencePolicy 时 Write 不自动落盘,需手动触发
md.Persist();                          // 把内存镜像追加为版本链新版本 + flush + 写 meta

// ⑤ 2PC 编排(与 TransactionLog 协同——Prepare 即持久化点)
const long seq = 100;
md.Prepare(seq);                       // 追加新版本 + flush + 写 meta(LastPreparedSeq = seq)
md.ConfirmCommitted(seq);              // CAS 推进 LastCommittedSeq;新版本正式成为当前
//   失败路径:
md.Abort(seq);                         // 内存回退到上一版本(零 IO)+ ReclaimTail 回退悬干新版本

// ⑥ 头截断(后台周期调)——保留最近 N=MaxMemoryVersions 个版本,更老的从链尾方向回收
md.ReclaimOldVersions();

3.3 异步启动(注入恢复 hints)

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var hints = new MetadataRecoveryHints
{
    HighestVersionAddress = knownHead,  // 上层最强知识——优先级最高(> meta > 扫盘)
    LastCommittedSeq = knownSeq,
};
md.Initialize(hints);                          // 恢复核心:启动主引擎就绪 → meta.Load → 三级回退
await md.WaitForReadyAsync(cts.Token);

3.4 Dispose

// 显式释放(幂等);底层引擎、meta 引擎、epoch、热区对齐内存、缓冲池一并释放
md.Dispose();
// 或异步释放(MetaPolicy 走异步轨)
await md.DisposeAsync().ConfigureAwait(false);

生命周期与全部 Structures 同构(详见 src/TC.Tier.Core/docs/lifecycle.md):OnInitializeBegin 启动主引擎 +(Managed 模式)并行启动 meta 引擎;恢复核心 WaitForDependenciesAsync 双 await 引擎就绪 → MetaPolicy.LoadAsync → 三级回退定位链头 → 加载链头版本到内存镜像。

变长档写读MaxPayloadSize 启用后 Write 允许 ≤ 上限的任意长度,record 按实际长度落盘。CurrentPayloadLength 探测当前内容长度(固定档恒 PayloadSize;变长档随块增长)后分配读缓冲;AsSpan()/GetSpan<T>() 自动按当前实际长度切片。

3.5 文件系统与介质

  • IFileSystem 经组合根 TierFs 注入——介质平权:memory: / local:///path / s3://bucket 等 spec 切换零代码改动;
  • 引擎名隔离同 fs 子目录——多结构同 fs 时引擎名须唯一(如 "my-log" / "my-log.meta");
  • DeleteOnClose:跨重启组合(如 3a 托管)须 false(默认),否则 Dispose 时清盘;测试场景可设 true 自动回收;
  • SectorSize_fs.Volume 静态属性读取——构造期可用,padding/热区分配安全。

4. API 详表

API 签名 说明
long Write(ReadOnlySpan<byte> data) data 写入内存镜像,推进版本号,按落盘策略触发持久化。固定档:长截短补;变长档:超 MaxPayloadSize 抛。返回新版本号(单调递增;恢复后从载入版本续推)
持久化 void Persist() 强制把内存镜像作为新版本追加到磁盘版本链 + flush + 写 meta。内容未变跳过(防重复/防缩容零覆写)
读(拷贝) int Read(Span<byte> dst) 零 IO 读当前内容。加载版本优先(首次 Write 前按盘上真实大小交付);Write 后 = 热区。返回实际读取字节数
读(零拷贝) Span<byte> AsSpan() 当前内容 0-copy Span 视图。仅 epoch 存活期内有效——并发写/Abort 可能使其失效
读(强类型引用) ref T GetRef<T>() where T : unmanaged 当前内容首字节按 T 重解释的引用(热路径 GetRefUnsafe 零校验)
读(强类型 Span) Span<T> GetSpan<T>() where T : unmanaged 按 T 重解释的 Span。要求内容大小与 sizeof(T) 对齐——历史大小 ≠ PayloadSize 时可能抛
热路径读 int ReadNoEpoch(Span<byte> dst) 不含 epoch 进出的读(供已持 epoch 的 scope/batch 内调)。裸调危险
当前长度 int CurrentPayloadLength { get; } 加载版本按盘上真实长度 / 热区按最近 Write 长度。分配读缓冲前探测用
水位·链头 LogicalAddress HighestVersionAddress { get; } 链头/最新版本地址
水位·链尾 LogicalAddress LowestVersionAddress { get; } 链尾/最老版本地址(头截断回收边界)
水位·版本号 long CurrentVersion { get; } 当前版本号(单调递增,0 = 空链/无数据)
扇区大小 uint SectorSize { get; } 引擎扇区大小(padding/对齐计算用)
头截断 void ReclaimOldVersions() 回收旧版本磁盘空间——保留最近 N=MaxMemoryVersions 个版本,更老的从链尾方向 ReclaimHead(epoch 保护:等 readers 退出后回收)
Opaque 写 void SetOpaqueMeta(ReadOnlySpan<byte> data) 登记外部 opaque meta 进策略缓冲,随下次水位提交原子落盘。Disabled 抛 InvalidOperationException;超 MetaOpaqueBytesArgumentException
Opaque 读 ReadOnlySpan<byte> ReadOpaqueMeta() 读最近已提交块内的 opaque 字节视图。无数据/Disabled 时为空 Span
2PC 见下表 ITransactionParticipant 六件套

2PC 六件套ITransactionParticipant,对齐 Log/Ring 范式,独立协议与数据写正交):

成员 语义
Prepare(long seq) / PrepareAsync(long seq, CancellationToken ct) 记录回退快照 → 追加新版本到磁盘链头 + flush + 写 meta。内容未变跳过追加。数据 flush 原生同步,异步版仅 meta 走 MetaPolicy.CommitAsync
ConfirmCommitted(long seq) CAS 推进 LastCommittedSeq + 持久化 meta(刷新水位)+ 清空 Abort 回退快照 + 触发回调。seq ≤ current 时 no-op
Abort(long seq) / AbortAsync(long seq, CancellationToken ct) 内存窗口回退到上一版本(零 IO)+ 尾截断 ReclaimTail 回退悬干新版本 + flush + 写 meta。幂等——恢复时可能对同一 seq 多次调
OnCommitted(long seq, Action callback) 注册提交回调(链式触发)。已提交到更高 seq 时立即同步触发
long LastCommittedSeq { get; } 当前已提交 seq(恢复/运行时读)
long LastPreparedSeq { get; } 最近 Prepare 的 seq。恢复裁决悬干用(> 协调者 committedSeq = 悬干,丢弃)

5. 自定义注入工厂

构造参数支持注入(默认实现满足绝大多数场景;替换走命名委托/接口注入,禁匿名 lambda):

注入项 接口/类型 默认实现 注入方式
Codec IMetadataCodec VersionedMetadata.Codec(私有嵌套 sealed,JIT 去虚化) protected MetadataBase 构造第一参数——子类继承传
恢复算法 IRecovery<MetadataRecoveryHints> DefaultMetadataRecovery(三级回退:hints → meta → 扫盘) 构造参数 recovery
Meta 策略工厂 MetaPolicyFactory<MetadataMetaHeader, MetadataMetaPayload> CreateMetaPolicyDefault(按 MetaPolicyKind 映射 Managed/Transport/Disabled) 构造参数 metaPolicyFactory——命名委托,禁匿名 lambda
Meta 传输 IMetaTransport Transport 模式回落 MetaHost(meta 块作为 IS_META record 嵌入版本链流) 构造参数 metaTransport——3a 外部隔离用 MetadataMetaTransport 或自实现
Epoch LightEpoch 内部 new 自管(进 Resources 统一释放) 构造参数 epoch——多结构共享同一实例时注入
持久化策略 IPersistencePolicy 不注入 = 写路径不自动落盘(仅 Prepare/Persist 强制 flush) 构造参数 persistencePolicy——SyncPersistencePolicy / AsyncPersistencePolicy
Logger ILogger null 构造参数 logger

Contracts 类型Structures/Metadata/Contracts/):

类型 角色
MetadataRecordFields 版本 record 业务字段包(Flags/PayloadLength/PaddingLength/PreviousVersion/MetadataVersion),codec 读写 header 时传参
MetadataMetaHeader meta 水位 Header(12B 纯规范:Magic/Version/Flags/PayloadLength/PaddingLength),走 [BinaryLayout] 源生成 codec
MetadataMetaPayload meta 水位 Payload(48B:HighestVersionAddress/LowestVersionAddress/LastCommittedSeq/LastPreparedSeq)
MetadataRecoveryHints 恢复提示(HighestVersionAddress? / LastCommittedSeq?),外部主动注入——最高优先级
IMetadataCodec 版本 record codec 接口(HeaderSize/Magic/WriteHeader/TryReadHeader/FillCrc/VerifyCrc/ReadVersion 等)
IPersistencePolicy / SyncPersistencePolicy / AsyncPersistencePolicy 落盘策略——见 §6

6. 持久化机制

6.1 版本链 N=2 轮转回收

  • 磁盘:版本链追加流——每次落盘 = Allocate(recordSize).Start + Write(addr, span) + Flush。record 几何 = [Header 42B][Payload 实际长][Padding 扇区对齐],CRC32C in Header(覆盖 Header 除 Crc 自身 + Payload + Padding)。
  • 内存:N 个对齐槽(AlignedMemoryManager,pinned native,零 GC)。Write 在覆盖 [0] 前先把 [0] 滑到 [1](保留为 Abort 零 IO 回退源)。N≥2 是底线——MaxMemoryVersions 强制 Math.Max(2, value)
  • 轮转回收ReclaimOldVersionsMaxMemoryVersions 保留窗口——[keepAddr, highest] = 最近 N 个版本,更老的从链尾方向 ReclaimHead。epoch 保护(BumpCurrentEpoch 等 readers 退出后回收)。
  • 逐 record 自身几何推进:链上 record 尺寸可能不同(PayloadSize 跨重启变更后新旧混尺寸),扫描/回收按每条 record 头部自述的 PayloadLength+PaddingLength 跳进——统一 record 步进会落在旧 record 中段,断链静默丢数据

版本 record 42B Header 布局MetadataHeader,源生成器 [BinaryLayout] 生成 codec):

偏移 长度 字段 说明
0 4 MagicValue 魔数 = RecordMagic.VersionedMetadata,块身份校验
4 2 Version 头版本号(major=1, minor=0),ValidEquals 校验
6 2 Flags DefaultFlags = FLAG_CRC32C \| FLAG_PAYLOAD_4B;meta record 叠加 FLAG_ENTRY_IS_META
8 4 PayloadLength 元数据结构体字节数(payload 实际长度)
12 2 PaddingLength 扇区对齐填充字节数
14 16 PreviousVersion 版本链指针,指向上一版本(链尾为 LogicalAddress.Empty
30 8 MetadataVersion 版本号(单调递增;调用方按版本号寻址/恢复定位)
38 4 Crc CRC32C(覆盖 Header 除 Crc 自身 + Payload + Padding)

6.2 同步 vs 异步持久化策略对比

策略 ShouldPersist(version) 何时落盘 适用 代价
不注入(默认) (不调) Prepare / Persist 显式触发 上层用 2PC 编排提交点(如 Session) Write 不自动落盘——崩在 Prepare 前 = 丢失
SyncPersistencePolicy 恒 true 每次 Write 立即 AppendVersionToDisk + flush 单值独立持久化(不参与 2PC) 每次 Write 强制 IO(最高一致性,最低吞吐)
AsyncPersistencePolicy 恒 false 后台批量 flush / Prepare / Persist 显式触发 高频写、容忍批量延迟落盘 后台线程批量;崩在 flush 前 = 丢失未提交版本

Prepare 无论何种策略都强制 flush——2PC 协议要求 Prepare 即持久化点(WriteAsync 不保证落盘)。

6.3 内存热窗口(Abort 零 IO 的关键)

Write 在覆盖热区 [0] 前,先把 [0] 滑到 [1],使 [1] 始终持有上一版本镜像:

[0] = 当前版本(最近 Write 的目标)
[1] = 上一版本(Abort 零 IO 回退源;写覆盖前的镜像)
[2..N-1] = 更老版本(MaxMemoryVersions > 2 时支持 MVCC)
  • 拷贝顺序从高索引往低索引[N-1] ← [N-2] ← ... ← [1] ← [0]),避免 [0]→[1] 覆盖 [1] 旧值前未读到;
  • 变长档槽增长EnsureHotSlotSize(needed) 按需重分配整组槽(max(needed, 当前×2) 对数摊薄,上限 _hotCapacity),搬移既有镜像;固定档恒跳过(槽尺寸 = PayloadSize);
  • 冷热分离:恢复载入的历史版本按盘上真实 PayloadLength 从自持 PinnedBufferPool 租借只读缓冲,不进按当前配置分配的读写热区——历史大小 ≠ 当前 PayloadSize 时不补零、不截断;首次 Write 后当前内容切到热区,加载版本归还池(两次 Write 后不可达);
  • Abort 回退分支:会话内有 Write(_currentVersion > _baseVersion)时——两次以上写走热-热回退([1]→[0]);单次写且前置 = 加载版本走冷-热回退(_serveLoaded = true,回到恢复载入的历史版本完整大小)。

6.4 崩溃安全

天然容错——无需 WAL:

  • 写到一半(torn write):链头 record magic/CRC 不匹配 → 恢复时 TryReadHeader 返回 false → 链断在上一合法 record(旧版本完好);
  • 内存镜像未落盘:恢复时从盘上链头载入(旧版本);
  • Prepare 后 Commit 前:磁盘链头是 Prepare 追加的新版本,但 LastCommittedSeq 未推进——恢复时 LastPreparedSeq > LastCommittedSeq 即悬干,Abort 触发尾截断 ReclaimTail 回退(按 record 自身几何 + _lastAppendedRecordSize)。

6.5 外部隔离传输(3a 托管)

把别的结构的 meta 块托管到 VersionedMetadata——用内置适配器 MetadataMetaTransport

using TC.Tier.Core.IO;
using TC.Tier.Runtime.Meta;
using TC.Tier.Runtime.Structures;
using TC.Tier.Runtime.Structures.Metadata;

using var fs = TierFs.New("local:///var/lib/myapp");

// ★ PayloadSize 须 ≥ meta 块上界:12B 头 + 水位 struct + MetaOpaqueBytes + 4B 尾
//   MetadataMetaPayload = 48B;按调用方 MaxMetaOpaqueBytes 估算后留余量更稳
var hostSettings = new VersionedMetadataSettings(
    new StorageEngineOptions("my-log.meta", 1L << 20, enableSegmentation: false))
{
    PayloadSize = 4096,                       // 留足余量(远大于 64 + MetaOpaqueBytes)
    MetaPolicyKind = MetaPolicyKind.Disabled, // 托管结构自身水位走扫盘(无嵌套 meta)
};

// 构造 + 启动(适配器内部 Initialize;首次读写 WaitForReady 就绪)
using var ext = new MetadataMetaTransport(fs, hostSettings);

// 注入到 Log/Ring/Mirror 等主结构的 settings.MetaPolicyKind = Transport 时使用
var logSettings = new EntryLogSettings(
    new StorageEngineOptions("my-log", 64L << 20)) { MetaPolicyKind = MetaPolicyKind.Transport };
using var log = new EntryLog(fs, logSettings, metaTransport: ext);

// ★ 适配器语义:WriteBlock = Write + Persist + flush + ReclaimOldVersions(N=2 轮转有界)
//   ReadLastBlock = 内存镜像零 IO 读 + 按统一布局自述裁剪为变长精确块
//   需要 2PC 跨结构原子提交时经 ext.Storage(VersionedMetadata 自身已实现 ITransactionParticipant)

WriteBlock 即 meta fsync 点(data 在调用链先行落盘);3a 语义下主结构主流水位线零影响——meta 块写入与主结构引擎完全解耦(详见 meta.md §6.3)。


7. 恢复协议

DefaultMetadataRecovery(继承 RecoveryBase<MetadataRecoveryHints> 模板,三级回退——对齐 meta.md §6.2 全结构统一优先级):

① WaitForDependenciesAsync:等主引擎 + meta 引擎(Managed 模式)双 await 全异步轨就绪
② MetaPolicy 装配(恢复核心钩子内——依赖引擎就绪)
③ meta.LoadAsync(水位 O(1);Disabled no-op)
④ 三级回退定位链头:
   1. hints(外部注入 HighestVersionAddress——最高优先级)
   2. meta payload(HighestVersionAddress + LastCommittedSeq + LowestVersionAddress——O(1))
   3. 扫盘按版本号定位 Head(无 meta / meta 损坏兜底)
⑤ LoadVersionToMemory(head):按 record 自身 PayloadLength 从自持 PinnedBufferPool 租借只读缓冲
   ——历史大小 ≠ 当前 PayloadSize 时不补零不截断,完整交付

扫盘:从 MinAddress 正向扫,按每条 record 自身 PayloadLength+PaddingLength 跳进,magic 不匹配 = 链结束。最后一条合法数据 record = 链头(最高地址 = 最新写入)。跳过 IS_META record(Transport 自流嵌入模式的 meta block 不参与数据版本号定位,几何跳进照常)。

链头判定细节

  • 不用地址值 == LogicalAddress.Empty 判断"有没有找到"——Empty 是合法地址(地址空间起点,首版本链头就是 Empty);用 headFound 布尔标志;
  • 链头 = 最高地址的数据 record——不用版本号比较:同一版本号可能被多次持久化(Write 落盘 + Prepare 再落盘),版本号相同但地址递增,最新地址才是当前链头;
  • 扫盘 64KB 页步进ScanProbePageSize)用于 magic 首扫定位起点;几何跳进后逐 record 头校验;最大扫盘 64MB 防失控。

与 Meta 横切件配合

  • 结构自身水位(HighestVersionAddress 等)经 Meta 横切件持久化(MetaPolicyKind 三模式);
  • 3a 场景下 VersionedMetadata 自身被 MetadataMetaTransport 包成别的结构的 meta 介质——结构自身的水位走 Managed(嵌套 .meta.meta 引擎)或 Disabled(扫盘)。

8. 反模式

# 反模式 后果 / 正确做法
1 PayloadSize < 托管块上界(3a 场景) WriteBlockArgumentException(fail-fast 不截断)。正确:PayloadSize ≥ 12B 头 + 水位 struct(48B)+ MetaOpaqueBytes + 4B 尾,留余量更稳
2 自写单槽文件存 meta(3a 场景) 独自写盘要自理 torn write 原子性、落盘顺序(data 先 meta 后)、崩溃恢复一致性、2PC 提交链路。正确:用 MetadataMetaTransport 托管到 VersionedMetadata(版本链天然容错 + N=2 轮转有界 + 2PC 已实现)
3 绕过 PersistencePolicy 自改落盘时机 一致性失守——Async 策略下 Write 不落盘,崩在后台 flush 前 = 丢失。正确:需要每次 Write 落盘就注入 SyncPersistencePolicy;上层编排走 2PC(Prepare+ConfirmCommitted
4 跨 await 持有 AsSpan() / GetRef<T>() 视图 视图仅 epoch 存活期内有效——并发写/Abort 可能使其失效。正确:跨 await 走 Read(dst)(拷贝交付)
5 用地址值 == LogicalAddress.Empty 判断"链头未找到" Empty 是合法地址(地址空间起点,首版本链头就是 Empty)。正确:恢复代码用 headFound 布尔标志(已是这样实现的)
6 统一 record 步进扫描版本链 PayloadSize 跨重启变更后新旧混尺寸,统一步进会落在旧 record 中段 → 静默断链丢数据。正确:按每条 record 头部自述 PayloadLength+PaddingLength 跳进
7 Disabled 模式下 SetOpaqueMeta 期望被接受 InvalidOperationException(禁用即报错,不静默吞)。正确:需要 opaque 就配 Managed/Transport
8 把 meta 块当独立提交路径(自流嵌入 3b 之外的语义) 已废除——自拍水位独立成块 = 并发水位回退 + 被内部提交冲掉。正确:opaque 只登记,随水位提交原子落盘(meta.md §4)

9. 想深入?指路

想懂什么 去哪
源码 src/TC.Tier.Runtime/Structures/Metadata/(VersionedMetadata / MetadataBase.* / Contracts/)
Meta 横切协议(三模式 + IMetaTransport + opaque 搭车 + 块格式) meta.md
Session 协调协议(写/读/检查点三 op、悬挂裁决) session.md
引擎使用(Allocate/Write/Flush/Reclaim/Compact) storage-engine.md
段几何 / 段表 segment-table.md
Lease 协议 lease-protocol.md
同族结构 ring.md / log.md / index.md / mirror.md / snapshot.md
总览(七种结构选型 / 组合 KV / 反模式) structures.md

机制级细节(record 42B header 字段全表、扫盘 64KB 页步进、_hotSlotSize 增长策略、Abort 回退分支条件)按需从源码 XML 注释与 docs/design/ 设计稿查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。


10. FAQ

问题 答案
Write 后立刻 Read 不到新值? 不会——Write 同步更新内存镜像,Read 零 IO 读内存。但磁盘上不一定有——不注入 SyncPersistencePolicy 时 Write 不自动落盘,崩在 Persist/Prepare 前磁盘链头仍是旧版本(恢复后读旧值)。
MaxMemoryVersions 配 1 行不行? 不行——构造强制 Math.Max(2, value)。N=1 时 Write 覆盖前无可保留的旧版本镜像,Abort 只能冷-热回退到加载版本(无法热-热回退到上一 Write)。N=2 是 Abort 零 IO 的几何底线。
跨重启改 PayloadSize 安全吗? 安全——历史版本按盘上真实 PayloadLength 从自持池租借只读缓冲,不补零不截断完整交付;本次运行的新版本按新 PayloadSize 几何落盘。版本链混尺寸由每条 record 头部自述几何支撑(扫描/回收按各自 PayloadLength+PaddingLength 跳进)。
PrepareAbort 之间还能 Read 到新值吗? 能——Prepare 已追加新版本到磁盘链头并 flush,但 LastCommittedSeq 未推进;内存镜像也是新值。Abort 后才回退(内存 + 磁盘链头 + 版本号一并回退到 _prepareSnapshotAddress)。
Disabled 模式恢复慢吗? 慢——扫盘 O(链长/盘),最大扫 64MB。GB/TB 级单值状态强烈建议开 Managed(O(1) 读水位)。VersionedMetadata 单值场景一般几 KB 到 MB,扫盘可接受;3a 托管场景按主流结构大小判断。
变长档下 AsSpan() 返回的 span 长度? 当前内容长度(加载版本按盘上真实 PayloadLength / 热区按最近 Write 长度)。不是 PayloadSize,也不是 MaxPayloadSize——按当前实际镜像切片。CurrentPayloadLength 是探测入口。