Table of Contents

TierKv 使用指南

TierKv = 本地持久 KV 存储产品:Ring(数据唯一真源,append-only)× 主索引三族(Hash 缺省 / BTree / SkipList)× 会话(三档读一致性 + 多 key 原子批)× Functions(RMW 读-改-写)× 检查点快速恢复 × 日志回收。泛型产品类 TierKv<TKey, TValue>,消费面经 [KvStore] 源生成器 封闭(推荐)或 TierKvBuilder 显式装配。

快速上手

一行特性声明 + 一个工厂调用,formatter 零代码(byte / byte[] / ReadOnlyMemory<byte> / unmanaged 结构体自动发射):

// AssemblyInfo.cs(或任意源文件顶层)——程序集级声明一次
[assembly: KvStore(typeof(long), typeof(MyPayload))]
// 与 TierKv 同卷创建并使用(异步优先)
await using var kv = await TierKvOfLongMyPayload.CreateAsync(fs, TierKvOptions.Default.WithKvName("users"));

await kv.PutFormattedAsync(42, new MyPayload { Qty = 7 });   // 写(Upsert,同 key 覆写;缺省内存档)
var (found, payload) = await kv.TryGetFormattedAsync(42);    // 读(未命中 found=false)
await kv.DeleteAsync(42);                                    // 删(墓碑,恢复重放不复活)

kv.TryGetFormatted(42, out var v);                           // 同步快路径(内存档直读——延迟敏感热路径)

TKey 要求 unmanaged + IEquatable<TKey>(结构体 key 表达,如 readonly record struct TestKey(long Id, int Tag))。 TValue 无约束——内建覆盖面之外的类型(如 string)实现 IValueFormatter<TValue> 后经 Formatter = typeof(...) 显式声明,生成器编译期校验(缺失/未实现接口 = 编译期报错 TCSG022/023)。

装配怎么选

路径 适用 说明
[KvStore] 生成封闭形态(推荐) 绝大多数场景 一行声明 → TierKvOfXxx 具名类型 + CreateAsync 默认装配(Ring/索引/formatter 全自动)
TierKvBuilder 显式装配 需要替换 Ring/索引实现或自定义装配 WithRingFactory / WithIndexFactory / WithKeyComparer / WithKeyResolver(必须成对注入工厂)

主索引选择(TierKvOptions.WithIndexKind / 特性 IndexKind = (int)KvIndexKind.Xxx 编译期缺省):

主索引 点查 范围扫描 并发写 适配
Hash(缺省) O(1) 最优 无(W7 补辅助有序索引) CAS 点查写密集
BTree O(logN) 原生 key 序 操作闸(单写多读) 需范围/有序
SkipList O(logN) 原生 key 序 CAS 无锁 高并发写 + 有序

切换索引不丢数据(Ring 唯一真源,索引可重建);跨索引类型重开走全量重放,同索引重开走 帧物化加速(见检查点)。

TierKvBuilder 显式装配(工厂必须成对注入,Settings 经 TierKvAssembly 取默认几何):

var kv = await new TierKvBuilder<long, long>(fs, TierKvOptions.Default
        .WithKvName("metrics").WithIndexKind(KvIndexKind.SkipList))
    .WithRingFactory((fso, o) => new RingOfLong(TierKvAssembly.RingSettings(fso, o), fso))
    .WithIndexFactory((fso, o, ring) => new SkipListOfLong(fso, TierKvAssembly.SkipListSettings(fso, o), keyResolver: ring))
    .StartAsync();

会话(读一致性 + 原子批)

会话为单线程对象(每会话一实例,内部零锁):

using var session = kv.CreateSession();                     // 缺省 ReadMyWrites(D3)
await session.PutFormattedAsync(1, 100L);                   // 写立即应用 + 登记写集

// ReadMyWrites:本会话最后一次写胜出他人并发覆盖
await otherSession.PutFormattedAsync(1, 999L);              // 他人覆盖
var (hit, v) = await session.TryGetFormattedAsync(1);       // 本会话仍读己之写(v = 100,不受影响)

// 三档:None(读恒最新)/ ReadMyWrites(缺省)/ Serializable(RMW + 写临界区版本保护)
using var serial = kv.CreateSession(KvSessionConditions.Serializable);

多 key 原子批(全或无)

session.BeginAtomicBatch();
await session.PutFormattedAsync(10, 111L);                  // 暂存:本会话读可见(stage 隔离)
await session.PutFormattedAsync(11, 222L);                  // 他人与索引不可见
await session.CommitBatchAsync();                           // 全应用(Ring 2PC 提交点语义)
// 或 session.AbortBatch();                                 // 全丢弃,零污染

批内写视为已持久化(提交点即落盘);崩溃任一窗口全或无。

pending 收口(完成语义三档)

await session.PutFormattedAsync(1, 1L, KvCommitPolicy.FireAndForget);  // 不即时刷(会话缺省档)
await session.CompletePendingAsync();                       // 一次刷盘至本会话高水位
// 三档:FireAndForget(缺省,内存档零 IO)/ WaitForPending(登记待收口,同 F&F 路径)/
//       Committed = 逐写等待持久化提交(组提交摊薄 fsync——写级不丢的显式旋钮)
// 注意:kv 直连的 PutAsync(key, value, ct) 两参便捷重载缺省 = Committed;带 policy 参数的
//       重载(kv.PutFormattedAsync / 会话全族)缺省 = FireAndForget(FASTER 形态——
//       持久化由 checkpoint 泵/显式 CheckpointAsync 承担)

Functions(RMW 读-改-写)

实现 IKvFunctions<TKey, TInput, TValue, TOutput, TContext>(或继承 KvFunctionsBase 只填 三个折叠钩子),引擎统一走「读折叠 → Format → 追加 → 索引 CAS 换绑」:

sealed class Counter : KvFunctionsBase<long, long, long, long, string?>
{
    public override bool InitialUpdater(ref long k, ref long input, ref long v, ref long o, ref string? c)
    { v = input; o = v; return true; }                         // 无值造值
    public override bool InPlaceUpdater(ref long k, ref long input, ref long v, ref long o, ref string? c)
    { v += input; o = v; return true; }                        // 追加前最后折叠(false 回落 Copy 流)
    public override bool CopyUpdater(ref long k, ref long input, ref long old, ref long newV, ref long o, ref string? c)
    { newV = old + input; o = newV; return true; }             // 不可变记录折叠(旧版本保留)
}

var status = await kv.RmwAsync(1, input: 5, new Counter(), context: "ctx");
// KvStatus.Ok / Error(折叠返回 false = Error 零写入);旧记录恒保留(版本历史)

读钩子按档位派发:ReadAsync 在 kv 直连走 ConcurrentReader、Serializable 会话走 SingleReader; 写钩子 SingleWriter / ConcurrentWriter 为校验面(返回 false = 拒绝该写入,Error 零写入)。

范围扫描(Scan)

需要 EnableRangeIndex(key 字节序 BTree 辅助索引,与主索引并存双写;代价 = 写放大 + 恢复多一帧)。 扫描按 key 字节序(字节字典序——构造前缀/区间值时按字节语义,非数值语义):

await foreach (var e in kv.ScanByPrefixAsync(prefix, prefixByteLength: 4))
    /* e.Key / e.Value */;

await foreach (var e in kv.ScanByRangeAsync(startKey, endKey))
    /* [start, end) 区间——最新值,已删 key 不产出 */;

TTL(过期删除)

PutAsync / PutFormattedAsync / 会话写带可选 timeToLive——过期 = 惰性读删(点查过期 即未命中;TimeSpan.Zero = 立即过期),回收期强删。RMW 写回会重置 TTL。

await kv.PutFormattedAsync(42, payload, timeToLive: TimeSpan.FromMinutes(30));
(await kv.TryGetFormattedAsync(42)).Found   // 过期后 = false

条件写(CAS)

CompareAndSwapAsync = 「比较 + 写入 + 换绑」单一原子单元(并发 CAS 恰一成功)。地址版以 绑定地址为期望(地址即版本——NewAddress 即 fencing token,续约/释放以它为期望地址再 CAS); 值版比较字节(expectedValue = null = 预期不存在——NX 抢占)。

// 获取即租约:NX + TTL——回执 NewAddress = 锁 token
var acquire = await kv.CompareAndSwapAsync(1, null, value, timeToLive: TimeSpan.FromSeconds(30));
// 持有者续约:以当前 token 为期望换新值 + 续 TTL——token 换新(地址即版本)
var renew = await kv.CompareAndSwapAsync(1, acquire.NewAddress, newValue, timeToLive: TimeSpan.FromSeconds(30));
// 他人持旧 token → 确定性拒绝(Swapped=false + CurrentAddress 携当前绑定)

TTL 过期语义与 Put 一致(惰性读删);timeToLive = null = 无过期。会话面同参透传, 写集过期语义与盘上帧一致(ReadMyWrites 自见过期)。

检查点与恢复

var cp = await kv.CheckpointAsync();   // Ring 全量落盘 → 索引帧落盘 → 版本图原子入盘
  • 崩溃后重开(同卷同名 TierKvOptions 重建实例):索引帧物化(O(索引))+ 增量重放 (帧水位, 尾]——非全量重放;帧缺失/损坏自动回退全量重放(数据等价)。
  • 版本代际:会话版本自单调分配器取值,检查点消费一个版本作代际标签,高水位随提交原子持久化, 恢复后续接(新会话版本恒大于历史)。
  • 恢复到最近检查点版本已内建;恢复到指定历史版本(回滚式 PITR)需要检查点清单记录化, 排期 W6.1+。

日志回收

var result = await kv.ReclaimAsync();   // result.NewBeginAddress / result.SupersededCount

低频全表扫判定存活记录(索引最新指针即本记录),安全下界以下前缀逻辑回收。回收后读安全 (存活记录全数可读);建议在检查点之后回收(帧水位随之收敛)。物理段回收(truncateDevice 语义)与自动回收策略为后续波次。

Watch 变更流

// 从头订阅(历史补扫 + 实时直通),带 1 字节前缀过滤
await foreach (var ev in kv.WatchAsync(LogicalAddress.Empty, prefix, prefixByteLength: 1))
{
    switch (ev.Kind)
    {
        case KvWatchEventKind.Put:    /* ev.Key 新值 */ break;
        case KvWatchEventKind.Delete: /* ev.Key 已删 */ break;
    }
    lastCursor = ev.Address;   // 事件地址 = 续传游标
}

// 断开后重订阅续传(无丢无重——历史从 Ring 真源补齐)
await foreach (var ev in kv.WatchAsync(lastCursor)) { ... }

// 检查点时点的规范续传游标(重启后从此续传)
var cp = await kv.CheckpointAsync();
var watchCursor = cp.WatchCursor;
  • 事件即提交事实:Put/Delete 事件在提交点后发布(原子批逐条、批内序);单写 FireAndForget 档的事件持久性跟随写入提交档(未刷盘记录崩溃后消失)。
  • 续传游标:事件地址(Ring 追加序,全局单调,跨恢复稳定);开区间 (from, ...]。 规范形态 = 已见最大事件地址或 cp.WatchCursorLogicalAddress.Empty = 从头订阅。 裸 TailAddress 是「下一写入位」形态,不作游标。
  • 断连:每订阅者有界通道(WithWatchChannelCapacity,缺省 8192),慢订阅者写满即断连 ——流干净结束 = 断连信号,凭游标重订阅续传;写路径永不被反压。
  • 回收联动:续传点早于回收线(BeginAddress)直接抛 InvalidOperationException (etcd compacted 同义)——检查点之后保留游标,勿长期滞留旧游标。
  • TTL:过期与回收强删不发事件(惰性读删无确定时刻;读侧表现为未命中)。

索引热切换

// 运行中把主索引从 Hash 切到 BTree(Ring 真源不变,数据不丢)
var result = await kv.SwitchIndexAsync((o, ring) =>
    new ByteOrderBTreeIndex<long>(fs, TierKvAssembly.BTreeSettings(fs, o), keyResolver: ring));
// result.SwitchStartAddress = 切换起点(Ring 尾);新索引即刻承载全部点查
  • 三步协议:①构建(工厂建新索引+启动 Ring 窗口重放)→②追平(等待重放完成, 服务全程不中断)→③补扫+发布(版本推进排水:增量补扫切换期间的新写入→新索引 单引用原子发布)→旧索引排水后释放。
  • 数据不丢:Ring 唯一真源;新索引=窗口重放+增量补扫的完整快照,发布后写入直达 新索引——写路径零切换感知。
  • 阻塞面:步骤③持提交门并排水(原子批/检查点/回收短暂排队);点查/单写/Watch/ 范围扫描全程不受阻。并发切换会被拒绝(等待当前切换完成)。
  • 重启语义:热切换是运行时视图操作——重启恢复 Options.IndexKind 装配的原族 (跨索引重启走重放全量重建,数据不丢);需保持切换后族请同步调整装配配置。 切换后检查点落新索引帧,重启按旧族装配时帧族不匹配走 fail-safe 全量重放(数据等价)。
  • 范围索引独立EnableRangeIndex 的辅助 BTree 不参与热切换(主索引切换后范围 扫描照常走辅助索引)。

配置参考(TierKvOptions)

不可变 record + With 链(TierKvOptions.Default.WithXxx(...));完整调优矩阵见 perf/tierkv.md 组合调优面。

参数(With 链) 默认 说明
WithKvName "tier-kv" 引擎子目录名(同卷同名重开 = 恢复)
WithIndexKind Hash 主索引(Hash/BTree/SkipList,见装配怎么选
WithHashTableCapacity 64K 槽 哈希表初始容量(2 的幂;百万级 key 免启动期扩容)
WithRingPageSize 256KB 页池页大小(写穿粒度=整页;大页致 Committed 档 ms 级退化)
WithRingMemorySize 64MB 页池总容量(mem 卷实占——测试勿抄大缺省)
WithRingColdReadRatio 0.25 冷读缓存占比(大冷读工作集建议 1.0)
WithRingMutableFraction / WithRingMaxPageCount 0.9 / 8192 mutable 区占比 / 页槽数上界
WithRingOverflowPolicy(policy, minSize) Disabled WiscKey 式 KV 分离(大 value 写放大/页池占用调优)
WithCheckpointInterval Infinite checkpoint 周期泵(开 = RPO ≈ 间隔;关 = 显式/逐写 Committed 承担)
WithRangeIndex 范围索引 + Scan API(见范围扫描
WithWatchChannelCapacity 8192 每订阅者事件通道(写满断连不反压写路径)
WithScanBatchSize / WithScanSpanPageThreshold 512 / 8 页 预扫批粒度 / 顺序模式地址跨度阈值
WithHints DIO IO hints(磁盘建议叠加 WriteThrough——TierWal 实测全介质最优;mem 零差异)
WithSegmentGrowthLimit 256MB Ring 段生长上限
WithMetaPolicyKind / WithMetaTupleFlushInterval Managed / 200ms meta 策略 / 元组回扫泵周期(窗口内变更合并落盘)

反模式

  • 会话跨逻辑线程共享——会话是单线程对象,并发请每线程/每任务各开会话。
  • 会话存活期长于 KV——会话必须先于 KV 释放(Dispose/DisposeAsync 有断言)。
  • 慢消费者无重订阅机制——Watch 断连后必须凭游标重订阅续传;丢弃游标的消费者断连即丢事件。
  • string 等 managed TValue 不声明 Formatter——编译期 TCSG022 直接报错,无运行时回退。
  • 跳过检查点长期只写——快速恢复依赖检查点帧;无帧回退全量重放(正确但慢)。
  • 网络介质挂载——TierKv 无 fsync 语义保障(raft 面另管), durability 契约不成立。

想深入

  • 设计文档:docs/design/tierkv-design.md(内部仓)
  • 性能数据:docs/perf/(随包发布)
  • 结构层积木:Ring / HashIndex / BTreeIndex / SkipListIndex 使用指南(TC.Tier.Runtime 包)