首页 > golang > golang与redis连接池的那些注意事项
2021
01-26

golang与redis连接池的那些注意事项

一、什么是连接池,连接池有什么用

先看看别人是怎么介绍连接池的吧:

连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等。也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

下面我来简单解释一下,因为每次 Redis客户端连接 Redis服务端都需要一段时间,而处理各种操作的时间很多时候都很短,如果每次进行各种操作时都需要重新连接 Redis,那么就会浪费大量时间。因此 Redis引入连接池,连接池可以实现建立多个客户端连接而不释放,避免浪费IO资源,不使用的时候就放在连接池,这样就减少了连接数据库所需要的时间,提高效率。

连接池就是建一个池子和一定量的管道。每次当管道被取尽时,就不能继续消耗IO资源了,这样就保证了IO资源不会耗尽。

二、代码展示

func GetRedis(config_name string) *redis.Pool {
   maxIdle := viper.GetInt(config_name + ".max_idle")
   maxActive := viper.GetInt(config_name + ".max_active")
   address := viper.GetString(config_name + ".dsn")
   password := viper.GetString(config_name + ".password")
   database := viper.GetInt(config_name + ".database")

   //该值表示如果连接池的的连接闲置超过该值就会关闭连接。如果该值为零,连接池中闲置的连接就不会关闭。应用程序应该设置这个限制超时时间不超过服务端的限制超时时间。
   idleTimeoutInt := viper.GetInt(config_name + ".idle_timeout")
   idleTimeout := time.Duration(idleTimeoutInt) * time.Second

   timeoutInt := viper.GetInt(config_name + ".timeout")
   timeout := time.Duration(timeoutInt) * time.Second
   // 建立连接池
   return &redis.Pool{
      MaxIdle:     maxIdle,  //最大闲置连接数
      MaxActive:   maxActive,  //最大活跃连接数,0代表无限
      IdleTimeout: idleTimeout,  //闲置连接的超时时间
      Wait:        false,
      Dial: func() (redis.Conn, error) {
         con, err := redis.Dial("tcp", address,
            redis.DialPassword(password),
            redis.DialDatabase(database),
            redis.DialConnectTimeout(timeout),
            redis.DialReadTimeout(timeout),
            redis.DialWriteTimeout(timeout))
         if err != nil {
            panic("failed to init redis pool,err:" + err.Error())
         }
         return con, nil
      },
      TestOnBorrow: func(c redis.Conn, t time.Time) error {
         _, err := c.Do("PING")
         return err
      },
   }
}
func demo(){
    rds := common.RedisPool.Get()
    if rds.Err() == nil {
       //延时关闭连接
       defer rds.Close()
    }
    rds.Do("SELECT", 0)
    key := "test:count"
    do, err := rds.Do("GET", key)
    count, err := redis.Int(do, err)
    count++
    rds.Do("INCRBY", key, 5)
    stat := common.RedisPool.Stats()
}


如果一个人占着管道不用,就会被闲置。如果在闲置处太久不动,达到闲置连接的超时时间,就会被请走。



三、底层详解

Redigo Pool 最重要的结构

type Pool struct {
    // 真正获取跟redis-server连接的函数, 必填参数
    Dial func() (Conn, error)  

    // 这是个可选参数, 用于在从 pool 获取连接时, 检查这个连接是否正常使用. 所以这个参数一般是必填的
    TestOnBorrow func(c Conn, t time.Time) error 

    // 最多有多少个空闲连接保留, 一般必填
    MaxIdle int

    // 最多有多少活跃的连接数, 一般必填
    MaxActive int

    // 空闲连接最长空闲时间, 一般必填
    IdleTimeout time.Duration

    // Pool 的活跃的连接数达到 MaxActive, 如果 Wait 为 true, Get会阻塞
    // 那么 Get() 将要等待一个连接放到 Pool中, 才会返回一个连接给使用方
    Wait bool

    // 设置连接最大存活时间
    MaxConnLifetime time.Duration
    
    chInitialized uint32 // set to 1 when field ch is initialized
    mu     sync.Mutex    // mu protects the following fields
    closed bool          // 设置 Pool 是否关闭
    active int           // 当前 Pool 的活跃连接数
    ch     chan struct{} // 配合 Wait 为 true 使用
    idle   idleList      // 空闲队列
}


特别注意事项

顺带说一句在 `pool.go` 里面总共有两个 Close() 函数:

1. func (p *Pool) Close() error {...}

这个函数是关闭 redigo 连接池的. 理论上可以不调用. 如果确实不放心, 需要在 main.go 里面 `defer pool.Close()` 
来调用

2. func (ac *activeConn) Close() error { ...}

这个函数是用来将从 Pool 中获取到的 activeConn 放回到 Pool 里面. 

这个函数是我们需要频繁调用的函数. 如果程序里Get() 之后没有 Close(), 那么就会造成 redis 连接泄漏.
更严重的情况, 如果Wait, MaxActive 都没有设置, 那么你的程序就会将 redis 搞瘫痪, 这是很危险的

3. Get连接以后,主动执行 rds.Do("SELECT", 0),切换到自己需要操作的库,因为默认取出来的链接,不一定默认select 0


Redigo 灵魂函数 -- put()

put 函数主要提供给 activeConn.Close() 调用

Close() 函数就不在详细说明, 主要根据 activeConn 的 stat, 判断在关闭连接之前是否发送过WATCHMULTIPSUBSCRIBESUBSCRIBEMONITOR 这些命令. 如果发送过就会把这些命令结束

func (p *Pool) put(pc *poolConn, forceClose bool) error {
    p.mu.Lock()
    // 判断 pool 是否关闭, 并且该命令是否需要强制关闭
    if !p.closed && !forceClose { 
        pc.t = nowFunc()
        // 将该 activeConn 压入 idleList 中
        p.idle.pushFront(pc)
        // 如果 idleList 的 count 已经大于 MaxIdle, 那么会将 idleList 的尾部的 activeConn pop 掉
        if p.idle.count > p.MaxIdle {
            pc = p.idle.back
            p.idle.popBack()
        } else {
            pc = nil
        }
    }

    // 如果是需要强制关闭或者是从尾部 pop 掉的 conn, 那么就会真正的关闭这个连接
    if pc != nil {
        p.mu.Unlock()
        pc.c.Close()
        p.mu.Lock()
        p.active--
    }

    // 如果开启了 Wait = true, 那么往 channel 里面发送一个struct{}{}, 代表等待的客户端可以获取连接了
    if p.ch != nil && !p.closed {
        p.ch <- struct{}{}
    }
    p.mu.Unlock()
    return nil
}


当Pool.get获取的连接,并没有保存在连接池中,而是当activeConn.Close()时,才调用put,保存连接。所以我们刚开始初始化连接池的时候,连接池里面的活跃链接和空闲连接其实都是空的,并没有任何可用的链接,只有当Get到链接,并且Close以后才会放入到连接池中!!!


我们可以验证一下,刚开始初始化好连接池的时候,打印连接池的状态:pool.Stats() ,其中的

ActiveCount  0 
IdleCount    0


这时候你Get一下再去打印 Stats()就会发现

ActiveCount  1     //本次get产生的,但是IdleCount 还是0
IdleCount    0

怎么让IdleCount > 0 呢,我们可以想一下,当我们多次Get,然后模拟一段处理时间比较长的业务,比如sleep 10秒,这时候3次请求同时开始,因为连接池没有空闲连接,所以每次Get都是新建立的链接,这时候等他们都执行完,都会Close,也就是上面说的都会put回连接池,这时候我们再看Stat ,就会发现连接池有空闲连接了。

ActiveCount  3     //3次get产生的
IdleCount    0


超级总结


  1. Get 以后必须 conn.Close()

  2. 如果中间用到多个db,select db以后,放回连接池的默认还是切换后的db,所以Get之后最好是显示select db 切换到自己想要操作的db

  3. IdleTimeout 必须设置的小于redis服务器的timeout,并且要小于nat的超时时间(一般5分钟)

本文》有 0 条评论

留下一个回复