サーバーサイドGo入門 #5 ~redisを使う~
前回はこちら prelude.hatenablog.jp
今回作るもの
redisを用いてReadのレスポンスを早めましょう。
ユーザーIDの追加
redisの導入にあたりまずユーザーIDをTODOに追加します。このユーザーIDをredisのキーとして用いTODOを保存します。
type TODO struct { gorm.Model // これ UserID int `json:"userID"` Author string `json:"author"` Name string `json:"name"` Description string `json:"description"` DueDate time.Time `json:"dueDate"` }
ユーザーIDの追加に伴い、TODO一覧APIをユーザーIDを指定して取得するものに変更しましょう。
func list(c *gin.Context) { userID := c.Param("user_id") todos := make([]TODO, 0) if err := gormDB.WithContext(c).Find(&todos, "user_id = ?", userID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, todos) } func setupRouter() *gin.Engine { router := gin.Default() router.GET("/todos/:user_id", list) router.POST("/todo", add) return router }
redisの導入
redis clientの初期化
では早速redisを導入しましょう。使用するライブラリは下記です。import時にv8
をつける必要があることに注意しましょう。
github.com
redisのクライアントもgorm同様にグローバルに定義し、main関数で初期化します。
// これと var redisClient *redis.Client func main() { _gormDB, err := gorm.Open(mysql.Open("root:mysql@tcp(127.0.0.1:3306)/todo?parseTime=true"), &gorm.Config{}) if err != nil { panic(err) } sqlDB, err := _gormDB.DB() if err != nil { panic(err) } defer sqlDB.Close() if !_gormDB.Migrator().HasTable(&TODO{}) { if err := _gormDB.Migrator().CreateTable(&TODO{}); err != nil { panic(err) } } gormDB = _gormDB // これ redisClient = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) log.Println(setupRouter().Run(":8080")) }
このクライアントを用いるように修正します。
redisを使う
今回はredisを下記のように使います。
list
:ユーザーIDをキーとしたデータがredisにあればそれを使用。なければDBから取得し、redisに保存。add
:ユーザーIDをキーとしてredisに保存されているTODOを削除。
ちなみに、redisにsetする際に下記のinterfaceを要求されます。
type BinaryMarshaler interface { MarshalBinary() (data []byte, err error) }
encoding - The Go Programming Language
そのため、set前にjsonにencodingしている点に注意してください。実際のコードはこのようになります。
func list(c *gin.Context) { userID := c.Param("user_id") todos := make([]TODO, 0) if b, err := redisClient.Get(c, userID).Bytes(); err == nil { if err := json.Unmarshal(b, &todos); err == nil { c.JSON(http.StatusOK, todos) return } } if err := gormDB.WithContext(c).Find(&todos, "user_id = ?", userID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } j, err := json.Marshal(todos) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := redisClient.Set(c, userID, j, 5*time.Minute).Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, todos) } func add(c *gin.Context) { if c.Request.Body == http.NoBody { c.JSON(http.StatusBadRequest, gin.H{"error": "body is required"}) return } var todo TODO if err := json.NewDecoder(c.Request.Body).Decode(&todo); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "body is invalid"}) return } if err := gormDB.WithContext(c).Create(&todo).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := redisClient.Del(c, strconv.Itoa(todo.UserID)).Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, todo) }
redisを動かす
redisのイメージを取得し、動かします。
$ docker pull redis $ docker run --rm --name redis -p 6379:6379 redis redis-server --appendonly yes
curlで確認
下記の通りcurlを行い確認します。2度目のTODO一覧の取得時間が非常に短いこと、TODOを追加した後のTODO一覧の取得が1度目の取得と同じ程度に遅くなることを確認してください。
$ curl -X POST http://localhost:8080/todo -d \ '{"userID": 1, "author": "Rob", "name": "Develop Generics", "description": "Add feature of generic type system", "dueDate": "2022-12-31T00:00:00Z" }' | jq
$ curl http://locahost:8080/todos/1 | jq
コード全文
// main.go package main import ( "encoding/json" "log" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" "gorm.io/driver/mysql" "gorm.io/gorm" ) var gormDB *gorm.DB var redisClient *redis.Client type TODO struct { gorm.Model UserID int `json:"userID"` Author string `json:"author"` Name string `json:"name"` Description string `json:"description"` DueDate time.Time `json:"dueDate"` } func list(c *gin.Context) { userID := c.Param("user_id") todos := make([]TODO, 0) if b, err := redisClient.Get(c, userID).Bytes(); err == nil { if err := json.Unmarshal(b, &todos); err == nil { c.JSON(http.StatusOK, todos) return } } if err := gormDB.WithContext(c).Find(&todos, "user_id = ?", userID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } j, err := json.Marshal(todos) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := redisClient.Set(c, userID, j, 5*time.Minute).Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, todos) } func add(c *gin.Context) { if c.Request.Body == http.NoBody { c.JSON(http.StatusBadRequest, gin.H{"error": "body is required"}) return } var todo TODO if err := json.NewDecoder(c.Request.Body).Decode(&todo); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "body is invalid"}) return } if err := gormDB.WithContext(c).Create(&todo).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := redisClient.Del(c, strconv.Itoa(todo.UserID)).Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, todo) } func setupRouter() *gin.Engine { router := gin.Default() router.GET("/todos/:user_id", list) router.POST("/todo", add) return router } func main() { _gormDB, err := gorm.Open(mysql.Open("root:mysql@tcp(127.0.0.1:3306)/todo?parseTime=true"), &gorm.Config{}) if err != nil { panic(err) } sqlDB, err := _gormDB.DB() if err != nil { panic(err) } defer sqlDB.Close() if !_gormDB.Migrator().HasTable(&TODO{}) { if err := _gormDB.Migrator().CreateTable(&TODO{}); err != nil { panic(err) } } gormDB = _gormDB redisClient = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) log.Println(setupRouter().Run(":8080")) }
まとめ
現実のサーバーサイドの開発ではキャッシュは非常によく行われるものと思います。今回はredisを深く理解して使うことよりも、まずGoで扱ってみて最初の1歩のハードルを低くすることを目的としました。
redisを使いキャッシュしていますが今回のコードでは速くなりません。そもそも処理が少ないため、redisとのやり取りによるオーバーヘッドの方が高くつきます。
雑談
redisの後はgoroutineとgPRCの導入を書こうと思っていましたが失踪しそうです。