Gin实践 连载十 优化你的应用结构和实现Redis缓存

优化你的应用结构和实现redis缓存

规划

在本章节,将介绍以下功能的整理:

  • 抽离、分层业务逻辑:减轻 routers/*.go 内的 api方法的逻辑(但本文暂不分层 repository,这块逻辑还不重)
  • 增加容错性:对 gorm 的错误进行判断
  • Redis缓存:对获取数据类的接口增加缓存设置
  • 减少重复冗余代码

问题在哪?

在规划阶段我们发现了一个问题,这是目前的伪代码:

if ! HasErrors() {
    if ExistArticleByID(id) {
        DeleteArticle(id)
        code = e.SUCCESS
    } else {
        code = e.ERROR_NOT_EXIST_ARTICLE
    }
} else {
    for _, err := range valid.Errors {
        logging.Info(err.Key, err.Message)
    }
}
c.JSON(http.StatusOK, gin.H{
    "code": code,
    "msg":  e.GetMsg(code),
    "data": make(map[string]string),
})

如果加上规划内的功能逻辑呢,伪代码会变成:

if ! HasErrors() {
    exists, err := ExistArticleByID(id)
    if err == nil {
        if exists {
            err = DeleteArticle(id)
            if err == nil {
                code = e.SUCCESS
            } else {
                code = e.ERROR_XXX
            }
        } else {
            code = e.ERROR_NOT_EXIST_ARTICLE
        }
    } else {
        code = e.ERROR_XXX
    }
} else {
    for _, err := range valid.Errors {
        logging.Info(err.Key, err.Message)
    }
}
c.JSON(http.StatusOK, gin.H{
    "code": code,
    "msg":  e.GetMsg(code),
    "data": make(map[string]string),
})

如果缓存的逻辑也加进来,后面慢慢不断的迭代,岂不是会变成如下图一样?

现在我们发现了问题,应及时解决这个代码结构问题,同时把代码写的清晰、漂亮、易读易改也是一个重要指标

如何改?

在左耳朵耗子的文章中,这类代码被称为 “箭头型” 代码,有如下几个问题:
1、我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服
2、除了宽度外还有长度,有的代码的 if-else 里的 if-else 里的 if-else 的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的
总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的。
简单来说就是让出错的代码先返回,前面把所有的错误判断全判断掉,然后剩下的就是正常的代码了

落实

本项目将对既有代码进行优化和实现缓存,希望你习得方法并对其他地方也进行优化
第一步:完成 Redis 的基础设施建设(需要你先装好 Redis)
第二步:对现有代码进行拆解、分层(不会贴上具体步骤的代码,希望你能够实操一波,加深理解?)

Redis

一、配置

打开 conf/app.ini 文件,新增配置:

[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200

二、缓存Prefix

打开 pkg/e 目录,新建 cache.go,写入内容:

package e

const (
	CACHE_ARTICLE = "ARTICLE"
	CACHE_TAG = "TAG"
)

三、缓存key

(1)、打开 service 目录,新建 cache_service/article.go

点击查看代码
package cache_service

import (
	"gin_log/pkg/e"
	"strconv"
	"strings"
)

type Article struct {
	ID int
	TagID int
	State int

	PageNum int
	PageSize int
}
func (a *Article) GetArticleKey() string {
	return e.CACHE_ARTICLE + "_" + strconv.Itoa(a.ID)
}
func (a *Article) GetArticlesKey() string {
	keys := []string{e.CACHE_ARTICLE, "LIST"}
	if a.ID > 0 {
		keys = append(keys, strconv.Itoa(a.ID))
	}
	if a.TagID > 0 {
		keys = append(keys, strconv.Itoa(a.TagID))
	}
	if a.State > 0 {
		keys = append(keys, strconv.Itoa(a.State))
	}
	if a.PageNum > 0 {
		keys = append(keys, strconv.Itoa(a.PageNum))
	}
	if a.PageSize > 0 {
		keys = append(keys, strconv.Itoa(a.PageSize))
	}
	return strings.Join(keys, "_")
}
(2)、打开 service 目录,新建 cache_service/tag.go
点击查看代码
package cache_service

import (
	"gin_log/pkg/e"
	"strconv"
	"strings"
)

type Tag struct {
	ID int
	Name string
	State int

	PageNum int
	PageSize int
}
func (t *Tag) GetTagsKey() string {
	keys := []string{e.CACHE_TAG, "LIST"}
	if t.Name != "" {
		keys = append(keys, t.Name)
	}
	if t.State >= 0 {
		keys = append(keys, strconv.Itoa(t.State))
	}
	if t.PageNum > 0 {
		keys = append(keys, strconv.Itoa(t.PageNum))
	}
	if t.PageSize > 0 {
		keys = append(keys, strconv.Itoa(t.PageSize))
	}
	return strings.Join(keys, "_")
}
这一部分主要是编写获取缓存 KEY 的方法 ### 四、Redis工具包 安装redis库 `go get github.com/gomodule/redigo@latest` 打开 pkg 目录,新建 gredis/redis.go,写入内容:
点击查看代码
package gredis

import (
	"encoding/json"
	"gin_log/pkg/setting"
	"github.com/gomodule/redigo/redis"
	"time"
)

var redisConn *redis.Pool

func Setup() error {
	redisConn = &redis.Pool{
		MaxIdle: setting.RedisSetting.MaxIdle,  // 最大空闲链接数
		MaxActive: setting.RedisSetting.MaxActive,  // 最大活跃链接数
		IdleTimeout: setting.RedisSetting.IdleTimeout,  // 在这段时间内保持空闲后关闭链接
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", setting.RedisSetting.Host)
			if err != nil {
				return nil, err
			}
			if setting.RedisSetting.Password != "" {
				_, err = c.Do("AUTH", setting.RedisSetting.Password)
				if err != nil {
					_ = c.Close()
					return nil, err
				}
			}
			return c, err
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}
	return nil
}
func Set(key string, data any, time int) error {
	conn := redisConn.Get()
	defer func() {
		_ = conn.Close()
	}()

	value, err := json.Marshal(data)
	if err != nil {
		return err
	}
	_, err = conn.Do("SET", key, value)
	if err != nil {
		return err
	}
	_, err = conn.Do("EXPIRE", key)
	if err != nil {
		return err
	}
	return nil
}
func Exists(key string) bool {
	conn := redisConn.Get()
	defer func() {
		_ = conn.Close()
	}()

	b, _ := redis.Bool(conn.Do("EXISTS", key))
	return b
}
func Get(key string) ([]byte, error) {
	conn := redisConn.Get()
	defer func() {
		_ = conn.Close()
	}()
	replay, err := redis.Bytes(conn.Do("GET", key))
	if err != nil {
		return nil, err
	}
	return replay, err
}
func Delete(key string) (bool, error) {
	conn := redisConn.Get()
	defer func() {
		_ = conn.Close()
	}()

	return redis.Bool(conn.Do("DEL", key))
}
func LikeDeletes(key string) error {
	conn := redisConn.Get()
	defer func() {
		_ = conn.Close()
	}()

	keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
	if err != nil {
		return err
	}

	for _, key := range keys {
		_, err = Delete(key)
		if err != nil {
			return err
		}
	}
	return nil
}
在这里我们做了一些基础功能封装 在这里我们做了一些基础功能封装 1、设置 RedisConn 为 redis.Pool(连接池)并配置了它的一些参数: * Dial:提供创建和配置应用程序连接的一个函数 * TestOnBorrow:可选的应用程序检查健康功能 * MaxIdle:最大空闲连接数 * MaxActive:在给定时间内,允许分配的最大连接数(当为零时,没有限制) * IdleTimeout:在给定时间内将会保持空闲状态,若到达时间限制则关闭连接(当为零时,没有限制) 2、封装基础方法 文件内包含 Set、Exists、Get、Delete、LikeDeletes 用于支撑目前的业务逻辑,而在里面涉及到了如方法: (1)RedisConn.Get():在连接池中获取一个活跃连接 (2)conn.Do(commandName string, args ...interface{}):向 Redis 服务器发送命令并返回收到的答复 (3)redis.Bool(reply interface{}, err error):将命令返回转为布尔值 (4)redis.Bytes(reply interface{}, err error):将命令返回转为 Bytes (5)redis.Strings(reply interface{}, err error):将命令返回转为 []string 在 redigo 中包含大量类似的方法,万变不离其宗,建议熟悉其使用规则和 Redis命令 即可 到这里为止,Redis 就可以愉快的调用啦。 ### 拆解、分层 在先前规划中,引出几个方法去优化我们的应用结构 * 错误提前返回 * 统一返回方法 * 抽离 Service,减轻 routers/api 的逻辑,进行分层 * 增加 gorm 错误判断,让错误提示更明确(增加内部错误码) ### 编写返回方法 要让错误提前返回,c.JSON 的侵入是不可避免的,但是可以让其更具可变性,指不定哪天就变 XML 了呢? 1、打开 pkg 目录,新建 app/request.go,写入文件内容: ``` package app

import (
"gin_log/pkg/logging"
"github.com/astaxie/beego/validation"
)

func MarkErrors(errors []*validation.Error) {
for _, err := range errors {
logging.Error(err.Key, err.Message)
}
return
}

2、打开 pkg 目录,新建 app/response.go,写入文件内容:

package app

import (
"gin_log/pkg/e"
"github.com/gin-gonic/gin"
)

type Gin struct {
C *gin.Context
}
func (g *Gin) Response(httpCode, errorCode int, data any) {
g.C.JSON(httpCode, gin.H{
"code": errorCode,
"msg": e.GetMsg(errorCode),
"data": data,
})
}

这样子以后如果要变动,直接改动 app 包内的方法即可
### 修改既有逻辑
打开 routers/api/v1/article.go,查看修改 GetArticle 方法后的代码为:

// GetArticle 获取单个文章
func GetArticle(c *gin.Context) {
appG := app.Gin{c}
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")

if valid.HasErrors() {
	app.MarkErrors(valid.Errors)
	appG.Response(http.StatusOK, e.INVALID_PARAMS, nil)
	return
}

if !models.ExistArticleByID(id) {
	appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil)
}

article := models.GetArticle(id)
appG.Response(http.StatusOK, e.SUCCESS, gin.H{
	"code": e.SUCCESS,
	"msg": e.GetMsg(e.SUCCESS),
	"data": article,
})

}

这里有几个值得变动点,主要是在内部增加了错误返回,如果存在错误则直接返回。
例如 service/article_service 下的 articleService.Get() 方法:

func (a Article) Get() (models.Article, error) {
var cacheArticle *models.Article
cache := cache_service.Article{ID: a.ID}
key := cache.GetArticleKey()
if gredis.Exists(key) {
data, err := gredis.Get(key)
if err != nil {
logging.Info(err)
} else {
json.Unmarshal(data, &cacheArticle)
return cacheArticle, nil
}
}
article, err := models.GetArticle(a.ID)
if err != nil {
return nil, err
}
gredis.Set(key, article, 3600)
return article, nil
}

而对于 gorm 的 错误返回设置,只需要修改 models/article.go 如下:

func GetArticle(id int) (*Article, error) {
var article Article
err := db.Where("id = ? AND deleted_on = ? ", id, 0).First(&article).Related(&article.Tag).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return &article, nil
}

习惯性增加 .Error,把控绝大部分的错误。另外需要注意一点,在 gorm 中,查找不到记录也算一种 “错误” 哦
posted @ 2022-09-14 17:12  专职  阅读(350)  评论(0编辑  收藏  举报