今天,我們要建立與 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
客戶端本身會處理連接池的健康狀態。今天我們寫了點能用的程式碼,更重要的是,我們學會了如何用正確的方式思考問題。