はじめに
この記事では、一般的なキャッシュ戦略である「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 関数です。これは読み取り時の処理です。
処理フロー:
- まず Redis (キャッシュ) にデータがあるか確認します。
- あれば (キャッシュヒット)、そのデータを返します。
- なければ (キャッシュミス)、PostgreSQL (DB) にアクセスしてデータを取得します。
- DBから取得したデータを Redis に 書き込み (セット) します。
- 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 関数です。これは書き込み/更新時の処理です。
処理フロー:
- まず PostgreSQL (DB) にデータを書き込み (または取得/作成) します。
- 成功したら、Redis (キャッシュ) にも同じデータを書き込みます。
- データを返します。
※ 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! が多くてやや見づらいですが、やっていることはシンプルです。
- 標準入力から
usernameを受け取ります。 -
read_target_user()(Cache Aside) を実行し、結果を表示します。 -
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:
-
can't get the data from the cache store.が出力されます (キャッシュミス)。 - その後
access the database...で DB からデータを取得 します。 -
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 の実装でした。あとは要件に合わせて、この実装を作り替えるだけですね(それが大変)。
この記事が、キャッシュ戦略を検討する際の一助となれば幸いです。