こんにちは、KOUKIです。
GolangのWebフレームワークであるfiberを使ってAPIを開発しています。
前回は、ページネーション機能を実装しました。
今回は、キャッシュを更新する方法を紹介します。
尚、本記事は「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
<目次>
前回
作るもの
Ambassdor機能を作りたいと思います。エンドポイントは、次の通りです。
- GET /api/ambassador/products/frontend
- GET /api/ambassador/products/backend
- POST /api/ambassador/links
- GET /api/ambassador/stats
- GET /api/ambassador/rankings
キャッシュ更新の問題点
以前、RedisにProductデータをキャッシングする実装を行いましたが、キャッシュ更新の課題が残っていました。
例えば、idが「1」のデータは現在、以下のようになっています。

1 2 3 4 5 6 7 |
{ "id": 1, "title": "tkcJbpq", "description": "RUcXZlT", "image": "http://tbKWSFP.net/rkODPuv.html", "price": 33 }, |
このデータを以下のパラメータで更新したいと思います。
- URL: http://localhost:8000/api/admin/products/1
- 形式: PUT
1 2 3 4 5 6 |
{ "title": "update", "description": "update", "image": "img_update", "price": 3.3 } |

更新が完了できたら、もう一度商品を取得してみましょう。

1 2 3 4 5 6 7 |
{ "id": 1, "title": "tkcJbpq", "description": "RUcXZlT", "image": "http://tbKWSFP.net/rkODPuv.html", "price": 33 }, |
更新前のデータを取得しましたね。これは、キャッシュを更新していないためです。
キャッシュ削除
まず、キャッシュを削除する処理を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// database/redis.go ... func SetupCacheChannel() { CacheChannel = make(chan string) go func(ch chan string) { for { time.Sleep(3 * time.Second) key := <-ch // メッセージが入ってくるまでブロック // キャッシュ削除 Cache.Del(context.Background(), key) log.Printf("Cache cleared %s", key) } }(CacheChannel) } func ClearCache(keys ...string) { for _, key := range keys { CacheChannel <- key } } |
ここでは、goroutineを使って実装しています。これをfor文と組み合わせて実行しているので、fiber APIとは別プロセスでキャッシュを削除するプロセスを立ち上げられます。
Cache.Delメソッドは、Redisに格納したデータを削除します。RedisはKey-Valueデータストアなので、Keyを指定すればサクッと削除できます。
Keyは、呼び出し元からChannelを介して取得します。goroutineを使って別プロセスで動かした場合、Channelを介さないとデータを送ることができません。
このキャッシュチャネルのプロセスは、main.goから動かしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
// main.go ... func main() { // Connection To Mysql // Migration // Redis // Cache database.SetupCacheChannel() // fiber API ... } |
キャッシュの更新タイミング
キャッシュは、「/api/ambassador/products/frontend」や「/api/ambassador/products/backend」へリクエストを送り、DBからデータを取得後、呼び出し元にリプライするタイミングで登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// productController.go func ProductFrontend(ctx *fiber.Ctx) error { var products []models.Product var c = context.Background() expiredTime := 30 * time.Minute // products_frontend keyでRedisからデータを取得 result, err := database.Cache.Get(c, "products_frontend").Result() if err != nil { database.DB.Find(&products) // Redisにデータを格納するため、エンコードする productBytes, err := json.Marshal(&products) if err != nil { panic(err) } // products_fronend keyでRedisにデータを格納 err = database.Cache.Set(c, "products_frontend", productBytes, expiredTime).Err() if err != nil { panic(err) } } else { // デコードする json.Unmarshal([]byte(result), &products) } return ctx.JSON(products) } |
そして、データを再取得するときに、Redisにキャッシュされたデータを返します。
キャッシュは、データの状態が変わったタイミングで更新されれば、とても良いですね。そうすれば、データを取得するときに、常に最新データを取得できます。
よって、プロダクトの作成、更新、削除のタイミングで、キャッシュを削除しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// productController.go func CreateProducts(ctx *fiber.Ctx) error { ... // プロダクト取得 ... // キャッシュ削除(goroutineで呼び出す) go database.ClearCache("products_frontend", "products_backend") ... } func UpdateProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 ... // プロダクト更新 ... // キャッシュ削除(goroutineで呼び出す) go database.ClearCache("products_frontend", "products_backend") return ctx.JSON(product) } func DeleteProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 ... // プロダクト削除 ... // キャッシュ削除(goroutineで呼び出す) go database.ClearCache("products_frontend", "products_backend") return nil } |
検証
キャッシュが更新されるか検証してみましょう。
先ほどと同様に、商品を更新してからデータを取得します。
データの更新

データの再取得

1 2 3 4 5 6 7 |
{ "id": 1, "title": "update", "description": "update", "image": "img_update", "price": 3.3 }, |
更新後のデータを取得することができました!
コンソールにも、以下のログが出力されています。
1 2 |
backend_1 | 2021/06/18 23:04:56 Cache cleared products_frontend backend_1 | 2021/06/18 23:04:59 Cache cleared products_backend |
次回
次回は、LinkとStatsを実装しましょう。
Go言語まとめ
ソースコード
ここまでのソースコードを以下に記載します。
productController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
// productController.go package controllers import ( "admin/src/database" "admin/src/models" "context" "encoding/json" "sort" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" ) var ( products_frontend = "products_frontend" products_backend = "products_backend" ) func Products(ctx *fiber.Ctx) error { var products []models.Product // 全てのプロダクトを取得 database.DB.Find(&products) return ctx.JSON(products) } func CreateProducts(ctx *fiber.Ctx) error { var product models.Product // リクエストデータをパースする if err := ctx.BodyParser(&product); err != nil { return err } // プロダクト取得 database.DB.Create(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return ctx.JSON(product) } func GetProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) var product models.Product product.ID = uint(id) // プロダクト検索 database.DB.Find(&product) return ctx.JSON(product) } func UpdateProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) product := models.Product{} product.ID = uint(id) if err := ctx.BodyParser(&product); err != nil { return err } // プロダクト更新 database.DB.Model(&product).Updates(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return ctx.JSON(product) } func DeleteProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) product := models.Product{} product.ID = uint(id) // プロダクト削除 database.DB.Delete(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return nil } func ProductFrontend(ctx *fiber.Ctx) error { var products []models.Product var c = context.Background() expiredTime := 30 * time.Minute // products_frontend keyでRedisからデータを取得 result, err := database.Cache.Get(c, products_frontend).Result() if err != nil { database.DB.Find(&products) // Redisにデータを格納するため、エンコードする productBytes, err := json.Marshal(&products) if err != nil { panic(err) } // products_fronend keyでRedisにデータを格納 err = database.Cache.Set(c, products_frontend, productBytes, expiredTime).Err() if err != nil { panic(err) } } else { // デコードする json.Unmarshal([]byte(result), &products) } return ctx.JSON(products) } func ProductBackend(ctx *fiber.Ctx) error { var products []models.Product var c = context.Background() expiredTime := 30 * time.Minute // キャッシュ操作 result, err := database.Cache.Get(c, products_backend).Result() if err != nil { database.DB.Find(&products) productBytes, err := json.Marshal(&products) if err != nil { panic(err) } database.Cache.Set(c, products_backend, productBytes, expiredTime).Err() } else { json.Unmarshal([]byte(result), &products) } var searchProducts []models.Product // 検索 // urlの?q=XXXXから文字列を取得 if q := ctx.Query("q"); q != "" { // 大文字小文字の区別をなくすため、全て小文字扱いにする lower := strings.ToLower(q) for _, product := range products { // 検索条件1: Title if strings.Contains(strings.ToLower(product.Title), lower) || // 検索条件2: Description strings.Contains(strings.ToLower(product.Description), lower) { searchProducts = append(searchProducts, product) } } } else { // 検索しない場合は、全てのデータを返却 searchProducts = products } // ソート if sortParam := ctx.Query("sort"); sortParam != "" { sortLower := strings.ToLower(sortParam) if sortLower == "asc" { sort.Slice(searchProducts, func(i, j int) bool { return searchProducts[i].Price < searchProducts[j].Price }) } else if sortLower == "desc" { sort.Slice(searchProducts, func(i, j int) bool { return searchProducts[i].Price > searchProducts[j].Price }) } } // ページネーション var total = len(searchProducts) // デフォルトは"1"ページ page, _ := strconv.Atoi(ctx.Query("page", "1")) // 1ページ最大9個の商品 perPage := 9 var data []models.Product if total <= page*perPage && total >= (page-1)*perPage { data = searchProducts[(page-1)*perPage : total] } else if total >= page*perPage { data = searchProducts[(page-1)*perPage : page*perPage] } else { data = []models.Product{} } // 1ページ目 -> 0 ~ 8 // 2パージ目 -> 9 ~ 17 return ctx.JSON(fiber.Map{ "data": data, "total": total, "page": page, "last_page": total/perPage + 1, }) } |
redis.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// database/redis.go package database import ( "context" "log" "time" "github.com/go-redis/redis/v8" ) var Cache *redis.Client var CacheChannel chan string func SetupRedis() { Cache = redis.NewClient(&redis.Options{ // docker-compose.ymlに指定したservice名+port Addr: "redis:6379", DB: 0, }) } func SetupCacheChannel() { CacheChannel = make(chan string) go func(ch chan string) { for { time.Sleep(3 * time.Second) key := <-ch // メッセージが入ってくるまでブロック // キャッシュ削除 Cache.Del(context.Background(), key) log.Printf("Cache cleared %s", key) } }(CacheChannel) } func ClearCache(keys ...string) { for _, key := range keys { CacheChannel <- key } } |
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// main.go package main import ( "admin/src/database" "admin/src/routes" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) func main() { // Connection To Mysql database.Connect() // Migration database.AutoMigrate() // Redis database.SetupRedis() // Cache database.SetupCacheChannel() // fiber API app := fiber.New() // CORSの設定 app.Use(cors.New(cors.Config{ // 認証にcookieなどの情報を必要とするかどうか AllowCredentials: true, })) // Setup Routes routes.Setup(app) app.Listen(":3000") } |
コメントを残す
コメントを投稿するにはログインしてください。