Connection Pooling

For concurrent applications like web servers, managing individual Redis connections can be inefficient and error-prone. A connection pool provides a managed set of shared connections, improving performance and reliability.

Redigo's redis.Pool is a thread-safe connection pool that manages a collection of redis.Conn instances.

Creating a Pool

The recommended way to create a pool is to initialize a redis.Pool struct.

func newPool(addr string) *redis.Pool {
  return &redis.Pool{
    MaxIdle: 3,                 // Maximum number of idle connections in the pool.
    MaxActive: 0,               // Maximum number of connections allocated by the pool at a given time. When zero, there is no limit.
    IdleTimeout: 240 * time.Second, // Close connections after remaining idle for this duration.
    Dial: func () (redis.Conn, error) { 
        return redis.Dial("tcp", addr) 
    },
  }
}

var pool = newPool("localhost:6379")

Pool Configuration

The redis.Pool struct has several fields to control its behavior:

  • Dial func() (redis.Conn, error): (Required) A function that creates and returns a new connection.
  • DialContext func(context.Context) (redis.Conn, error): An alternative to Dial that accepts a context.
  • MaxIdle int: The maximum number of idle connections to keep in the pool. If MaxIdle is zero, no idle connections are kept.
  • MaxActive int: The maximum number of connections that can be allocated from the pool at any time. If MaxActive is zero, there is no limit.
  • IdleTimeout time.Duration: If a connection has been idle for longer than this duration, it will be closed. If zero, idle connections are not closed. This should be set to a value less than the Redis server's timeout.
  • Wait bool: If true and MaxActive is reached, Get() will block until a connection becomes available. If false, Get() will return an ErrPoolExhausted error immediately.
  • MaxConnLifetime time.Duration: Connections older than this duration will be closed, even if they are active.
  • TestOnBorrow func(c redis.Conn, t time.Time) error: An optional function to check the health of a connection before it's returned to the application. If the function returns an error, the connection is closed.

Getting and Using a Connection

To get a connection from the pool, call the Get() method. It's crucial to Close() the connection when you're done with it. Calling Close() on a pooled connection returns it to the pool, it does not close the underlying network connection.

The standard pattern is to use defer:

func handleRequest(w http.ResponseWriter, r *http.Request) {
    conn := pool.Get()
    // Use defer to ensure the connection is returned to the pool.
    defer conn.Close()

    // Use the connection to execute commands.
    reply, err := conn.Do("GET", "my-key")
    if err != nil {
        // handle error
        return
    }
    // ... process reply
}

For context-aware operations, you can use pool.GetContext(ctx).

Health Checks

The TestOnBorrow function is useful for preventing the use of stale or broken connections. For example, you can PING the server for connections that have been idle for a while.

pool := &redis.Pool{
    // ... other configuration
    TestOnBorrow: func(c redis.Conn, t time.Time) error {
        if time.Since(t) < time.Minute {
            return nil
        }
        _, err := c.Do("PING")
        return err
    },
}