缓存:Redis
在 MySQL 中的大查询或 MongoDB 中的大聚合可能需要几秒甚至几分钟才能完成。你绝对不希望频繁地触发这些操作。
将查询或聚合结果缓存到内存中是缓解这个问题的绝佳方法。如果你的 API 服务器在单个机器或节点上运行,只需将这些结果放入内存中的 HashMaps
或 Dictionaries
中即可解决问题。
但是,如果你有多台机器或节点运行 API 服务器并共享其公共内存的话,则 Redis 才是你的最佳选择。
尝试 Redis
-
在你的机器上安装 Redis 并启动它。
-
添加 redis 依赖。
go get -u github.com/go-redis/redis/v8
- 更新代码。
添加 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 已可以供君驱使了!💐
Loading...
> 此处输出代码运行结果