基于源码的完整文档,覆盖并发安全、数据完整性、内存安全、输入验证与跨平台 I/O 加固五个维度。
实现位置:database.rs:169-181
let lock_file = std::fs::OpenOptions::new()
.create(true).write(true)
.open(&lock_path)?;
lock_file.try_lock_exclusive().map_err(|_| {
TriviumError::Generic(format!(
"Database '{}' is already opened by another process. ...", path, lock_path
))
})?;通过 fs2::FileExt::try_lock_exclusive 在 <db_path>.lock 文件上持有独占锁。锁由 Database 结构体的 _lock_file 字段持有,在 Database drop 时自动释放。
保证:
- 同一数据库文件不可能被两个进程同时写入
- 锁文件在进程异常退出后由 OS 自动释放(不同于 PID 文件,不会产生死锁残留)
- 在 Linux/macOS(
flock)和 Windows(LockFileEx)上均有效
实现位置:database.rs:114-121 + 全文所有读写操作
fn lock_or_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(|poisoned| {
tracing::warn!("Mutex was poisoned, recovering...");
poisoned.into_inner()
})
}MemTable 和 Wal 均包装在 Arc<Mutex<T>> 中。任何线程 panic 并持有锁时,Mutex 转为"中毒"状态。lock_or_recover 在此情形下不会 unwrap 崩溃,而是主动剥离中毒标记,恢复内部数据继续服务。
保证:
- 单线程 panic 不会导致整个进程崩溃
- 后续请求可以继续正常访问数据(数据状态由 WAL 保证一致性)
- 所有读写操作均通过此函数获取锁,零例外
实现位置:database.rs:279-293(以 insert 为例)
// 先持有 memtable 锁写内存,再持有 wal 锁写日志
// 两把锁不同时持有,避免死锁
let id = {
let mut mt = lock_or_recover(&self.memtable);
mt.insert(vector, payload.clone())?
};
{
let mut w = lock_or_recover(&self.wal);
w.append(&WalEntry::Insert { ... })?;
}memtable 锁和 wal 锁从不同时持有——先释放 memtable 锁,再获取 wal 锁。这是经典的锁顺序规则,消除了死锁的可能性。
保证:
- 任意并发调用组合下不会发生死锁
- 不同操作(insert、delete、link)遵循完全一致的锁获取顺序
实现位置:storage/compaction.rs
后台 Compaction 线程通过 Arc::clone 共享 memtable 和 wal 的引用,同样使用 lock_or_recover 获取锁。Compaction 操作与前台写操作在同一个 Mutex 下序列化,不存在竞态条件。
实现位置:storage/wal.rs:113-115(写入),wal.rs:232-244(读取)
每条 WAL 记录的磁盘格式为:
[len: u32 (4B)] [bincode 序列化数据: len bytes] [crc32: u32 (4B)]
写入时计算,读取(崩溃恢复)时验证:
// 写入
let checksum = crc32fast::hash(&data);
writer.write_all(&checksum.to_le_bytes())?;
// 恢复
let computed_crc = crc32fast::hash(&data);
if stored_crc != computed_crc {
tracing::error!("WAL CRC mismatch ... Stopping recovery.");
break; // 停止回放,丢弃后续数据
}保证:磁盘坏块、OS 写半条记录均可被检测。CRC 不匹配时停止回放而非跳过,防止损坏数据静默渗入。
实现位置:storage/wal.rs:214-217
// 单条不超过 256MB
if len > 256 * 1024 * 1024 {
break; // 损坏的 len 字段
}防止损坏的 len 字段触发天量内存分配(OOM 或 DoS)。
实现位置:storage/wal.rs:260-297
崩溃恢复时对事务做两阶段过滤:只有见到匹配 TxCommit 的事务才会回放,否则整体丢弃:
| 状态 | 行为 |
|---|---|
| TxBegin + 匹配 TxCommit | 全量回放 |
| TxBegin,无 TxCommit(掉电) | 整体丢弃,并物理截断 WAL 尾部(Partial Truncation) |
| 无事务边界的独立操作(旧格式) | 直接回放(向后兼容),推进安全游标 |
极限防御:LSN (Log Sequence Number) 与物理截断
如果在回放时发现未闭合的事务(通常因为机器暴力断电),系统不仅在内存中丢弃它们,还会通过计算最后一个完美闭环事务的精确物理字节偏移量 (safe_commit_offset),在重播前直接调用内置 set_len()。这彻底切断了具有传染性的“幽灵尾部”,防止系统下次启动接收正常追加后,由于 in_tx=true 的状态污染,将新的健康数据错吞进旧的失效事务里。
保证:要么全部回放,要么全部丢弃;不出现"插入了 5 条、应该 10 条"的部分状态。并且绝对防止失效事务封条污染后续追加数据。
实现位置:storage/file_format.rs:86-103(写),file_format.rs:282-326(读)
Mmap 模式下,.tdb 和 .vec 均写入成功后,才原子写 .flush_ok 标记,内含两文件的精确字节大小:
[tdb_size: u64 (8B)] [vec_size: u64 (8B)]
加载时交叉校验,失败则降级为安全模式(忽略 .vec,仅从 .tdb 骨架恢复 + WAL 回放)。
实现位置:storage/file_format.rs:257-267
const MAGIC: &[u8; 4] = b"TVDB";
if mmap.len() < HEADER_SIZE as usize {
return Err(TriviumError::Generic("File too small for header".into()));
}
if &bytes[0..4] != MAGIC {
return Err(TriviumError::Generic(
format!("Invalid file magic: expected TVDB, got {:?}", &bytes[0..4])
));
}防止加载非 TriviumDB 文件或截断损坏文件,同时避免后续偏移量计算出现越界读取。
实现位置:storage/file_format.rs:142-240,storage/vec_pool.rs:flush_rewrite()
所有持久化路径均遵循:
① 写 .tmp 临时文件 → ② sync_all() → ③ robust_rename(tmp → 正式文件)
任何步骤崩溃,旧文件完好。.tmp 在下次启动时可安全忽略。
实现位置:storage/wal.rs:48-68
pub enum SyncMode {
Full, // 每条后 fsync — 防 OS 崩溃,金融级
Normal, // 每条后 flush 到 OS 缓冲 — 防进程崩溃(默认)
Off, // 不主动 flush — 仅测试
}实现位置:storage/wal.rs:336-341
Database::drop 时调用 flush_writer(),将 BufWriter 缓冲区主动刷入磁盘,防止正常退出时因 Arc<Mutex<Wal>> 析构链导致的静默数据丢失。
实现位置:storage/vec_pool.rs:open() 和 flush_append()
// SAFETY: MAP_PRIVATE (copy-on-write)
// - VectorType 要求 T: Pod + Zeroable,字节对齐和全零初始化安全
// - len 由 expected_count * dim * size_of::<T>() 精确计算,不超出文件大小
let mmap = unsafe {
memmap2::MmapOptions::new().len(expected_size).map_copy(&file)?
};MAP_PRIVATE 映射:写入只产生进程私有 COW 页,不影响底层文件,其他进程/映射不受影响。
实现位置:storage/vec_pool.rs:get() 和 rebuild_merged_cache()
let ptr = bytes.as_ptr();
if (ptr as usize) % std::mem::align_of::<T>() == 0 {
unsafe { std::slice::from_raw_parts(ptr as *const T, self.dim) }
} else {
// 非对齐:bytemuck::pod_read_unaligned 安全回退
}mmap 返回的地址始终页对齐(4096B);f32 需 4B 对齐、u64 需 8B 对齐,均严格满足。代码中有运行时对齐检查和安全回退路径。
实现位置:vector.rs:130-141
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
// SAFETY: 运行时已确认 CPU 支持
return unsafe { cosine_similarity_avx2(a, b) };
}
cosine_similarity_scalar(a, b) // 安全标量回退不支持 AVX2 的 CPU 自动回退到纯 Rust 路径,不会执行非法指令。
实现位置:vector.rs:13-14
pub trait VectorType: bytemuck::Zeroable + bytemuck::Pod + ...Pod(Plain Old Data)是编译期保证:无指针、无引用、无 padding 歧义、全零合法。使得 bytemuck::cast_slice 在编译期被证明安全,完全不需要运行时 unsafe。
实现位置:database.rs(BQ 精排分支)
if offset + dim <= vectors.len() {
let score = T::similarity(query_vector, &vectors[offset..offset + dim]);
}访问 flat_vectors 时,始终通过 offset + dim <= len 守卫,防止 mmap_count 与实际数组大小不一致时的越界访问。
实现位置:query/executor.rs:eval_expr_by_id() 和路径扩展逻辑
// 🚀 OOM 防御:我们只在中间计算层存储 NodeId,绝对不克隆包含 Vector 和 Payload 的巨型 Node 结构!
let mut bindings_set: Vec<HashMap<String, u64>> = Vec::new();由于图查询匹配(如 (a)-[]->(b)-[]->(c))会产生爆炸性的笛卡尔积中间结果,若在每一步扩展中传递深拷贝的实体节点数据(包含高维向量、全量 JSON Payload),1 万条路径即会瞬间击穿几个 GB 的内存。
TriviumDB 执行器在整条中间路径遍历中仅保留轻量级 u64 IDs,所有的 WHERE 财产过滤条件使用即时查询计算,仅仅在最后的 RETURN 吐出最终小切片结果时,才去执行昂贵的 build_node 数据装填,物理上隔离了内存爆炸。
实现位置:storage/memtable.rs,database.rs:Transaction::commit()
if vector.len() != self.dim {
return Err(TriviumError::DimensionMismatch { expected: dim, got: vector.len() });
}所有写入向量路径(insert、insert_with_id、update_vector、事务 Dry-Run)均强校验维度,返回类型化错误而非 panic。
实现位置:database.rs:462-475
for item in qv {
let f = item.to_f32();
if f.is_nan() || f.is_infinite() {
return Err(TriviumError::Generic("Query vector contains NaN or Infinity".into()));
}
}NaN 进入余弦计算会传染整个结果。在检索管线 L0 层直接拦截,防止毒素向量污染后续所有计算。
实现位置:database.rs:478-485
safe_cfg.top_k = safe_cfg.top_k.max(1);
safe_cfg.fista_lambda = safe_cfg.fista_lambda.clamp(1e-5, 100.0);
safe_cfg.teleport_alpha = safe_cfg.teleport_alpha.clamp(0.0, 1.0);
safe_cfg.dpp_quality_weight = safe_cfg.dpp_quality_weight.clamp(0.0, 10.0);
safe_cfg.bq_candidate_ratio = safe_cfg.bq_candidate_ratio.clamp(0.0, 1.0);所有数学参数强制钳入合法范围:fista_lambda 过大全变 0,teleport_alpha 超出 [0,1] PPR 概率失去意义,dpp_quality_weight 过大导致 float 溢出。
实现位置:database.rs:Transaction::commit() 第一阶段及其他直写 API
所有业务验证(节点是否存在、ID 是否冲突、维度是否匹配、是否包含 NaN/Inf)在纯内存虚拟状态上完成,不触碰 MemTable 或 WAL。
防OOM极巨载荷拦截 (Payload Limiting):为防止恶意构造或意外产生的数以百兆计的超大 Payload 文本占用物理内存和撑爆日志,Database::insert、update_payload 和 Transaction::commit 会强制性对 JSON 载荷施加 8MB 大小限制 (MAX_PAYLOAD_SIZE)。超越此数值的数据写入将被直接判定失败,保护操作系统的内存水位和文件视窗。
任何验证失败直接返回 Err,MemTable 和 WAL 零损伤。只有 Dry-Run 全部通过,才进入不可失败的 WAL 写入 → MemTable 应用路径。
实现位置:storage/memtable.rs:update_vector()
// 必须检查 payload 存在性,而非 ids_to_indices
// delete() 移除 payload 但 ids_to_indices 中的槽位仍存在(指向已置零位置)
if !self.payloads.contains_key(&id) {
return Err(TriviumError::NodeNotFound(id));
}防止对已逻辑删除的节点进行向量更新。
实现位置:database.rs:delete(),storage/memtable.rs,index/bq.rs
传统图索引(如 HNSW)在节点被逻辑删除后,会在索引图中留下幽灵引用,导致检索结果污染和性能退化。TriviumDB 通过 FreeList 墓碑隐式复用机制 彻底消除了该问题:
- 原位物理擦除:无需进行极其耗时的全局紧凑,已删除的
index将被推入快速复用队列。下一个插入的节点直接占据其所在的物理行。 - 并行特征网同步清零:删除节点时,对应的
fast_tags位特征槽不仅立即随之置零,彻底杜绝该废弃指纹在后续查询被误读; - BQ 动态刷新:下一次 Compaction 触发 BQ 增量重建时,由于底层是纯扁平的一维二进制指纹数组(
Vec<u64>),置零的槽位自然产生空回聚光灯盲区;新索引通过直接赋值原子切换,旧索引并发安全释放。
保证:删除操作对检索质量零副作用,无空间碎片,无需用户手动触发重建,不仅杜绝了 Ghost Node 幽灵节点,更实现了无限频次改写下的 O(1) 平均生命周期开销!
实现位置:graph/traversal.rs:69-83
// 能量阈值守护:得分 ≤ 0 的节点不再传播(防负反馈循环)
next_tier.retain(|_, energy| *energy > 0.0);
// 侧向抑制 Top-K(防稠密图 OOM)
if lateral_inhibition_threshold > 0 && next_tier.len() > lateral_inhibition_threshold {
sorted_tier.truncate(lateral_inhibition_threshold);
}
if next_tier.is_empty() { break; } // 能量衰竭,提前终止两道独立截断:能量守护防负反馈循环,侧向抑制 Top-K 防稠密图爆炸性展开导致 OOM。
实现位置:storage/vec_pool.rs:flush_rewrite() 和 flush_append()
self.mmap = None; // 先解除内核映射锁
robust_rename(&tmp, dst)?; // 再执行原子替换Windows 强制锁定语义:映射存活时 rename 目标文件必定 ERROR_ACCESS_DENIED。先 Drop mmap,COW 私有脏页安全丢弃(数据已写入 .tmp)。
实现位置:storage/file_format.rs 和 storage/vec_pool.rs
Windows 杀毒软件在文件关闭瞬间抢占扫描,通常几毫秒后自动释放。实现指数退避重试(1→2→4→…→50ms,最多 10 次)仅针对 ERROR_ACCESS_DENIED(5) 和 ERROR_SHARING_VIOLATION(32),其他错误立即快速失败。非 Windows 平台编译为直接调用 std::fs::rename,零开销。
实现位置:storage/wal.rs:307-330
truncate(true) 将文件截断为零字节但保留 inode,不触发杀软的"新文件扫描",避免 WAL clear 期间再次产生文件锁冲突。
实现位置:storage/vec_pool.rs:advise_dontneed()
// 通知 OS 立即回收刚刚写入磁盘的高维物理页,阻止污染 VFS 文件缓存
#[cfg(target_os = "linux")]
libc::madvise(ptr, len, libc::MADV_DONTNEED);对于数十 GB 的向量基库,单纯依靠 OS 自我调节 LRU 会引发主机端周期性严重卡顿(Threshing)。引擎使用安全封装的非阻塞 FFI 建议系统,配合 Windows 的 VirtualUnlock 提供安全回收,以极低的成本维持了 60 帧 0 卡顿的主机交互体验。
| 位置 | unsafe 操作 | 安全契约 |
|---|---|---|
vec_pool.rs:open() |
MmapOptions::map_copy() |
T: Pod+Zeroable;MAP_PRIVATE;len ≤ 文件实际大小 |
vec_pool.rs:flush_append() |
MmapOptions::map_copy() |
同上;重映射前旧 mmap 已释放 |
vec_pool.rs:get() |
slice::from_raw_parts() |
运行时对齐检查;index < mmap_count 守卫 |
vec_pool.rs:rebuild_merged_cache() |
slice::from_raw_parts() |
同上 |
file_format.rs:load() |
Mmap::map() |
仅读;mmap Drop 前不删文件 |
file_format.rs:load_bq() |
ptr::copy_nonoverlapping() |
bytemuck Pod 对齐;dst Vec 已预分配足够容量;src 长度精确边界 |
vector.rs:cosine_similarity_avx2() |
AVX2 SIMD 指令 | 运行时 is_x86_feature_detected! 检测通过才调用 |
index/bq.rs:popcount_distance() |
CPU 原生 popcnt 指令 |
运行时自动检测 CPU 支持;纯数学运算,无内存安全风险 |
所有 unsafe 块均附有明确的 // SAFETY: 注释。整个代码库没有 unsafe impl Send/Sync——Send + Sync 由 Arc<Mutex<T>> 自动推导,类型系统级别安全。