← Back to Blog
EN中文

实时搜索中的内存索引与原子置禁策略

在构建高吞吐量的实时搜索系统时,内存索引(Memory Indexer)是连接写入流与持久化存储的关键组件。它的核心挑战在于:如何在维持高并发写入的同时,高效处理文档的即时失效(Banning)与状态查询。

本文探讨一种基于原子操作的文档管理策略,并使用 Rust 演示其核心实现逻辑。

设计背景:读写冲突与状态管理

在典型的搜索架构中,文档被写入内存后并非立即落盘,而是先驻留在内存结构中积累。此时,系统需要维护一份 DocInfo 列表,用于追踪每个文档的状态(如是否存活、是否被删除)。

如果简单地使用一把全局大锁保护整个列表,读写性能会随着并发量的增加急剧下降。一种更优的策略是利用原子操作(Atomic Operations)来管理单个文档的状态,从而将锁的粒度降到最低,甚至在某些读取路径上实现无锁化。

核心实现:原子状态管理

我们将实现一个简化的内存文档管理器。每个文档包含一个原子计数器(用于引用计数或版本控制)和一个状态标记。

数据结构定义

利用 Rust 的 std::sync::atomic,我们可以构建一个线程安全的文档状态结构:

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, RwLock};

// 模拟文档元数据
struct DocInfo {
    // 文档全局唯一 ID
    doc_id: u64,
    // 原子布尔值,标记文档是否有效(未被 Ban)
    is_valid: AtomicBool,
    // 原子计数器,可用于版本控制或引用计数
    ref_count: AtomicU64,
}

impl DocInfo {
    fn new(doc_id: u64) -> Self {
        Self {
            doc_id,
            is_valid: AtomicBool::new(true),
            ref_count: AtomicU64::new(0),
        }
    }

    // 快速检查文档状态,无需互斥锁
    fn is_active(&self) -> bool {
        self.is_valid.load(Ordering::Acquire)
    }

    // 原子操作:标记文档为失效(Ban)
    fn ban(&self) {
        self.is_valid.store(false, Ordering::Release);
    }
}

容器与并发控制

为了管理这些文档,我们需要一个容器。虽然单个文档的状态是原子的,但容器本身的扩容(push)仍需要保护。这里使用 RwLock,但关键在于:读取文档状态时不需要获取写锁,甚至不需要获取读锁(如果我们直接持有 Arc 指针)。

struct MemoryIndexer {
    // 使用 RwLock 保护 Vec 的结构变更(如添加新文档)
    // Vec 中存放 Arc<DocInfo>,使得单个文档的地址稳定且可被独立共享
    docs: RwLock<Vec<Arc<DocInfo>>>,
}

impl MemoryIndexer {
    fn new() -> Self {
        Self {
            docs: RwLock::new(Vec::new()),
        }
    }

    // 写入新文档:需要获取写锁
    fn add_document(&self, doc_id: u64) {
        let doc = Arc::new(DocInfo::new(doc_id));
        let mut w_guard = self.docs.write().unwrap();
        w_guard.push(doc);
    }

    // 标记删除:通过索引查找,只需要读锁获取引用,具体修改是原子的
    fn ban_document(&self, index: usize) -> bool {
        let r_guard = self.docs.read().unwrap();
        if let Some(doc) = r_guard.get(index) {
            // 这里体现了设计的精髓:
            // 我们持有容器的读锁,但却能修改文档的状态,因为状态是内部可变的(Interior Mutability)
            doc.ban();
            return true;
        }
        false
    }

    // 状态快照:高频操作,锁竞争极低
    fn get_active_count(&self) -> usize {
        let r_guard = self.docs.read().unwrap();
        r_guard.iter()
            .filter(|doc| doc.is_active())
            .count()
    }
}

这种模式的优势

  1. 读写分离的极致化:由于 AtomicBool 提供了内部可变性,我们在修改文档状态(逻辑上的“写”)时,只需要持有容器的“读锁”。这意味着多个线程可以同时 Ban 不同的文档,而不会互相阻塞。只有在添加新文档(改变容器大小)时才需要写锁。
  2. CPU 缓存友好:相比于对整个大结构加锁,原子操作仅使相关的缓存行失效,减少了核心间的总线流量。
  3. 避免移位开销:在内存索引中,“删除”通常只是标记。物理删除会推迟到刷盘或合并阶段。这种“软删除”策略避免了 Vec::remove 带来的内存移动开销,保证了索引位置的稳定性。

总结

在 Rust 中,结合 RwLock 保护结构与 Atomic 保护状态,能够构建出极高性能的内存索引组件。这种设计模式不仅适用于搜索引擎,也广泛适用于任何需要高频状态更新与并发查询的系统。它展示了如何通过细粒度的并发控制,在保证内存安全(Safe Rust)的前提下,逼近 C++ 手动内存管理的性能极限。