王睿

王睿 查看完整档案

深圳编辑中南大学  |  探测制导与控制技术 编辑长亮科技  |  python后台开发 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

王睿 赞了文章 · 2019-03-04

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

原文地址:优化你的应用结构和实现Redis缓存
项目地址:https://github.com/EDDYCJY/go...

如果对你有所帮助,欢迎点个 Star 👍

前言

之前就在想,不少教程或示例的代码设计都是一步到位的(也没问题)

但实际操作的读者真的能够理解透彻为什么吗?左思右想,有了今天这一章的内容,我认为实际经历过一遍印象会更加深刻

规划

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

  • 抽离、分层业务逻辑:减轻 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),
})

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

image

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

如何改?

在左耳朵耗子的文章中,这类代码被称为 “箭头型” 代码,有如下几个问题:

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

写入内容:传送门

(2)、打开 service 目录,新建 cache_service/tag.go

写入内容:传送门

这一部分主要是编写获取缓存 KEY 的方法,直接参考传送门即可

四、Redis 工具包

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

package gredis

import (
    "encoding/json"
    "time"

    "github.com/gomodule/redigo/redis"

    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

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 != "" {
                if _, err := c.Do("AUTH", setting.RedisSetting.Password); 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 interface{}, time int) (bool, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    value, err := json.Marshal(data)
    if err != nil {
        return false, err
    }

    reply, err := redis.Bool(conn.Do("SET", key, value))
    conn.Do("EXPIRE", key, time)

    return reply, err
}

func Exists(key string) bool {
    conn := RedisConn.Get()
    defer conn.Close()

    exists, err := redis.Bool(conn.Do("EXISTS", key))
    if err != nil {
        return false
    }

    return exists
}

func Get(key string) ([]byte, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    reply, err := redis.Bytes(conn.Do("GET", key))
    if err != nil {
        return nil, err
    }

    return reply, nil
}

func Delete(key string) (bool, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    return redis.Bool(conn.Do("DEL", key))
}

func LikeDeletes(key string) error {
    conn := RedisConn.Get()
    defer 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 (
    "github.com/astaxie/beego/validation"

    "github.com/EDDYCJY/go-gin-example/pkg/logging"
)

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

    return
}

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

package app

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/pkg/e"
)

type Gin struct {
    C *gin.Context
}

func (g *Gin) Response(httpCode, errCode int, data interface{}) {
    g.C.JSON(httpCode, gin.H{
        "code": httpCode,
        "msg":  e.GetMsg(errCode),
        "data": data,
    })

    return
}

这样子以后如果要变动,直接改动 app 包内的方法即可

修改既有逻辑

打开 routers/api/v1/article.go,查看修改 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
    }

    articleService := article_service.Article{ID: id}
    exists, err := articleService.ExistByID()
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR_CHECK_EXIST_ARTICLE_FAIL, nil)
        return
    }
    if !exists {
        appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil)
        return
    }

    article, err := articleService.Get()
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR_GET_ARTICLE_FAIL, nil)
        return
    }

    appG.Response(http.StatusOK, e.SUCCESS, article)
}

这里有几个值得变动点,主要是在内部增加了错误返回,如果存在错误则直接返回。另外进行了分层,业务逻辑内聚到了 service 层中去,而 routers/api(controller)显著减轻,代码会更加的直观

例如 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 中,查找不到记录也算一种 “错误” 哦

最后

显然,本章节并不是你跟着我敲系列。我给你的课题是 “实现 Redis 缓存并优化既有的业务逻辑代码”

让其能够不断地适应业务的发展,让代码更清晰易读,且呈层级和结构性

如果有疑惑,可以到 go-gin-example 看看我是怎么写的,你是怎么写的,又分别有什么优势、劣势,取长补短一波?

参考

本系列示例代码

本系列目录

推荐阅读

查看原文

赞 17 收藏 7 评论 3

王睿 赞了回答 · 2018-11-15

解决golang中同一个package中函数互相调用的问题

go run *.go

or

go build .
./run

关注 5 回答 4

王睿 赞了回答 · 2018-07-10

解决如何使用django自动下载刚生成的报表或者文件?

http://blog.csdn.net/qq_18863...
这个你可以参考下,这是生成excel文件并传输的代码。只要把POST动作,关联到这段代码,然后就可以实现在post之后,浏览器自动弹出下载提示咯。

关注 6 回答 5

王睿 赞了回答 · 2018-07-10

解决关于python 状态模式的疑问?

首先为了多态性的考虑,因为python中没有virtual 关键字,所以通过抛出异常的方式来实现虚基类的概念。

再者某个类可以通过设置__set__,__get__特殊方法将其变为数据描述符,这样的好处是,比如后期对于某个属性(该属性绑定了一个数据描述符的类的实例)需要统一开个根号,你就不需要在每个用到它的地方都做修改(如果只是一个简单的整型,你就得在所有引用的地方一个一个改),而是直接修改对应的特殊方法的内部实现就可以了

关注 2 回答 1

王睿 回答了问题 · 2018-07-09

解决使用flume向elasticsearch写日志,下载了jar包后仍然提示有问题?

经过自己查询很多文章了解到,flume所支持elasticsearch版本较为落后,如果要想使用flume直接向es传输,需要使用低版本的es,而且需要修改jar包中的一些方法。flume的更新速度也远不如es的更新速率,所以应该选取其它的日志收集策略

关注 1 回答 1

王睿 赞了回答 · 2018-07-09

解决dockerfile如何定义可变的配置文件?

这个一般放在.env中,不放在Dockerfile

关注 4 回答 3

王睿 回答了问题 · 2018-05-29

解决python有修改配置文件的模块吗?

我最后得到了一种解决方案,其来自新公司的自动发布系统,就是编写好jinja2的配置文件模板,然后使用python注入参数。

关注 3 回答 3

王睿 回答了问题 · 2018-05-25

解决如何在python代码内部将获取的数据分流?

最后通过阅读一些文档发现,如果涉及很密集的运算,那么选择python本来就不明智。Queue的设计也没有考虑大量的流量处理。如果问的问题一直没有答案,那最有可能的就是问题本身就不对。

关注 3 回答 3

王睿 赞了回答 · 2018-05-25

解决发送tcp请求用线程还是进程实现?

用多线程,大体来说,io密集型用线程,计算密集型用多进程

关注 4 回答 3

王睿 提出了问题 · 2018-04-26

解决发送tcp请求用线程还是进程实现?

比如我有这样一个简单的代码块,主要功能是通过flask应用来接收http请求,然后做合法性判断,再将梳理后的数据使用tcp发送给其他模块。那么发送tcp请求这个类应该继承threading.Thread还是multiprocessing.Process比较好呢?目前flask应用承载在主进程上,而发送tcp信息这个算是IO操作吧。因为除了使用tcp发送数据外,还会再监听一个tcp端口来接收数据。

关注 4 回答 3

认证与成就

  • 获得 3 次点赞
  • 获得 46 枚徽章 获得 0 枚金徽章, 获得 9 枚银徽章, 获得 37 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-01-06
个人主页被 473 人浏览