» Go:使用Gin构建REST API » 2. 开发 » 2.9 缓存:Redis

缓存:Redis

在 MySQL 中的大查询或 MongoDB 中的大聚合可能需要几秒甚至几分钟才能完成。你绝对不希望频繁地触发这些操作。

将查询或聚合结果缓存到内存中是缓解这个问题的绝佳方法。如果你的 API 服务器在单个机器或节点上运行,只需将这些结果放入内存中的 HashMapsDictionaries 中即可解决问题。 但是,如果你有多台机器或节点运行 API 服务器并共享其公共内存的话,则 Redis 才是你的最佳选择。

尝试 Redis

  1. 在你的机器上安装 Redis 并启动它。

  2. 添加 redis 依赖。

go get -u github.com/go-redis/redis/v8
  1. 更新代码。

添加 infrastructure/cache/helper.go

package cache

import "context"

type Helper interface {
	Save(ctx context.Context, key, value string) error
	Load(ctx context.Context, key string) (string, error)
}

使用 redis,infrastructure/cache/redis.go

/*
Package cache 包含所有缓存实现。
*/
package cache

import (
	"context"
	"time"

	"github.com/go-redis/redis/v8"

	"literank.com/rest-books/infrastructure/config"
)

const (
	defaultTimeout = time.Second * 10
	defaultTTL     = time.Hour * 1
)

type RedisCache struct {
	c redis.UniversalClient
}

func NewRedisCache(c *config.CacheConfig) *RedisCache {
	timeout := defaultTimeout
	if c.Timeout > 0 {
		timeout = time.Second * time.Duration(c.Timeout)
	}
	r := redis.NewClient(&redis.Options{
		Addr:         c.Address,
		Password:     c.Password,
		DB:           c.DB,
		ReadTimeout:  timeout,
		WriteTimeout: timeout,
	})
	return &RedisCache{
		c: r,
	}
}

// Save sets key and value into the cache
func (r *RedisCache) Save(ctx context.Context, key, value string) error {
	if _, err := r.c.Set(ctx, key, value, defaultTTL).Result(); err != nil {
		return err
	}
	return nil
}

// Load reads the value by the key
func (r *RedisCache) Load(ctx context.Context, key string) (string, error) {
	value, err := r.c.Get(ctx, key).Result()
	if err != nil {
		if err == redis.Nil {
			return "", nil
		}
		return "", err
	}
	return value, nil
}

添加相应配置项,infrastructure/config/config.go

@@ -8,8 +8,9 @@ import (
 )
 
 type Config struct {
-       App ApplicationConfig `json:"app" yaml:"app"`
-       DB  DBConfig          `json:"db" yaml:"db"`
+       App   ApplicationConfig `json:"app" yaml:"app"`
+       Cache CacheConfig       `json:"cache" yaml:"cache"`
+       DB    DBConfig          `json:"db" yaml:"db"`
 }
 
 type DBConfig struct {
@@ -23,6 +24,13 @@ type ApplicationConfig struct {
        Port int `json:"port" yaml:"port"`
 }
 
+type CacheConfig struct {
+       Address  string `json:"address" yaml:"address"`
+       Password string `json:"password" yaml:"password"`
+       DB       int    `json:"db" yaml:"db"`
+       Timeout  int    `json:"timeout" yaml:"timeout"`
+}
+
 // Parse parses config file and returns a Config.
 func Parse(filename string) (*Config, error) {
        buf, err := os.ReadFile(filename)

放入对应配置值,config.yml

@@ -5,3 +5,8 @@ db:
   dsn: "test_user:test_pass@tcp(127.0.0.1:3306)/lr_book?charset=utf8mb4&parseTime=True&loc=Local"
   mongo_uri: "mongodb://localhost:27017"
   mongo_db_name: "lr_book"
+cache:
+  address: localhost:6379
+  password: test_pass
+  db: 0
+  timeout: 50

合入 redis 连接,application/wire_helper.go

@@ -2,6 +2,7 @@ package application
 
 import (
        "literank.com/rest-books/domain/gateway"
+       "literank.com/rest-books/infrastructure/cache"
        "literank.com/rest-books/infrastructure/config"
        "literank.com/rest-books/infrastructure/database"
 )
@@ -10,6 +11,7 @@ import (
 type WireHelper struct {
        sqlPersistence   *database.MySQLPersistence
        noSQLPersistence *database.MongoPersistence
+       kvStore          *cache.RedisCache
 }
 
 func NewWireHelper(c *config.Config) (*WireHelper, error) {
@@ -21,7 +23,8 @@ func NewWireHelper(c *config.Config) (*WireHelper, error) {
        if err != nil {
                return nil, err
        }
-       return &WireHelper{sqlPersistence: db, noSQLPersistence: mdb}, nil
+       kv := cache.NewRedisCache(&c.Cache)
+       return &WireHelper{sqlPersistence: db, noSQLPersistence: mdb, kvStore: kv}, nil
 }
 
 func (w *WireHelper) BookManager() gateway.BookManager {
@@ -31,3 +34,7 @@ func (w *WireHelper) BookManager() gateway.BookManager {
 func (w *WireHelper) ReviewManager() gateway.ReviewManager {
        return w.noSQLPersistence
 }
+
+func (w *WireHelper) CacheHelper() cache.Helper {
+       return w.kvStore
+}

假设列出所有图书操作需要在数据库中执行一个大查询,你需要将查询结果存入 Redis 以便下次可快速访问。

更改 application/executor/book_operator.go

@@ -2,17 +2,22 @@ package executor
 
 import (
        "context"
+       "encoding/json"
 
        "literank.com/rest-books/domain/gateway"
        "literank.com/rest-books/domain/model"
+       "literank.com/rest-books/infrastructure/cache"
 )
 
+const booksKey = "lr-books"
+
 type BookOperator struct {
        bookManager gateway.BookManager
+       cacheHelper cache.Helper
 }
 
-func NewBookOperator(b gateway.BookManager) *BookOperator {
-       return &BookOperator{bookManager: b}
+func NewBookOperator(b gateway.BookManager, c cache.Helper) *BookOperator {
+       return &BookOperator{bookManager: b, cacheHelper: c}
 }
 
 func (o *BookOperator) CreateBook(ctx context.Context, b *model.Book) (*model.Book, error) {
@@ -29,7 +34,32 @@ func (o *BookOperator) GetBook(ctx context.Context, id uint) (*model.Book, error
 }
 
 func (o *BookOperator) GetBooks(ctx context.Context) ([]*model.Book, error) {
-       return o.bookManager.GetBooks(ctx)
+       rawValue, err := o.cacheHelper.Load(ctx, booksKey)
+       if err != nil {
+               return nil, err
+       }
+
+       books := make([]*model.Book, 0)
+       if rawValue != "" {
+               // 缓存 key 存在
+               if err := json.Unmarshal([]byte(rawValue), &books); err != nil {
+                       return nil, err
+               }
+       } else {
+               // 缓存 key 不存在
+               books, err = o.bookManager.GetBooks(ctx)
+               if err != nil {
+                       return nil, err
+               }
+               value, err := json.Marshal(books)
+               if err != nil {
+                       return nil, err
+               }
+               if err := o.cacheHelper.Save(ctx, booksKey, string(value)); err != nil {
+                       return nil, err
+               }
+       }
+       return books, nil
 }
 
 func (o *BookOperator) UpdateBook(ctx context.Context, id uint, b *model.Book) (*model.Book, error) {

微调 adaptor/router.go

@@ -22,7 +22,7 @@ type RestHandler struct {
 
 func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
        rest := &RestHandler{
-               bookOperator:   executor.NewBookOperator(wireHelper.BookManager()),
+               bookOperator:   executor.NewBookOperator(wireHelper.BookManager(), wireHelper.CacheHelper()),
                reviewOperator: executor.NewReviewOperator(wireHelper.ReviewManager()),
        }
        // Create a new Gin router

这些就是引入 redis 所需的调整。现在让我们试下缓存驱动的新端点。

用 curl 进行测试

列出所有图书:

curl -X GET http://localhost:8080/books

结果与之前相似,但是性能显著提升。你可以通过 Gin 框架的日志看出迹象。

[GIN] 2024/02/26 - 15:29:58 | 200 |    3.313312ms |       127.0.0.1 | GET      "/books"
[GIN] 2024/02/26 - 15:30:26 | 200 |     749.161µs |       127.0.0.1 | GET      "/books"
[GIN] 2024/02/26 - 15:30:31 | 200 |      635.58µs |       127.0.0.1 | GET      "/books"

使用 redis-cli 查看 Redis 中的值:

redis-cli

在 redis 客户端 shell 中调试这些键值:

127.0.0.1:6379> keys *
1) "lr-books"
127.0.0.1:6379> get lr-books
"[{\"id\":1,\"title\":\"Great Book II\",\"author\":\"Carl Smith\",\"published_at\":\"2022-01-01T08:00:00+08:00\",\"description\":\"Another sample book description\",\"isbn\":\"8334567890\",\"total_pages\":3880,\"created_at\":\"2024-02-25T16:29:31.353+08:00\",\"updated_at\":\"2024-02-25T16:29:31.353+08:00\"}]"
127.0.0.1:6379> del lr-books
(integer) 1

赞!Redis 已可以供君驱使了!💐