3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

Rustで一定時間データをキャッシュする仕組みを作った

Posted at

目的

APIで大量にアクセスがある場合、あまり変わらない想定のマスターデータがAPIの呼び出しの都度データベースにアクセスしてほしくないです。

そこで一定時間データをキャッシュする仕組みを作りました。

データはハッシュマップに保存して、同じキーの場合、一定時間なら同じデータを返して、過ぎていた場合新しいデータを取ってきます。

ハッシュマップはonce_cellを使ってグローバルにおいておきます。

結果

ランダムな文字列を保持するModelがあります。これを1秒間キャッシュします。
以下の結果では最初の2回は同じ結果ですが、3秒後には違う結果になっています。

Some(Model { value: "3u0TUQ79MrSTY9VE" })
Some(Model { value: "3u0TUQ79MrSTY9VE" })

# 3秒経過

Some(Model { value: "hko6Tvjg1MT48A6u" })

コード

Cargo.toml
[package]
name = "holder_sample"
version = "0.1.0"
edition = "2021"

[dependencies]
once_cell = "1"
thiserror = "1"
chrono = { version = "0.4", features = ["serde", "clock"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8.5"
main.rs
pub mod error;
pub mod holder;
pub mod model;

use crate::holder::Holder;
use crate::model::Model;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;

// グローバルなデータをキャッシュするハッシュマップ
// キーはモデルを一意で表す文字列です。DBなら主キーなど。
static MODEL_HASH: Lazy<Mutex<HashMap<String, Holder<Model>>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

#[tokio::main]
async fn main() -> Result<(), crate::error::Error> {
    // キャッシュ保持期間は1秒
    let cache_expire_second_count = 1;

    // 最初のモデルを取得する。最初はキャッシュされていないので、クロージャーが呼ばれて生成される
    let model = Holder::get("12345", &MODEL_HASH, cache_expire_second_count, || async {
        Ok(Some(Model::new()))
    })
    .await?;
    println!("{:?}", model);

    // すぐに取得すると、キャッシュが利用されて上記と同じ結果になる
    // クロージャーは呼ばれない
    let model = Holder::get("12345", &MODEL_HASH, cache_expire_second_count, || async {
        Ok(Some(Model::new()))
    })
    .await?;
    println!("{:?}", model);

    // しばらく待つ
    tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;

    // キャッシュ保持期間が過ぎるのでクロージャーが呼ばれて新しいデータができる
    let model = Holder::get("12345", &MODEL_HASH, cache_expire_second_count, || async {
        Ok(Some(Model::new()))
    })
    .await?;
    println!("{:?}", model);

    Ok(())
}
error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Invalid {0}")]
    Invalid(String),
}
model.rs
use rand::distributions::{Alphanumeric, DistString};

#[derive(Debug, Clone)]
pub struct Model {
    pub value: String,
}

impl Model {
    // ランダムな文字列で生成する
    pub fn new() -> Self {
        let mut rng = rand::thread_rng();
        Self {
            value: Alphanumeric.sample_string(&mut rng, 16),
        }
    }
}
holder.rs
use crate::error::Error;
pub use chrono::{prelude::*, Duration};
use std::{collections::HashMap, future::Future, sync::Mutex};

pub struct Holder<T> {
    // 保持するデータ
    target: T,
    // キャッシュが無効になる時間
    expire_at: DateTime<Utc>,
}

impl<T> Holder<T>
where
    T: Clone,
{
    pub fn new(target: T, expire_at: DateTime<Utc>) -> Self {
        Self { target, expire_at }
    }

    fn get_target(&self, now: &DateTime<Utc>) -> Option<T> {
        if self.expire_at > *now {
            Some(self.target.clone())
        } else {
            None
        }
    }

    fn inner_get(
        key: &str,
        now: &DateTime<Utc>,
        mutex: &Mutex<HashMap<String, Holder<T>>>,
    ) -> Result<Option<T>, Error> {
        let hash = mutex
            .lock()
            .map_err(|_| Error::Invalid("lock failure".to_owned()))?;
        let holder = hash.get(key);
        Ok(if let Some(holder) = holder.as_ref() {
            holder.get_target(now)
        } else {
            None
        })
    }

    fn inner_set(
        key: &str,
        target: T,
        mutex: &Mutex<HashMap<String, Holder<T>>>,
        expire_at: DateTime<Utc>,
    ) -> Result<(), Error> {
        let mut hash = mutex
            .lock()
            .map_err(|_| Error::Invalid("lock failure".to_owned()))?;
        hash.insert(key.to_owned(), Holder::new(target, expire_at));
        Ok(())
    }

    pub async fn get<Fut>(
        key: &str,
        mutex: &Mutex<HashMap<String, Holder<T>>>,
        cache_expire_second_count: u64,
        f: impl FnOnce() -> Fut,
    ) -> Result<Option<T>, Error>
    where
        Fut: Future<Output = Result<Option<T>, Error>>,
    {
        let now = Utc::now();
        {
            if let Some(target) = Self::inner_get(key, &now, mutex)? {
                return Ok(Some(target));
            }
        }
        let target = f().await?;
        if let Some(target) = target.as_ref() {
            Self::inner_set(
                key,
                target.clone(),
                mutex,
                now + Duration::seconds(cache_expire_second_count as i64),
            )?;
        }
        Ok(target)
    }
}
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?