Redigoを使う(4)　コネクションプールを使う

# はじめに

RedisのGo言語向けクライアントライブラリ[Redigo](https://github.com/gomodule/redigo)の使い方を見ます。
本記事ではコネクションプールの使い方を見ていきます。

# 環境

* OS: Windows 10
* Redis: [win-3.2.100](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.2.100)
* Go言語: [1.11](https://blog.golang.org/go1.11)

# 基本の流れ

コネクションプールとは、Redisサーバへ接続するときにコネクションを再利用することで接続を高速化する機能です。

さっそくですが、Redigoでコネクションプールを利用する典型的なコードを以下に示します。

```go:main.go
package main

import (
	"fmt"
	"time"

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

func newPool(addr string) *redis.Pool {
	return &redis.Pool{
		MaxIdle:     3,
		MaxActive:   0,
		IdleTimeout: 240 * time.Second,
		Dial:        func() (redis.Conn, error) { return redis.Dial("tcp", addr) },
	}
}

func main() {
	// コネクションプールの作成
	pool := newPool("localhost:6379")

	// コネクションの取得
	conn := pool.Get()
	defer conn.Close()

	// 値の書き込み
	w, err := conn.Do("SET", "temperature", 20)
	if err != nil {
		panic(err)
	}
	fmt.Println(w) // OK

	// 値の取得
	r, err := redis.Int(conn.Do("GET", "temperature"))
	if err != nil {
		panic(err)
	}
	fmt.Println(r) // 20
}
```

`redis.Pool`構造体を始めに作成しておき、Redisの操作が必要になるごとに`pool.Get()`でプールからコネクションを取得し、使い終わったら`conn.Close()`する、というのが基本的な使い方です。

ウェブアプリのサーバで使うときは、起動時にプールを作成してグローバル変数などで持っておいて、リクエストをハンドルする関数の先頭で`pool.Get()`と`defer conn.Close()`を行うという書き方になると思います。

# プールのコネクション管理

プールのコネクション管理の仕方と、パラメータについて理解しておきましょう。

プールの起動直後は、プールはコネクションを1つも持たない状態です。`pool.Get`が呼ばれると、プールはサーバとの間で新たなコネクションを確立し、戻り値として返します。そしてそのコネクションで`conn.Close`が呼ばれたとき、プールはサーバとの間のコネクションを切断せずに、「アイドル状態」のコネクションとして蓄えておきます。その後で再度`pool.Get`が呼ばれたときは、プールはアイドル状態のコネクションを「利用中」に変えて戻り値として返します。もしアイドル状態のコネクションがなければ、新しいコネクションを確立します。

デフォルトでは、リソースが許す限りいくつでもコネクションを確立できます。パラメータ`MaxActive`を使うと、個数に上限を設けることができます。その場合、`MaxActive`を超えてコネクションをpool.Getしたとき、そのコネクションでconn.Doなどを呼ぶと`ErrPoolExhausted`エラーが返ってきます。あるいは、パラメータ`Wait`をtrueにしておくと、コネクション数が`MaxActive`を下回るまで、pool.Getの呼び出しをブロッキングで待たせられます。

また、パラメータ`MaxIdle`では、アイドル状態のコネクション数の上限を設けることができます。コネクションの`conn.Close`が呼ばれたとき、既に上限数のアイドルのコネクションが存在していれば、プールはコネクションをアイドル状態にせず、サーバから切断します。

以下は、上記の挙動を理解するためのコードです。アイドルのコネクションの上限数を`MaxIdle = 3`、コネクションの総数の上限を`MaxActive = 6`とした状態で、10個のコネクションの確立と終了を1個ずつ試み、pool.Statsで得たプールの状態をつど出力しています。

```go:main.go
package main

import (
	"fmt"
	"time"

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

func newPool(addr string) *redis.Pool {
	return &redis.Pool{
		MaxIdle:     3,
		MaxActive:   6,
		IdleTimeout: 240 * time.Second,
		Dial:        func() (redis.Conn, error) { return redis.Dial("tcp", addr) },
	}
}

func printStats(pool *redis.Pool) {
	s := pool.Stats()
	fmt.Printf("Active: %d, Idle: %d, InUse: %d\n",
		s.ActiveCount, s.IdleCount, s.ActiveCount-s.IdleCount)
}

func main() {
	// コネクションプールの作成
	pool := newPool("localhost:6379")

	printStats(pool)
	fmt.Println("------")

	// コネクションを作成
	conns := make([]redis.Conn, 10, 10)
	for i := 0; i < 10; i++ {
		conns[i] = pool.Get()
		printStats(pool)
	}
	fmt.Println("------")

	// コネクションをクローズ
	for i := 0; i < 10; i++ {
		conns[i].Close()
		printStats(pool)
	}
}
```

実行結果は以下のようになります。アクティブ（＝使用中＋アイドル）のコネクション数が6を超えておらず、またアイドルの数が3を超えることがない点を確認し下さい。

```
Active: 0, Idle: 0, InUse: 0
------
Active: 1, Idle: 0, InUse: 1
Active: 2, Idle: 0, InUse: 2
Active: 3, Idle: 0, InUse: 3
Active: 4, Idle: 0, InUse: 4
Active: 5, Idle: 0, InUse: 5
Active: 6, Idle: 0, InUse: 6
Active: 6, Idle: 0, InUse: 6
Active: 6, Idle: 0, InUse: 6
Active: 6, Idle: 0, InUse: 6
Active: 6, Idle: 0, InUse: 6
------
Active: 6, Idle: 1, InUse: 5
Active: 6, Idle: 2, InUse: 4
Active: 6, Idle: 3, InUse: 3
Active: 5, Idle: 3, InUse: 2
Active: 4, Idle: 3, InUse: 1
Active: 3, Idle: 3, InUse: 0
Active: 3, Idle: 3, InUse: 0
Active: 3, Idle: 3, InUse: 0
Active: 3, Idle: 3, InUse: 0
Active: 3, Idle: 3, InUse: 0
```

なお、デフォルトでは、アイドル状態のコネクションは永久にクローズされません。`IdleTimeout`または`MaxConnLifetime`のパラメータを設定することで、期限を決めてクローズさせることができます。アイドル状態になってから`IdleTimeout`の時間が経過したコネクションはクローズされます。また確立されてから`MaxConnLifetime`の時間が経過したコネクションもクローズされます。

# おわりに

Redigoのコネクションプールの使い方を見ました。

# 参考

* [Redigoを使う(1)　基本的な使い方](https://qiita.com/riverplus/items/c1df770838d3868c3a13)
* [Redigoを使う(2)　様々なデータ型を扱う](https://qiita.com/riverplus/items/04aa969d699c78f58598)
* [Redigoを使う(3)　トランザクションを行う](https://qiita.com/riverplus/items/5379111adda57f9ac18a)
* [Redigoを使う(5)　ユーティリティ関数](https://qiita.com/riverplus/items/a359ba7e74642a870440)
* [Redigoを使う(6)　パブリッシュ／サブスクライブ](https://qiita.com/riverplus/items/e31c6da6f5af28d3a3f7)