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.WatchCursor;LogicalAddress.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 包)