【個人開発】最もジェネレーティブで、最もアーティフィシャルで、そして最もインテリジェンスなやり方でRedisを実装した話


はじめまして！もんたです。

私もんた、「[もんたの森](https://www.montanomori.com/)」っていうもんた版イラストやみたいなWebサービスを趣味で開発しているのですが、最近そのもんたの森にRedisを導入しまして、今回はそのお話をしようかなと思います。

この記事を読んで僕と同じかけだしエンジニアの個人開発のモチベーションにつながれば幸いです！

あ、そういえばいろいろやらせてもろてます。
よかったら覗いてみてあげてください。

**【たまーに描いた絵をアップする X ( Twitter ) 】**

https://x.com/monta_no_mori

**【最近始めた Instagram 】**

https://www.instagram.com/monta_no_mori/

**【もんたのLINEスタンプ】**

https://store.line.me/stickershop/author/2887587/ja

## 1. **プロジェクト概要**

### 背景

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/273bed42-e84b-f00b-faa3-ba4596ac4fbc.png)

時は202x年xx月xx日にまで遡る…


👴「こんにはもんたさん。もんたの森のパフォーマンスを高めるためにももんたの森にRedisを導入しませんか？そういえば最近Redisについて勉強していたそうじゃないですか。」

🐶「も、もんたの森にRedisを導入するだって…！？そ、そんなの実装がめんどくさいじゃねぇかよ！！めんどくさい実装をやらせて個人開発のやる気をなくす。そんなこったろ？やる気なくなった俺を見てウイスキーでも飲みながらニヤニヤするつもりだったんだろうが！」

👴「違いますよ。実装？そんなの生成AIに任せればいいじゃないですか。そんな誰もが思いつくような安っぽい方法はもんたの森には似合いません。暇人のあなたに相応しくない。最もジェネレーティブで、最もアーティフィシャルに、そして最もインテリジェンスなやり方でRedisを導入させていただきます。」

🐶「それは助かります。ありがとうございます。がんばります。」

そしてもんたの森にRedisを導入するプロジェクトが始まるのであった。


---

こっからちゃんとした理由。

もんたの森にRedisを実装する目的配下になります。
- Redisについて学んだことのアウトプット
- もんたの森のパフォーマンスを良くしたい

また、以下のNotionページにてスケジュール管理しながら実装を進めました。
たまたま土日空いていた（定期）ので時間とって実装することができました！！

https://monta-database.notion.site/Redis-7c1c2e1ae0f44087a2748b4623285ff7?pvs=4

### もんたの森について

これ見たらわかりやすいよ

https://qiita.com/Maminumemonta0706/items/9e8632698fb6a8329b8c#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%A6%82%E8%A6%81

### きたないPR

汚いコードですが参考にどうぞ。本当に汚いです。本当です…

https://github.com/Hiroto0706/shin-monta-no-mori/pull/26

### 成果

プロジェクトの成果を軽く話しておきます！

以下のPageSpeedInsightを用いて説明すると、Redisを導入したことでPageSpeedInsightのパフォーマンスが90台に乗るようになりました！

![スクリーンショット 2024-09-03 8.06.16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/211831d2-8104-010a-a025-d9d43473fe69.jpeg)

Redisを実装する前はPageSpeedInsightのパフォーマンスの箇所はだいたい80~85とかだったのですが、Redisを実装したことで安定して90前後の値になるようになりました！

Redisを導入したことで、データ読み込みが高速になり、ページのロード時間が短くなったことがパフォーマンス向上の大きな要因かなと考えています。

https://pagespeed.web.dev/analysis/https-www-montanomori-com/0lw2czii12?form_factor=mobile


## 2. **Redisの概要**

Redis(REmote Dictionary Server)について軽ーく説明します。

こちらはChatGPTからの回答です。

```
Redisは、オープンソースのインメモリデータベースで、主に高速なデータ処理やキャッシュとして使われます。
データはメモリ内に保存されるため、非常に速い読み書きが可能です。
データ構造はシンプルで、キーと値のペアで管理され、リスト、セット、ハッシュなどの複数のデータ型をサポートしています。
リアルタイムアプリケーションやセッション管理、ランキングボードなどに適しており、使い方次第で様々な用途に応用できます。
```

他にはこちらの記事とかめちゃくちゃわかりやすかったので参考にどうぞ。

https://qiita.com/keinko/items/60c844bcf329bd3f4af8

まぁ、めっちゃ簡単にいうと**高速にデータを提供するために使われるキャッシュで、データを保存するときはキー・バリュー形式で保存する便利なやつ**って認識でOkかと思います。


## 3. **Redisの実装詳細**

- Redis実装のPR（関係ないコミットも含まれてるんですが、多めに見てください…🙇‍♂️）

https://github.com/Hiroto0706/shin-monta-no-mori/pull/26

こっからは、どのように実装したかについて話していこうかを書いていきます。

今回の実装では`https://github.com/redis/go-redis`というライブラリを使って実装しました。

### Redisを使っている箇所

基本的にもんたの森では、Redisはユーザーサイドで実行されるデータ取得の際に使われます。
だいたいカッコで囲われた部分とかのデータを取得する処理で、Redisは使われています。

![スクリーンショット 2024-09-02 18.49.14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/063dbbc0-f823-97fa-07b4-bc34742813ed.jpeg)

### もんたの森でのRedis実装

以下のファイルがもんたの森のRedis実装におけるすべてです。
このファイルではRedisのクライアントの作成、インターフェースの定義、メソッドの定義などを書いています。

https://github.com/Hiroto0706/shin-monta-no-mori/blob/main/server/internal/cache/redis.go

#### Get & Set メソッド

GetとSetメソッドは以下のように実装しています。

Get、Setメソッドは特に大きな違いはそんなにないです。
Redisではメモリにデータを保存する（Setする）際に、そのキャッシュデータがどれくらいの期間有効か指定する必要があるので、expirationという有効期間を意味する値が1個多いだけになります。

```go
// Set は、データを Redis に保存します。
func (r *RedisContext) Set(ctx context.Context, key string, i interface{}, expiration time.Duration) error {
	data, err := json.Marshal(i)

	if err != nil {
		return fmt.Errorf("failed to marshal data: %w", err)
	}

	if err := r.client.Set(ctx, key, data, expiration).Err(); err != nil {
		return fmt.Errorf("failed to set data in Redis: %w", err)
	}

	return nil
}

// Get は、Redis からデータを取得します。
func (r *RedisContext) Get(ctx context.Context, key string, i interface{}) error {
	data, err := r.client.Get(ctx, key).Result()
	if err != nil {
		if errors.Is(err, redis.Nil) {
			return fmt.Errorf("key %s does not exist in Redis: %w", key, err)
		}

		return fmt.Errorf("failed to get data from Redis: %w", err)
	}

	if err := json.Unmarshal([]byte(data), i); err != nil {
		return fmt.Errorf("failed to unmarshal data: %w", err)
	}

	return nil
}
```

##### なぜデータをMarshal Unmarshal する必要があるのか？

GetとSetにおいてデータのマーシャル、アンマーシャルを行っている理由について説明します。

コメントで「ここ」って書いてある場所が該当します。

```go
func (r *RedisContext) Set(ctx context.Context, key string, i interface{}, expiration time.Duration) error {
	data, err := json.Marshal(i) // ここ

	// ***

	return nil
}

func (r *RedisContext) Get(ctx context.Context, key string, i interface{}) error {
	// ***

	if err := json.Unmarshal([]byte(data), i); err != nil { // ここ
		return fmt.Errorf("failed to unmarshal data: %w", err)
	}

	return nil
}
```

理由としては、Redisはキーに対してバイト列や文字列データを保存するからになります。

Goでは基本的にデータの型は構造体で定義された複雑なデータ型になっていることが多いです。
実際、もんたの森でも構造体で定義した型を使ってデータを扱っていました。

そのような複雑な形式のデータはRedisでは保存することができません。


以下のようなUser構造体のデータをRedisには保存できません。

```go
type (
	User struct {
		Name  string
        Email string
        Age   int
	}
)

user := User{
    Name:  "Monta",
    Email: "monta@example.com",
    Age:   50,
}
```

Redisに保存する際は、以下のようにRedisが扱いやすい形式に変換する必要がある。

```
{
    "Name": "Monta",
    "Email": "monta@example.com",
    "Age": 50
}
```

もんたの森の場合は、RedisにはJSON形式でデータが保存されています。
GetメソッドからJSON形式で取り出したデータは、そのままの形式だと扱いにくいです。

なので、Goで実装されたコードでも扱いやすい形式にするために、アンマーシャルして指定された構造体に変換しています。

```go
	if err := json.Unmarshal([]byte(data), i); err != nil {
		return fmt.Errorf("failed to unmarshal data: %w", err)
	}
```

#### Del メソッド

続いてDelメソッドについての説明です。

Delメソッドは文字通り、Redisから対象のキャッシュデータを削除する際に使われます。

もんたの森の場合は、データの変更が行われた場合、Redisの削除を行わないと最新のデータとキャッシュデータに差が生じるので、「データの追加」「データの更新」「データの削除」が実行された場合に、特定のキーのキャッシュデータを削除するように実装してあります。

```go
// Del は、指定されたキーパターンを含むすべてのキーを Redis から削除します。
func (r *RedisContext) Del(ctx context.Context, patterns []string) error {
	var errs []string
	for _, pattern := range patterns {
		iter := r.client.Scan(ctx, 0, pattern, 0).Iterator()
		for iter.Next(ctx) {
			key := iter.Val()
			if err := r.client.Del(ctx, key).Err(); err != nil {
				errs = append(errs, fmt.Sprintf("failed to delete key %s: %v", key, err))
			}
		}

		if err := iter.Err(); err != nil {
			errs = append(errs, fmt.Sprintf("failed to iterate keys with pattern %s: %v", pattern, err))
		}
	}

	if len(errs) > 0 {
		return fmt.Errorf(strings.Join(errs, ", "))
	}

	return nil
}
```

patternsで渡されてきたパターンを含むキーをすべて取得し、削除処理を実装するようにしています。

例えば、以下のようにRedisにデータ保存されているとします。

```
illustration_1
illustration_2
illustrations_list_offset_0
illustrations_list_offset_1
illustrations_list_by_character_1_0
illustrations_list_by_category_3_0
```

`illustration_*`と`illustrations_list*`というパターンを受け取った場合、`iter := r.client.Scan(ctx, 0, pattern, 0).Iterator()`を実行すると、iterには以下のような値が保持されます。

- **patternが`illustration_*`の時：**
    `illustration_1`と`illustration_2`がiterに代入される。
    これらのキーに対して、Delete処理を実装する。


- **patternが`illustrations_list*`の時:**
    `illustrations_list_offset_0`と`illustrations_list_offset_1`と`illustrations_list_by_character_1_0`と`illustrations_list_by_category_3_0`がiterに代入される。
    これらのキーに対して、Delete処理を実装する。


もんたの森では以下のようにkeyPatternを文字列の配列で保持し、それを引数に渡すことで複数のプレフィックスのキーも削除できるように実装しています。

```go
	// redisキャッシュの削除
	keyPattern := []string{
		cache.IllustrationsPrefix + "*",
		cache.GetIllustrationKey(id),
	}
	err = ctx.Server.RedisClient.Del(ctx, keyPattern)
```

## 4. **Redisのリリース手順**

もんたの森は[Fly.io](https://fly.io/)というPaaSを使ってホスティングしています。

Fly.ioについては『Fly.io とは』とかで調べたらいろんな記事が出てくるのでそちらをご覧ください！

Fly.ioではRedisが使えるので、今回はFly.ioのRedisを使って本番リリースを行いました。


本番リリースはめちゃくちゃ楽にできました。以下の手順を見れば楽に実装できるかと思います。

https://fly.io/docs/upstash/redis/

### もんたの森ではこうやってデプロイした

手順に従って以下のコマンドを実行すると、Fly.ioの環境にRedisが作成されます。
作成が成功したら、Redisの本番用のURLが作成されるかと思います。

```
flyctl redis create
```

生成されるURLは以下のような構造になっています。

```
redis://[username]:[password]@[host]:[port]
```

redisのクライアントにアクセスするには`Addr`、`DB`、`Password`が必要になります。
仮に、RedisURLが以下の場合、引数に渡す値は以下のようになります。

```
redis://default:mypassword@localhost:6379
```

```go
	rds := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		DB:       0, // デフォルトは0
		Password: "mypassword",
	})
```




## 5. **学びと改善点**
### 学び
今回のもんたの森にRedisを導入したことで、以下のことを勉強できました。

- Redisの概要とユースケース
- RedisをGoで実装する方法
- これまで曖昧だったキー・バリューをRedisに保存する方法
- Fly.ioでRedisをデプロイする方法


今回、Redisを実装するにあたって概要を1から勉強し直しました。改めて勉強したことで、曖昧だった部分がさらに明確に理解できた（気がします）。

Redisの実装はなんとなーく仕事で見たことあるし触ったことあるので知ってはいたのですが、やはり人間知っているとやったことがあるには大きい差があると思うので、実際に触って動くものを作ることができてよかったです。
今回の実装で曖昧だったRedisの実装方法を深く理解できたと思います。

### 改善点
実際にデータをセットするとき、データを取得するときのコードがかなり冗長になっているので、そこは関数化するなどのリファクタリングを行うことで、読みやすいコードにしたいです。

いつになるかわかりませんが、以下のNotionでスケジュール管理しながらよしなに進めておこうかなと思います。（がんばっぞ！）

https://monta-database.notion.site/Redis-364d530981c54404836137b7d6b1faee?pvs=4


## 6. **まとめと今後の展望**

いかがでしたでしょうか。

今回は個人で開発した「もんたの森」というWebアプリにRedisを実装したよってことについてお話しさせていただきました！

この記事を読んで少しでも個人開発に興味を持ってくれる人が増えたらなと思います！

今後もエンジニアとして学びを続け、多くのユーザーに毎日使われるサービスを開発できればなと思います！

最後までお読みいただきありがとうございました！🐶

<br />

よかったらこちらも覗いてみてください。

**【たまーに描いた絵をアップする X ( Twitter ) 】**

https://x.com/monta_no_mori

**【最近始めた Instagram 】**

https://www.instagram.com/monta_no_mori/

**【もんたのLINEスタンプ】**

https://store.line.me/stickershop/author/2887587/ja