Mini Moka

RustキャッシュライブラリLRU並行性インメモリ

GitHub概要

moka-rs/mini-moka

A simple concurrent caching library that might fit to many use cases

スター137
ウォッチ2
フォーク9
作成日:2022年7月9日
言語:Rust
ライセンス:Apache License 2.0

トピックス

なし

スター履歴

moka-rs/mini-moka Star History
データ取得日時: 2025/10/22 08:07

キャッシュライブラリ

Mini Moka

概要

Mini Mokaは、Rust向けの高速で並行処理に対応したインメモリキャッシュライブラリです。

詳細

Mini Mokaは、人気の高いMokaキャッシュライブラリの軽量版として設計されたRust向けのインメモリキャッシュライブラリです。Javaの有名なCaffeineライブラリからインスパイアされたアーキテクチャを採用し、ハッシュマップ上に構築されたキャッシュ実装を提供します。スレッドセーフな並行キャッシュと、シングルスレッドアプリケーション向けの非スレッドセーフキャッシュの両方をサポートしています。取得操作での完全な並行性と、更新操作での高い並行性を実現し、DashMapを基盤とした効率的な実装となっています。LFU(Least Frequently Used)ポリシーによるキャッシュ受け入れ制御と、LRU(Least Recently Used)ポリシーによる退避制御を組み合わせることで、ほぼ最適なヒット率を維持します。エントリ数ベースや重み付きサイズベースの容量制限、TTL(Time To Live)やTTI(Time To Idle)による有効期限管理など、柔軟なキャッシュポリシーを提供します。

メリット・デメリット

メリット

  • 高性能: 並行アクセスに最適化された高速キャッシュ実装
  • スレッドセーフ: 複数スレッド間での安全な共有が可能
  • 軽量: Mokaの軽量版として最小限の機能で高性能を実現
  • 柔軟な設定: CacheBuilderによる詳細なカスタマイズ対応
  • Rust最適化: Rust言語の特性を活かした安全で効率的な実装
  • 豊富な退避ポリシー: LRU+LFUの組み合わせによる最適化
  • 有効期限管理: TTLとTTI両方のポリシーサポート

デメリット

  • メモリ制限: インメモリのため大容量データには不適切
  • 永続化なし: プロセス終了時にデータが失われる
  • Rust専用: 他言語での利用不可
  • 学習コスト: Rustの所有権システムとキャッシュ概念の理解が必要
  • 設定複雑性: 高度な機能使用時の設定が複雑になる可能性

主要リンク

書き方の例

基本的なキャッシュ使用

use mini_moka::sync::Cache;

fn main() {
    // 最大10,000エントリを保存できるキャッシュを作成
    let cache = Cache::new(10_000);

    // 値の設定
    cache.insert("key1", "value1");
    cache.insert("key2", "value2");

    // 値の取得
    if let Some(value) = cache.get("key1") {
        println!("Found: {}", value);
    }

    // 存在確認
    if cache.contains_key("key2") {
        println!("Key exists");
    }

    // キーの削除
    cache.remove("key1");

    // キャッシュサイズの確認
    println!("Cache size: {}", cache.entry_count());
}

並行アクセスの例

use mini_moka::sync::Cache;
use std::sync::Arc;
use std::thread;

fn main() {
    const NUM_THREADS: usize = 16;
    const NUM_KEYS_PER_THREAD: usize = 64;

    // 共有キャッシュの作成
    let cache = Cache::new(10_000);

    // 複数スレッドでの並行アクセス
    let handles: Vec<_> = (0..NUM_THREADS)
        .map(|thread_id| {
            // キャッシュのクローン(軽量操作)
            let cache = cache.clone();
            
            thread::spawn(move || {
                // 各スレッドでキーを読み書き
                for key_id in 0..NUM_KEYS_PER_THREAD {
                    let key = format!("key-{}-{}", thread_id, key_id);
                    let value = format!("value-{}-{}", thread_id, key_id);
                    
                    // 書き込み
                    cache.insert(key.clone(), value.clone());
                    
                    // 読み込み
                    if let Some(cached_value) = cache.get(&key) {
                        assert_eq!(cached_value, value);
                    }
                }
            })
        })
        .collect();

    // 全スレッドの完了を待機
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final cache size: {}", cache.entry_count());
}

CacheBuilderを使用した詳細設定

use mini_moka::sync::{Cache, CacheBuilder};
use std::time::Duration;

fn main() {
    // CacheBuilderで詳細設定
    let cache: Cache<String, String> = CacheBuilder::new(1_000)
        .time_to_live(Duration::from_secs(300))    // 5分のTTL
        .time_to_idle(Duration::from_secs(60))     // 1分のTTI
        .initial_capacity(100)                      // 初期容量
        .build();

    // データの挿入
    cache.insert("user:123".to_string(), "Alice".to_string());
    cache.insert("user:456".to_string(), "Bob".to_string());

    // TTL/TTI期限前の取得
    if let Some(user) = cache.get("user:123") {
        println!("User: {}", user);
    }

    // キャッシュ統計情報の取得
    println!("Hit count: {}", cache.hit_count());
    println!("Miss count: {}", cache.miss_count());
    println!("Hit ratio: {:.2}", cache.hit_ratio());
}

重み付きキャッシュ

use mini_moka::sync::{Cache, CacheBuilder};

#[derive(Clone)]
struct DataItem {
    data: Vec<u8>,
    metadata: String,
}

impl DataItem {
    fn size(&self) -> u32 {
        (self.data.len() + self.metadata.len()) as u32
    }
}

fn main() {
    // 重み付きキャッシュ(総サイズ制限)
    let cache: Cache<String, DataItem> = CacheBuilder::new(1000)
        .weigher(|_key, value| value.size()) // カスタム重み計算
        .build();

    // 大きなデータアイテムの保存
    let large_item = DataItem {
        data: vec![0u8; 1024], // 1KB
        metadata: "Large data item".to_string(),
    };

    let small_item = DataItem {
        data: vec![0u8; 100], // 100B
        metadata: "Small data item".to_string(),
    };

    cache.insert("large".to_string(), large_item);
    cache.insert("small".to_string(), small_item);

    // 重み付きサイズの確認
    println!("Weighted size: {}", cache.weighted_size());
}

非同期バージョン(future機能)

use mini_moka::future::Cache;
use std::time::Duration;

#[tokio::main]
async fn main() {
    // 非同期キャッシュの作成
    let cache = Cache::builder()
        .time_to_live(Duration::from_secs(300))
        .max_capacity(1000)
        .build();

    // 非同期でのデータ挿入・取得
    cache.insert("async_key", "async_value").await;

    if let Some(value) = cache.get("async_key").await {
        println!("Async value: {}", value);
    }

    // 期限切れまで待機
    tokio::time::sleep(Duration::from_secs(301)).await;

    // キャッシュクリーンアップ(期限切れアイテムの削除)
    cache.run_pending_tasks().await;

    // 期限切れ後は値が取得できない
    assert!(cache.get("async_key").await.is_none());
}

エラーハンドリングとフォールバック

use mini_moka::sync::Cache;
use std::collections::HashMap;

struct DataService {
    cache: Cache<String, String>,
    fallback_storage: HashMap<String, String>,
}

impl DataService {
    fn new() -> Self {
        Self {
            cache: Cache::new(1000),
            fallback_storage: HashMap::new(),
        }
    }

    fn get_data(&mut self, key: &str) -> Option<String> {
        // まずキャッシュから試行
        if let Some(cached_value) = self.cache.get(key) {
            println!("Cache hit for key: {}", key);
            return Some(cached_value);
        }

        // キャッシュミス時はフォールバックストレージから取得
        if let Some(fallback_value) = self.fallback_storage.get(key) {
            println!("Fallback hit for key: {}", key);
            // フォールバックから取得した値をキャッシュに保存
            self.cache.insert(key.to_string(), fallback_value.clone());
            return Some(fallback_value.clone());
        }

        println!("Data not found for key: {}", key);
        None
    }

    fn set_data(&mut self, key: String, value: String) {
        // キャッシュとフォールバックストレージ両方に保存
        self.cache.insert(key.clone(), value.clone());
        self.fallback_storage.insert(key, value);
    }

    fn invalidate(&self, key: &str) {
        self.cache.remove(key);
    }

    fn cache_stats(&self) {
        println!("Cache entries: {}", self.cache.entry_count());
        println!("Cache hit ratio: {:.2}", self.cache.hit_ratio());
    }
}

fn main() {
    let mut service = DataService::new();

    // データの設定
    service.set_data("user:1".to_string(), "Alice".to_string());
    service.set_data("user:2".to_string(), "Bob".to_string());

    // データの取得(キャッシュヒット)
    service.get_data("user:1");

    // キャッシュ無効化
    service.invalidate("user:1");

    // 無効化後の取得(フォールバックヒット)
    service.get_data("user:1");

    // 統計情報の表示
    service.cache_stats();
}

カスタム有効期限ポリシー

use mini_moka::sync::{Cache, CacheBuilder};
use std::time::{Duration, SystemTime};

#[derive(Clone)]
struct TimestampedValue {
    value: String,
    created_at: SystemTime,
}

impl TimestampedValue {
    fn new(value: String) -> Self {
        Self {
            value,
            created_at: SystemTime::now(),
        }
    }

    fn is_expired(&self, max_age: Duration) -> bool {
        self.created_at.elapsed().unwrap_or(Duration::MAX) > max_age
    }
}

fn main() {
    let cache: Cache<String, TimestampedValue> = CacheBuilder::new(1000)
        .time_to_live(Duration::from_secs(60)) // 1分のTTL
        .build();

    // タイムスタンプ付きデータの挿入
    cache.insert("timestamped_key".to_string(), 
                 TimestampedValue::new("timestamped_value".to_string()));

    // カスタム有効期限チェック
    let max_age = Duration::from_secs(30);
    
    if let Some(timestamped_value) = cache.get("timestamped_key") {
        if timestamped_value.is_expired(max_age) {
            println!("Value is expired according to custom policy");
            cache.remove("timestamped_key");
        } else {
            println!("Value is still valid: {}", timestamped_value.value);
        }
    }
}