固然Swoole有连接池,重要的是永葆管道、发表和订阅、连接池等等18新利

go语言的client在redis官网络有超多l客户端,个人以为redigo使用起来更人性化,首要的是源代码结构很清楚,首要的是支撑管道、公布和订阅、连接池等等,所以笔者接受redigo作为尝试.

相见的主题材料

连接池。由于PHP未有连接池,当高产出时就能够有大气的数据库连接直接冲击到MySQL上,最后促成数据库挂掉。尽管Swoole有连接池,不过Swoole只是PHP的叁个恢弘,在此之前使用Swoole进程中就踩过不菲的坑。经过大家的探究或许认为使用Golang越发可控一些。

golang操作redis重要有四个库,go-redis和redigo。两个操作都比较轻松,差异上redigo更像叁个client实行各样操作都以由此Do函数去做的,redis-go对函数的卷入更加好,相比较之下redigo操作redis显得微微麻烦。然则官方更推荐redigo,所以项目中自己利用了redigo。

1.redigo的安装

极端运维: go get github.com/garyburd/redigo/redis ,安装完成后在$GOPATH/src下有相关的文件目录

这是正常的安装方式,不知道什么原因,这块我死活安装不成功.

不可能,从github网络一向下载redigo-master.zip工程包,将redis文件夹直接拷贝到$GOPATH/src/github.com/garyburd/redigo

框架的挑肥拣瘦

在PHP中央司法机关接用的是Yaf,所以在Go中自然来说就选拔了Gin。因为大家直接以来的尺度是:尽量相近底层代码。

卷入过于宏观的框架不便利对全体类别的掌握控制及驾驭。笔者不供给你告诉自身那么些目录是干嘛的,那一个布局怎么写,这么些函数怎么用等等。

Gin是八个轻路由框架,很吻合我们的须求。为了更加好地付出,大家也做了几当中间件。

1.连接redis

2.测试redigo

虚构机运行redis 服务,张开LiteIDE,新扩展七个go文件:

// RedisTest project main.go
package main

import (
    "fmt"

    "github.com/garyburd/redigo/redis"
)

func main() {

    c, err := redis.Dial("tcp", "192.168.74.128:6379")
    if err != nil {
        fmt.Println(err)
        return
    }
    //密码授权
    c.Do("AUTH", "123456")
    c.Do("SET", "a", 134)
    a, err := redis.Int(c.Do("GET", "a"))

    fmt.Println(a)

    defer c.Close()
}

编译生成后运转,打字与印刷出不错结果.

中间件——input

各样接口都亟需得到GET或POST的参数,然而gin自带的情势只好回去string,所以大家开展了简便的卷入。封装过后大家就足以凭借所需直接转变到想要的数据类型。

package input

import (
    "strconv"
)

type I struct {
    body string
}

func (input *I) get(p string) *I {
    d, e := Context.GetQuery(p)
    input.body = d
    if e == false {
        return input
    }

    return input
}

func (input *I) post(p string) *I {
    d, e := Context.GetPostForm(p)
    input.body = d
    if e == false {
        return input
    }

    return input
}

func (input *I) String() string {
    return input.body
}

func (input *I) Atoi() int {
    body, _ := strconv.Atoi(input.body)
    return body
}

package input

//获取GET参数
func Get(p string) *I {
    i := new(I)
    return i.get(p)
}

//获取POST参数
func Post(p string) *I {
    i := new(I)
    return i.get(p)
}

打包早先

pid, _ := strconv.Atoi(c.Query("product_id"))
alias := c.Query("product_alias")

装进之后

  pid := input.Get("product_id").Atoi()
  alias := input.Get("product_alias").String()
package redisclientimport ("fmt"redigo "github.com/garyburd/redigo/redis")var pool *redigo.Poolfunc init() {redis_host := "127.0.0.1"redis_port := 6379pool_size := 20pool = redigo.NewPool (redigo.Conn, error) {c, err := redigo.Dial("tcp", fmt.Sprintf("%s:%d", redis_host, redis_port))if err != nil {return nil, err}return c, nil}, pool_size)}func Get() redigo.Conn {return pool.Get()}

中间件——logger

gin自个儿的logger比较轻易,常常大家都急需将日志按日期分文件写到有个别目录下。所以大家友好重写了贰个logger,那一个logger能够兑现将日志按日期分文件并将错误消息发送给Sentry。

package ginx

import (
    "fmt"
    "io"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    "sao.cn/configs"
)

var (
    logPath string
    lastDay int
)

func init() {
    logPath = configs.Load().Get("SYS_LOG_PATH").(string)
    _, err := os.Stat(logPath)
    if err != nil {
        os.Mkdir(logPath, 0755)
    }
}

func defaultWriter() io.Writer {
    writerCheck()
    return gin.DefaultWriter
}

func defaultErrorWriter() io.Writer {
    writerCheck()
    return gin.DefaultErrorWriter
}

func writerCheck() {
    nowDay := time.Now().Day()
    if nowDay != lastDay {
        var file *os.File
        filename := time.Now().Format("2006-01-02")
        logFile := fmt.Sprintf("%s/%s-%s.log", logPath, "gosapi", filename)

        file, _ = os.Create(logFile)
        if file != nil {
            gin.DefaultWriter = file
            gin.DefaultErrorWriter = file
        }
    }

    lastDay = nowDay
}

package ginx

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/url"
    "time"

    "github.com/gin-gonic/gin"
    "gosapi/application/library/output"
    "sao.cn/sentry"
)

func Logger() gin.HandlerFunc {
    return LoggerWithWriter(defaultWriter())
}

func LoggerWithWriter(outWrite io.Writer) gin.HandlerFunc {
    return func(c *gin.Context) {
        NewLog(c).CaptureOutput().Write(outWrite).Report()
    }
}

const (
    LEVEL_INFO  = "info"
    LEVEL_WARN  = "warning"
    LEVEL_ERROR = "error"
    LEVEL_FATAL = "fatal"
)

type Log struct {
    startAt time.Time
    conText *gin.Context
    writer  responseWriter
    error   error

    Level     string
    Time      string
    ClientIp  string
    Uri       string
    ParamGet  url.Values `json:"pGet"`
    ParamPost url.Values `json:"pPost"`
    RespBody  string
    TimeUse   string
}

func NewLog(c *gin.Context) *Log {
    bw := responseWriter{buffer: bytes.NewBufferString(""), ResponseWriter: c.Writer}
    c.Writer = &bw

    clientIP := c.ClientIP()
    path := c.Request.URL.Path
    method := c.Request.Method
    pGet := c.Request.URL.Query()
    var pPost url.Values
    if method == "POST" {
        c.Request.ParseForm()
        pPost = c.Request.PostForm
    }
    return &Log{startAt: time.Now(), conText: c, writer: bw, Time: time.Now().Format(time.RFC850), ClientIp: clientIP, Uri: path, ParamGet: pGet, ParamPost: pPost}
}

func (l *Log) CaptureOutput() *Log {
    l.conText.Next()
    o := new(output.O)
    json.Unmarshal(l.writer.buffer.Bytes(), o)
    switch {
    case o.Status_code != 0 && o.Status_code < 20000:
        l.Level = LEVEL_ERROR
        break
    case o.Status_code > 20000:
        l.Level = LEVEL_WARN
        break
    default:
        l.Level = LEVEL_INFO
        break
    }

    l.RespBody = l.writer.buffer.String()
    return l
}

func (l *Log) CaptureError(err interface{}) *Log {
    l.Level = LEVEL_FATAL
    switch rVal := err.(type) {
    case error:
        l.RespBody = rVal.Error()
        l.error = rVal
        break
    default:
        l.RespBody = fmt.Sprint(rVal)
        l.error = errors.New(l.RespBody)
        break
    }

    return l
}

func (l *Log) Write(outWriter io.Writer) *Log {
    l.TimeUse = time.Now().Sub(l.startAt).String()
    oJson, _ := json.Marshal(l)
    fmt.Fprintln(outWriter, string(oJson))
    return l
}

func (l *Log) Report() {
    if l.Level == LEVEL_INFO || l.Level == LEVEL_WARN {
        return
    }

    client := sentry.Client()
    client.SetHttpContext(l.conText.Request)
    client.SetExtraContext(map[string]interface{}{"timeuse": l.TimeUse})
    switch {
    case l.Level == LEVEL_FATAL:
        client.CaptureError(l.Level, l.error)
        break
    case l.Level == LEVEL_ERROR:
        client.CaptureMessage(l.Level, l.RespBody)
        break
    }
}

出于Gin是一个轻路由框架,所以肖似数据库操作和Redis操作并未有对应的包。那就需求大家团结去选用好用的包。

而后大家调用redisclient包中的.Get(卡塔尔国就足以生成贰个redis连接池对象来操作redis

Package - 数据库操作

前期学习阶段选用了datbase/sql,然则那些包有个用起来很优伤的难题。

pid := 10021
rows, err := db.Query("SELECT title FROM `product` WHERE id=?", pid)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    var title string
    if err := rows.Scan(&title); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %dn", title, pid)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

上述代码,假诺select的不是title,而是*,那时就须要超前把表构造中的全部字段都定义成多个变量,然后传给Scan方法。

那般,假如一张表中有13个以上字段的话,开垦进度就能要命麻烦。那么我们期待的是什么样吗。提前定义字段是必需的,但是平常的话应该是概念成一个构造体吧? 大家目的在于的是询问后方可一贯将查询结果调换到布局化数据。

花了点时间搜索,终于找到了这么叁个包——github.com/jmoiron/sqlx。

    // You can also get a single result, a la QueryRow
    jason = Person{}
    err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
    fmt.Printf("%#vn", jason)
    // Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"}

    // if you have null fields and use SELECT *, you must use sql.Null* in your struct
    places := []Place{}
    err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
    if err != nil {
        fmt.Println(err)
        return
    }

sqlx其实是对database/sql的增加,那样一来开荒起来是或不是就爽多了,嘎嘎~

干什么不用ORM? 如故上生龙活虎节说过的,尽量不要过于包装的包。

2.操作redis

Package - Redis操作

最先大家利用了redigo【github.com/garyburd/redigo/redis】,使用上倒是未有怎么不适的,可是在压测的时候开掘三个题目,即连接池的利用。

func factory(name string) *redis.Pool {
    conf := config.Get("redis." + name).(*toml.TomlTree)
    host := conf.Get("host").(string)
    port := conf.Get("port").(string)
    password := conf.GetDefault("passwd", "").(string)
    fmt.Printf("conf-redis: %s:%s - %srn", host, port, password)

    pool := &redis.Pool{
        IdleTimeout: idleTimeout,
        MaxIdle:     maxIdle,
        MaxActive:   maxActive,
        Dial: func() (redis.Conn, error) {
            address := fmt.Sprintf("%s:%s", host, port)
            c, err := redis.Dial("tcp", address,
                redis.DialPassword(password),
            )
            if err != nil {
                exception.Catch(err)
                return nil, err
            }

            return c, nil
        },
    }
    return pool
}

/**
 * 获取连接
 */
func getRedis(name string) redis.Conn {
    return redisPool[name].Get()
}

/**
 * 获取master连接
 */
func Master(db int) RedisClient {
    client := RedisClient{"master", db}
    return client
}

/**
 * 获取slave连接
 */
func Slave(db int) RedisClient {
    client := RedisClient{"slave", db}
    return client
}

以上是概念了多个连接池,这里就生出了二个标题,在redigo中施行redis命令时是内需活动从连接池中拿到连接,而在利用后还必要团结将连接放回连接池。最早我们正是未有将一而再一连放回去,以致压测的时候一贯压不上去。

这正是说有未有更加好的包呢,答案当然是迟早的 —— gopkg.in/redis.v5

func factory(name string) *redis.Client {
    conf := config.Get("redis." + name).(*toml.TomlTree)
    host := conf.Get("host").(string)
    port := conf.Get("port").(string)
    password := conf.GetDefault("passwd", "").(string)
    fmt.Printf("conf-redis: %s:%s - %srn", host, port, password)

    address := fmt.Sprintf("%s:%s", host, port)
    return redis.NewClient(&redis.Options{
        Addr:        address,
        Password:    password,
        DB:          0,
        PoolSize:    maxActive,
    })
}

/**
 * 获取连接
 */
func getRedis(name string) *redis.Client {
    return factory(name)
}

/**
 * 获取master连接
 */
func Master() *redis.Client {
    return getRedis("master")
}

/**
 * 获取slave连接
 */
func Slave() *redis.Client {
    return getRedis("slave")
}

能够看看,那些包正是直接再次回到必要的接连了。

那便是说大家去看一下她的源码,连接有未有放回去啊。

func (c *baseClient) conn() (*pool.Conn, bool, error) {
    cn, isNew, err := c.connPool.Get()
    if err != nil {
        return nil, false, err
    }
    if !cn.Inited {
        if err := c.initConn(cn); err != nil {
            _ = c.connPool.Remove(cn, err)
            return nil, false, err
        }
    }
    return cn, isNew, nil
}

func (c *baseClient) putConn(cn *pool.Conn, err error, allowTimeout bool) bool {
    if internal.IsBadConn(err, allowTimeout) {
        _ = c.connPool.Remove(cn, err)
        return false
    }

    _ = c.connPool.Put(cn)
    return true
}

func (c *baseClient) defaultProcess(cmd Cmder) error {
    for i := 0; i <= c.opt.MaxRetries; i++ {
        cn, _, err := c.conn()
        if err != nil {
            cmd.setErr(err)
            return err
        }

        cn.SetWriteTimeout(c.opt.WriteTimeout)
        if err := writeCmd(cn, cmd); err != nil {
            c.putConn(cn, err, false)
            cmd.setErr(err)
            if err != nil && internal.IsRetryableError(err) {
                continue
            }
            return err
        }

        cn.SetReadTimeout(c.cmdTimeout(cmd))
        err = cmd.readReply(cn)
        c.putConn(cn, err, false)
        if err != nil && internal.IsRetryableError(err) {
            continue
        }

        return err
    }

    return cmd.Err()
}

能够观望,在此个包中的平底操作会先去connPool中Get一个连接,用完之后又试行了putConn方法将三番五次放回connPool。

package mainimport ("fmt""redisclient""github.com/garyburd/redigo/redis")func main() {c := redisclient.Get()//记得销毁本次链连接defer c.Close()//写入数据_, err := c.Do("SET", "go_key", "redigo")if err != nil {fmt.Println("err while setting:", err)}//判断key是否存在is_key_exit, err := redis.Bool(c.Do("EXISTS", "go_key"))if err != nil {fmt.Println("err while checking keys:", err)} else {fmt.Println(is_key_exit)}//获取value并转成字符串account_balance, err := redis.String(c.Do("GET", "go_key"))if err != nil {fmt.Println("err while getting:", err)} else {fmt.Println(account_balance)}//删除key_, err = c.Do("DEL", "go_key")if err != nil {fmt.Println("err while deleting:", err)}//设置key过期时间_, err = c.Do("SET", "mykey", "superWang", "EX", "5")if err != nil {fmt.Println("err while setting:", err)}//对已有key设置5s过期时间n, err := c.Do("EXPIRE", "go_key", 5)if err != nil {fmt.Println("err while expiring:", err)} else if n != int64 {fmt.Println}}

结束语

package main

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

    "gosapi/application/library/initd"
    "gosapi/application/routers"
)

func main() {
    env := initd.ConfTree.Get("ENVIRONMENT").(string)
    gin.SetMode(env)

    router := gin.New()
    routers.Register(router)

    router.Run(":7321") // listen and serve on 0.0.0.0:7321
}

七月十六日起先写main,今后早已上线一个星期了,暂且尚未察觉什么难题。

透过压测比较,在品质上进步了大要上四倍左右。原先响合时间在70微秒左右,未来是10皮秒左右。原先的吞吐量差不离在1200左右,以往是3300左右。

固然Go很棒,然而本身照旧想说:PHP是最佳的言语!

  

但愿对我们享有助于~