1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust + PostgreSQL + Redis で試す「Cache Aside」と「Write Through」パターン実装

Posted at

はじめに

この記事では、一般的なキャッシュ戦略である「Cache Aside」と「Write Through」の2つのパターンについて、Rust を用いた実装例を紹介します。

データベースには PostgreSQL、キャッシュストアには Redis を使用します。

各パターンの理論的な詳細解説(メリット・デメリットなど)は省略し、具体的なコードと動作確認にフォーカスします。理論的な解説は「参考」に挙げている記事をご参照ください。

また、今回検証に使用したプログラムは、以下のリポジトリに格納しています。記事中では一部の関数を省略しているため、全体像を確認したい方はこちらを参照してください。

検証前のデータ状況

まず、検証を開始する前のデータベース (PostgreSQL) とキャッシュ (Redis) の状態を確認します。

Postgres

docker exec -it simple_postgres_db psql -U user app_db

app_db=# select * from users;
 id | username |          created_at           
----+----------+-------------------------------
  4 | user01   | 2025-11-02 01:43:23.038598+00
 12 | user02   | 2025-11-04 07:50:02.877286+00
 14 | user05   | 2025-11-08 01:32:08.314346+00
 18 | user06   | 2025-11-08 01:41:45.293676+00
(4 rows)

Redis

docker exec -it simple_redis_cache redis-cli

127.0.0.1:6379> KEYS *
1) "user01"
127.0.0.1:6379> GET user01
"{\"id\":4,\"username\":\"user01\",\"created_at\":\"2025-11-02T01:43:23.038598Z\"}"

redisにはuser01のみ存在している状態。

実装コード

今回使用する主要な関数です。

1. Cache Aside (read_target_user)

はじめに、Cache Aside パターンを実装した read_target_user 関数です。これは読み取り時の処理です。

処理フロー:

  1. まず Redis (キャッシュ) にデータがあるか確認します。
  2. あれば (キャッシュヒット)、そのデータを返します。
  3. なければ (キャッシュミス)、PostgreSQL (DB) にアクセスしてデータを取得します。
  4. DBから取得したデータを Redis に 書き込み (セット) します。
  5. DBから取得したデータを返します。

cache.get_value, cache.set_value, db.get_user は、それぞれ Redis と DB へアクセスするヘルパー関数(リポジトリ参照)を呼び出しています。

/// implement 'cache aside pattern'
async fn read_target_user(
    db: &PostgresRepo,
    cache: &RedisCache,
    username: &str,
) -> Result<String, Error> {
    
    // 1. Retrieve the target value from the cache if it exists.
    let cache_result = match cache.get_value(username).await {
        Ok(retrieved_value) => {
            println!("\ncache hit!!!");
            Some(retrieved_value)
        },
        Err(_) => {
            println!("\ncan't get the data from the cache store.");
            None
        }
    };

    // 2. return the value if the value exists.
    if let Some(value) = cache_result {
        return Ok(value);
    } 

    // 3. Access the database
    println!("\naccess the database...");
    let db_result = match db.get_user(username).await {
        Ok(data) => data,
        Err(e) => {
            // there is no user.
            return Err(e);
        }
    };

    // 4. Update the cache store.
    println!("\nset the value in the cache store.");
    cache.set_value(&username, &db_result.to_json().unwrap()).await?;

    // 5. Return the value from the database.
    Ok(db_result.to_json()?)
}

2. Write Through (write_target_user)

次に、Write Through パターンを実装した write_target_user 関数です。これは書き込み/更新時の処理です。

処理フロー:

  1. まず PostgreSQL (DB) にデータを書き込み (または取得/作成) します。
  2. 成功したら、Redis (キャッシュ) にも同じデータを書き込みます。
  3. データを返します。

db.get_or_create_user, cache.set_value は、それぞれ DB と Redis へアクセスするヘルパー関数です。

/// implement 'write through pattern'
async fn write_target_user(
    db: &PostgresRepo,
    cache: &RedisCache,
    username: &str,
) -> Result<String, Error> {

    // 1. update the database.
    println!("\naccess the database...");
    let user = db.get_or_create_user(username).await?;
    let user_json = user.to_json().unwrap();

    // 2. set the value in the cache store.
    println!("\nset the data in the cache store...");
    cache.set_value(username, &user_json).await?;
    
    Ok(user_json)
}

3. main 関数

最後に、これら2つの関数を呼び出す main 関数です。
println! が多くてやや見づらいですが、やっていることはシンプルです。

  1. 標準入力から username を受け取ります。
  2. read_target_user() (Cache Aside) を実行し、結果を表示します。
  3. write_target_user() (Write Through) を実行し、結果を表示します。
#[tokio::main]
async fn main() -> Result<()> {
    dotenv().ok();
    println!("The env variables are read.");

    let db_url = env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
    let redis_url = env::var("REDIS_URL").context("REDIS_URL must be set")?;

    println!("input username:");
    let mut input_value = String::new();
    std::io::stdin().read_line(&mut input_value).expect("Failed to read line");

    // shadowing 'input_value to remove '\n' in the end of the variable
    let input_value = input_value.trim();

    let db_repo = PostgresRepo::connect(&db_url).await?;
    let cache = RedisCache::connect(&redis_url).await?;

    println!("---Start functions!!---");
    
    println!("read_target_user() started.");
    let target_user = match read_target_user(&db_repo, &cache, &input_value).await {
        Ok(result) => result,
        Err(e) => {
            eprintln!("\nERROR: failed to read the user.");
            eprintln!("Details: {:?}", e);
            "nodata".to_string()
        }
    };
    println!("read_target_user() executed successfully.");
    println!("the user is {}", target_user);
    println!("----------------------------------");

    println!("write_target_user() started");
    let target_user = match write_target_user(&db_repo, &cache, &input_value).await {
        Ok(result) => result,
        Err(e) => {
            eprintln!("\nERROR: failed to write the user.");
            eprintln!("Details: {:?}", e);
            return Err(e);
        }
    };
    println!("write_target_user() executed successfully.");
    println!("the user is {}", target_user);
    println!("----------------------------------");

    Ok(())
}

動作確認

それでは、プログラムを実行して動作を確認します。

case 1: キャッシュヒットするユーザー (user01)

user01 は、検証前の時点で DB にも Redis にも 存在するユーザーです。

(.venv) root@IT-PC-2403-1122:/home/me/work/rust/cache-demo/db-app# cargo run
   Compiling db-app v0.1.0 (/home/me/work/rust/cache-demo/db-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.50s
     Running `target/debug/db-app`
The env variables are read.
input username:
user01
---Start functions!!---
read_target_user() started.

cache hit!!!
read_target_user() executed successfully.
the user is {"id":4,"username":"user01","created_at":"2025-11-02T01:43:23.038598Z"}
----------------------------------
write_target_user() started

access the database...
[PostgreSQL] User 'user01' already exists. Skipping insertion.

set the data in the cache store...
write_target_user() executed successfully.
the user is {"id":4,"username":"user01","created_at":"2025-11-02T01:43:23.038598Z"}
----------------------------------

実行結果のポイント:

  • read_target_user: cache hit!!! が出力され、DB にアクセスせず Redis から データを取得しています。
  • write_target_user: DB にアクセスし (access the database...)、already exists が表示された後、キャッシュにもデータをセット (set the data in the cache store...) しています。

case 2: キャッシュミスするユーザー (user02)

次に、DB には存在するが Redis には存在しない user02 を入力します。

(.venv) root@IT-PC-2403-1122:/home/me/work/rust/cache-demo/db-app# cargo run
   Compiling db-app v0.1.0 (/home/me/work/rust/cache-demo/db-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.59s
     Running `target/debug/db-app`
The env variables are read.
input username:
user02
---Start functions!!---
read_target_user() started.

can't get the data from the cache store.

access the database...

set the value in the cache store.
read_target_user() executed successfully.
the user is {"id":12,"username":"user02","created_at":"2025-11-04T07:50:02.877286Z"}
----------------------------------
write_target_user() started

access the database...
[PostgreSQL] User 'user02' already exists. Skipping insertion.

set the data in the cache store...
write_target_user() executed successfully.
the user is {"id":12,"username":"user02","created_at":"2025-11-04T07:50:02.877286Z"}
----------------------------------

実行結果のポイント:

  • read_target_user:
    1. can't get the data from the cache store. が出力されます (キャッシュミス)。
    2. その後 access the database...DB からデータを取得 します。
    3. set the value in the cache store.Redis にデータを書き込んで います。

↑これが Cache Aside の典型的な読み取り動作です。

  • write_target_user: user01 の場合と同様に、DB とキャッシュの両方に書き込み(Write Through)を行っています。(検証の関係でread_target_userと一緒に呼び出してしまっているので、違いが出てませんね。。)

もちろんredisに値が格納されています。

127.0.0.1:6379> KEYS *
1) "user02"
2) "user01"
127.0.0.1:6379> GET user02
"{\"id\":12,\"username\":\"user02\",\"created_at\":\"2025-11-04T07:50:02.877286Z\"}"

最後に

Rust を使って、Cache Aside と Write Through パターンの簡単な動作確認用プログラムを作成しました。

今回は単純な Read/Write の実装でした。あとは要件に合わせて、この実装を作り替えるだけですね(それが大変)。

この記事が、キャッシュ戦略を検討する際の一助となれば幸いです。

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?