
今天,我們要建立與 Redis 的連接。
但別急著寫程式碼。先問一個 問題為什麼我們需要連接池?為什麼不能每次請求都建立新連接?
這個問題的答案很明確。如果每個請求都觸發一次 TCP 握手、認證、執行命令、然後斷開,在高併發下你的系統會死於資源耗盡和延遲。
解決方案很簡單,而且是唯一的標準答案:連接池 (Connection Pool)。
兩種模式的開銷差異
我們使用 github.com/redis/go-redis/v9,這是目前最成熟、最穩定的 Go Redis 客戶端。
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
// NewRedisClient 建立一個 Redis 客戶端。
// 參數直接使用 redis.Options,因為這就是函式庫的 API。
func NewRedisClient(opts *redis.Options) *redis.Client {
return redis.NewClient(opts)
}
func main() {
// 背景 context,用於啟動和測試。
ctx := context.Background()
// 1. 配置 Redis 連接
// 這些參數才是你真正需要關心的。
client := NewRedisClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
// --- 這裡才是關鍵 ---
// PoolSize 應該基於你的併發需求和壓力測試結果來設定。
// 先給一個合理的預設值,例如 CPU 核心數的 4 倍。
PoolSize: 100,
// 最小閒置連接數。保持一些熱連接,避免突發流量下的冷啟動延遲。
MinIdleConns: 10,
// 超時設定是為了保護你的應用程式,而不是 Redis。
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
PoolTimeout: 4 * time.Second, // 從連接池獲取連接的等待時間
})
defer client.Close()
// 2. 啟動時檢查連接 - 做一次就夠了,確保你的環境是正常的。
if err := client.Ping(ctx).Err(); err != nil {
log.Fatalf("無法連接到 Redis: %v", err)
}
fmt.Println("✅ Redis 連接成功!")
// 3. 執行業務邏輯 - 直接使用 client,不要再包一層。
ticketID := int64(1)
initialQuantity := int64(100)
key := fmt.Sprintf("ticket:%d", ticketID)
// 設定初始票券數量
if err := client.Set(ctx, key, initialQuantity, 0).Err(); err != nil {
log.Fatalf("設定票券數量失敗: %v", err)
}
// 驗證設定
quantity, err := client.Get(ctx, key).Int64()
if err != nil {
log.Fatalf("獲取票券數量失敗: %v", err)
}
fmt.Printf("✅ 票券 %d 初始數量: %d\n", ticketID, quantity)
// 4. 測試原子操作
newQuantity, err := client.DecrBy(ctx, key, 10).Result()
if err != nil {
log.Fatalf("減少票券數量失敗: %v", err)
}
fmt.Printf("✅ 減少 10 張票後,剩餘數量: %d\n", newQuantity)
// 5. 監控你的連接池 - 這才是最重要的
stats := client.PoolStats()
fmt.Printf("📊 連接池狀態: Hits=%d, Misses=%d, Timeouts=%d, TotalConns=%d, IdleConns=%d\n",
stats.Hits, stats.Misses, stats.Timeouts, stats.TotalConns, stats.IdleConns)
}

系統參數是從壓力測試和監控中找到的。
問題:為什麼是 100?不是 30 或 200?
答案:100 是一個起始猜測。找到它的方法只有一個:
從一個小的、合理的預設值開始。比如 10 * GOMAXPROCS。這個值足夠小,不會耗盡你的檔案描述符,也足夠應付一定的併發。
建立監控。go-redis 提供了 PoolStats。需要監控 Misses(請求需要建立新連接)和 Timeouts(請求等待連接超時)。把這些指標接入你的監控系統。
進行壓力測試。模擬你的預期峰值流量,甚至更高。觀察你的監控儀表板。
根據數據調整。
如果 Misses 數量持續很高,說明連接池不夠用,請求正在等待建立新連接,這會增加延遲。適當增加 PoolSize。
如果 Timeouts 數量開始增加,說明連接池已經完全耗盡,連新連接都來不及建立。大幅增加 PoolSize,或者檢查你的 Redis 是否已經過載。
如果 TotalConns 遠小於 PoolSize,並且 IdleConns 很多,說明你設置得太大了,正在浪費資源。可以考慮減小 PoolSize。
基於我們的壓力測試結果,我們看到資料庫方案在 1500 RPS 時達到瓶頸。
Redis 的效能遠超資料庫,但我們仍需要為每個併發請求準備一個連接。
計算邏輯:
問題:為什麼是 3 秒?不是 500 毫秒?
答案:超時不是為了適應 Redis 的速度(它很快),而是為了保護你的應用程式。它定義了你的應用願意為一次 Redis 操作等待多久。
這是一個業務決策:如果一個用戶請求要等待超過 3 秒,你還想讓他等下去嗎?或者你寧願快速失敗,告訴他「請重試」?
這是系統的穩定器:當 Redis 因為某些原因(網路抖動、CPU 跑滿)變慢時,如果沒有超時,你的所有 Go 協程都會被卡住,等待 Redis 回應。很快,你的整個應用程式就會因為資源耗盡而雪崩。短超時(Fail-Fast)會讓有問題的請求失敗,但能保護系統的其餘部分。
500ms 可能是個好數字,3 秒也可能是。
正確的值取決於你的服務等級目標(SLO)和網路環境。
同樣,去測量你的 P99 延遲,然後在它之上加一點緩衝,把它設定為超時。

go-redis 客戶端本身會處理連接池的健康狀態。今天我們寫了點能用的程式碼,更重要的是,我們學會了如何用正確的方式思考問題。