iT邦幫忙

2025 iThome 鐵人賽

DAY 12
1
Cloud Native

Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟系列 第 12

Go 語言搶票煉金術 Day 12 - 建立連接:在 Go 中與 Redis 對話

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250915/20124462YARMcXUyfa.png

Go 語言搶票煉金術 Day 12 - 建立連接:在 Go 中與 Redis 對話

今天,我們要建立與 Redis 的連接。

但別急著寫程式碼。先問一個 問題為什麼我們需要連接池?為什麼不能每次請求都建立新連接?

## 問題定義:為什麼不能每次都建立新連接?

這個問題的答案很明確。如果每個請求都觸發一次 TCP 握手、認證、執行命令、然後斷開,在高併發下你的系統會死於資源耗盡和延遲。

解決方案很簡單,而且是唯一的標準答案:連接池 (Connection Pool)

兩種模式的開銷差異
https://ithelp.ithome.com.tw/upload/images/20250926/20124462Is0HAPJtq7.png

實作:一個 Redis 客戶端

我們使用 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)
}

https://ithelp.ithome.com.tw/upload/images/20250926/20124462tXM5Y291iG.png

配置參數的「為什麼」

系統參數是從壓力測試和監控中找到的。

PoolSize: 怎麼找?

問題:為什麼是 100?不是 30 或 200?

答案:100 是一個起始猜測。找到它的方法只有一個:

  1. 從一個小的、合理的預設值開始。比如 10 * GOMAXPROCS。這個值足夠小,不會耗盡你的檔案描述符,也足夠應付一定的併發。

  2. 建立監控go-redis 提供了 PoolStats。需要監控 Misses(請求需要建立新連接)和 Timeouts(請求等待連接超時)。把這些指標接入你的監控系統。

  3. 進行壓力測試。模擬你的預期峰值流量,甚至更高。觀察你的監控儀表板。

  4. 根據數據調整

    • 如果 Misses 數量持續很高,說明連接池不夠用,請求正在等待建立新連接,這會增加延遲。適當增加 PoolSize

    • 如果 Timeouts 數量開始增加,說明連接池已經完全耗盡,連新連接都來不及建立。大幅增加 PoolSize,或者檢查你的 Redis 是否已經過載。

    • 如果 TotalConns 遠小於 PoolSize,並且 IdleConns 很多,說明你設置得太大了,正在浪費資源。可以考慮減小 PoolSize

基於我們的壓力測試結果,我們看到資料庫方案在 1500 RPS 時達到瓶頸。
Redis 的效能遠超資料庫,但我們仍需要為每個併發請求準備一個連接。

計算邏輯

  • 預期併發量:1500 RPS
  • 每個請求平均處理時間:1-2ms
  • 理論所需連接數:1500 × 0.002 = 3 個
  • 實際所需連接數:3 × 10 (安全邊際) = 30 個
  • 連接池大小:30 × 6.67 (2-4 倍) ≈ 200 個

ReadTimeout / WriteTimeout: 這是系統的保險絲

問題:為什麼是 3 秒?不是 500 毫秒?

答案:超時不是為了適應 Redis 的速度(它很快),而是為了保護你的應用程式。它定義了你的應用願意為一次 Redis 操作等待多久。

  • 這是一個業務決策:如果一個用戶請求要等待超過 3 秒,你還想讓他等下去嗎?或者你寧願快速失敗,告訴他「請重試」?

  • 這是系統的穩定器:當 Redis 因為某些原因(網路抖動、CPU 跑滿)變慢時,如果沒有超時,你的所有 Go 協程都會被卡住,等待 Redis 回應。很快,你的整個應用程式就會因為資源耗盡而雪崩。短超時(Fail-Fast)會讓有問題的請求失敗,但能保護系統的其餘部分。

500ms 可能是個好數字,3 秒也可能是。
正確的值取決於你的服務等級目標(SLO)和網路環境。
同樣,去測量你的 P99 延遲,然後在它之上加一點緩衝,把它設定為超時。

https://ithelp.ithome.com.tw/upload/images/20250926/2012446213bbRtwL6D.png

重點摘要

  1. 連接池是必須的
  2. 配置來自測試,不是公式:基於壓力測試預期併發量和 Redis 效能特性。
  3. 讓你的系統可觀測go-redis 客戶端本身會處理連接池的健康狀態。

心得小結

今天我們寫了點能用的程式碼,更重要的是,我們學會了如何用正確的方式思考問題。

參考資源


上一篇
Go 語言搶票煉金術 Day 11 - Redis 的工具:搞懂 String 和 Hash
系列文
Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言