本文 AI 產出,尚未審核

Golang 資料庫操作 – NoSQL 資料庫(MongoDB、Redis)

簡介

在現代的 Web 與微服務架構中,NoSQL 資料庫 已成為不可或缺的組件。相較於傳統的關聯式資料庫,MongoDB 與 Redis 提供了彈性的資料模型與極佳的效能,特別適合處理大量的非結構化資料、快取需求或即時訊息傳遞。

對於使用 Golang 開發的後端服務而言,掌握如何在程式中正確、有效率地操作這兩種 NoSQL 資料庫,能大幅提升系統的可擴展性與開發速度。本篇文章將從核心概念出發,搭配實作範例,說明在 Go 中與 MongoDB、Redis 互動的最佳方式,並分享常見陷阱與實務應用情境,讓初學者與中階開發者都能快速上手。


核心概念

1. MongoDB 基礎

MongoDB 是一個文件導向的資料庫,資料以 BSON(類 JSON)格式存放於 collection 中。Go 官方提供的驅動 go.mongodb.org/mongo-driver 支援完整的 CRUD、索引、聚合等功能。

1.1 連線與設定

package main

import (
    "context"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
    // 建立 10 秒的 timeout context,避免連線卡住
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // MongoDB URI,建議使用環境變數或密碼管理服務
    clientOpts := options.Client().ApplyURI("mongodb://localhost:27017")
    client, err := mongo.Connect(ctx, clientOpts)
    if err != nil {
        log.Fatalf("MongoDB 連線失敗: %v", err)
    }

    // 確認連線狀態
    if err = client.Ping(ctx, nil); err != nil {
        log.Fatalf("MongoDB Ping 失敗: %v", err)
    }

    log.Println("MongoDB 連線成功")
    // 使用完畢記得關閉連線
    defer client.Disconnect(context.Background())
}

重點:使用 context 來控制連線與操作的逾時,避免因網路問題造成程式卡死。

1.2 基本 CRUD

package main

import (
    "context"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type User struct {
    ID        primitive.ObjectID `bson:"_id,omitempty"` // 自動產生的 ObjectID
    Name      string             `bson:"name"`
    Age       int                `bson:"age"`
    CreatedAt time.Time          `bson:"created_at"`
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
    coll := client.Database("demo").Collection("users")

    // --- Insert ---
    newUser := User{Name: "Alice", Age: 28, CreatedAt: time.Now()}
    insertRes, err := coll.InsertOne(ctx, newUser)
    if err != nil {
        log.Fatalf("Insert 錯誤: %v", err)
    }
    log.Printf("插入文件 ID: %v\n", insertRes.InsertedID)

    // --- Find ---
    var result User
    filter := bson.M{"name": "Alice"}
    if err = coll.FindOne(ctx, filter).Decode(&result); err != nil {
        log.Fatalf("Find 錯誤: %v", err)
    }
    log.Printf("查詢結果: %+v\n", result)

    // --- Update ---
    update := bson.M{"$set": bson.M{"age": 29}}
    updateRes, _ := coll.UpdateOne(ctx, filter, update)
    log.Printf("更新筆數: %d\n", updateRes.ModifiedCount)

    // --- Delete ---
    deleteRes, _ := coll.DeleteOne(ctx, filter)
    log.Printf("刪除筆數: %d\n", deleteRes.DeletedCount)
}

說明

  • bson.M 為 map 型別的快捷寫法,用於建構查詢或更新條件。
  • InsertOneFindOneUpdateOneDeleteOne 為最常用的單筆操作;若需批次處理,可改用 InsertManyUpdateMany 等。

1.3 索引與效能

在大量資料的查詢情境下,索引是提升效能的關鍵。以下示範在 age 欄位建立升冪索引,並說明如何檢查索引建立結果。

indexModel := mongo.IndexModel{
    Keys:    bson.D{{Key: "age", Value: 1}}, // 1 為升冪,-1 為降冪
    Options: options.Index().SetName("idx_age"),
}
if _, err := coll.Indexes().CreateOne(ctx, indexModel); err != nil {
    log.Fatalf("建立索引失敗: %v", err)
}
log.Println("索引 idx_age 建立完成")

最佳實踐:對於頻繁使用的查詢條件(如 user_idemail)務必建立索引;同時避免在大量寫入的欄位上建立過多索引,會造成寫入延遲。


2. Redis 基礎

Redis 是一個 鍵值快取 資料庫,支援字串、哈希、列表、集合、有序集合等多種資料結構,且提供 Pub/Sub、Lua 腳本、事務等高階功能。Go 中最常用的官方客戶端是 github.com/go-redis/redis/v9

2.1 連線與設定

package main

import (
    "context"
    "log"
    "time"

    "github.com/go-redis/redis/v9"
)

func main() {
    // 建立一個 5 秒的 timeout context
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis 伺服器位址
        Password: "",               // 若有密碼請填寫
        DB:       0,                // 使用的 DB 編號,預設 0
    })

    // Ping 測試連線
    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatalf("Redis 連線失敗: %v", err)
    }
    log.Println("Redis 連線成功")
}

提示:在生產環境建議使用 連線池(Redis 客戶端內建)以及 TLS 加密,確保安全與效能。

2.2 常見資料結構操作

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/go-redis/redis/v9"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})

    // --- String ---
    if err := rdb.Set(ctx, "username", "bob", 0).Err(); err != nil {
        log.Fatalf("Set 錯誤: %v", err)
    }
    name, _ := rdb.Get(ctx, "username").Result()
    fmt.Printf("username = %s\n", name)

    // --- Hash ---
    userKey := "user:1001"
    rdb.HSet(ctx, userKey, "name", "Alice", "age", 30)
    age, _ := rdb.HGet(ctx, userKey, "age").Int()
    fmt.Printf("user age = %d\n", age)

    // --- List (Queue) ---
    queueKey := "task:queue"
    rdb.RPush(ctx, queueKey, "task1", "task2", "task3")
    task, _ := rdb.LPop(ctx, queueKey).Result()
    fmt.Printf("取出任務: %s\n", task)

    // --- Set ---
    rdb.SAdd(ctx, "tags", "golang", "redis", "mongodb")
    members, _ := rdb.SMembers(ctx, "tags").Result()
    fmt.Printf("Set 成員: %v\n", members)

    // --- Pub/Sub ---
    go func() {
        sub := rdb.Subscribe(ctx, "news")
        ch := sub.Channel()
        for msg := range ch {
            fmt.Printf("收到訊息: %s\n", msg.Payload)
        }
    }()
    // 發布訊息
    if err := rdb.Publish(ctx, "news", "Hello Redis!").Err(); err != nil {
        log.Fatalf("Publish 錯誤: %v", err)
    }
}

說明

  • SetGet 為最基本的字串操作。
  • HSetHGet 用於儲存結構化資料(類似關聯式資料庫的一筆記錄)。
  • RPushLPop 可實作 FIFO 工作佇列。
  • Publish/Subscribe 為即時訊息傳遞的典型應用。

2.3 使用 Redis 作為快取層

以下示範 Cache-Aside(先查快取,若未命中再查資料庫)的典型流程,適用於查詢頻繁、更新較少的資料。

func GetUserProfile(ctx context.Context, rdb *redis.Client, db *mongo.Collection, userID string) (*User, error) {
    cacheKey := fmt.Sprintf("user:profile:%s", userID)

    // 1. 嘗試從 Redis 讀取
    if data, err := rdb.Get(ctx, cacheKey).Result(); err == nil {
        var user User
        if err := json.Unmarshal([]byte(data), &user); err == nil {
            // 命中快取
            return &user, nil
        }
    }

    // 2. 快取未命中 → 從 MongoDB 讀取
    var user User
    filter := bson.M{"_id": userID}
    if err := db.FindOne(ctx, filter).Decode(&user); err != nil {
        return nil, err
    }

    // 3. 把結果寫回 Redis,設定過期時間(例如 5 分鐘)
    if bytes, err := json.Marshal(user); err == nil {
        rdb.Set(ctx, cacheKey, bytes, 5*time.Minute)
    }

    return &user, nil
}

關鍵:快取的 TTL(Time‑To‑Live) 必須根據資料變動頻率調整,避免「陳舊資料」問題。


常見陷阱與最佳實踐

類別 常見問題 解決方案 / 最佳實踐
MongoDB 忘記在 context 中設定 timeout,導致程式卡死。 使用 context.WithTimeout,並在每次 DB 操作前傳入。
大量寫入時未建立批次寫入(bulk)或交易,效能低下。 使用 mongo.Collection.BulkWritesession.StartTransaction
索引未同步,查詢仍走全表掃描。 explain 查詢計畫,確保使用索引;定期檢查 db.collection.getIndexes()
Redis 直接使用 Set 存放大量資料,導致記憶體飽和。 設定合理的 TTL,或使用 LRU/LFU 策略(maxmemory-policy)。
Pub/Sub 訊息遺失(消費者斷線)。 考慮使用 Redis StreamsXADDXREADGROUP)提供持久化與消費者組。
在高併發環境下忘記使用 pipeline,造成大量往返 RTT。 使用 client.Pipeline()client.TxPipeline() 合併多個指令。
共通 直接把敏感連線資訊硬編碼在程式碼。 使用環境變數、Vault、或 Kubernetes Secret 管理機密。
忽略錯誤回傳,導致隱蔽的失敗。 所有 DB/Cache 呼叫務必檢查 err,並適當記錄或回傳。

實際應用場景

場景 為何選擇 MongoDB 為何選擇 Redis
使用者檔案 資料結構多變(可擴充的 JSON),需要支援複雜查詢與聚合。 讀取頻繁、更新較少,可使用快取提升讀取效能。
即時排行榜 需要聚合大量交易紀錄,使用 Aggregation Pipeline 計算分數。 有序集合(ZSET)提供 O(log N) 的排序與範圍查詢,適合即時排行榜。
工作佇列 任務資料較大、需要持久化,且可能需要失敗重試。 使用 List 或 Streams 作為輕量級佇列,結合 BRPOPXREADGROUP
會話管理 需要儲存使用者的多筆關聯資料(如購物車、偏好設定)。 Session ID → Hash 結構,配合 TTL 自動過期。
即時聊天 訊息儲存與搜尋(關鍵字、時間範圍)適合 MongoDB。 Pub/Sub 或 Streams 用於即時訊息推送。

總結

  • MongoDB 提供彈性的文件模型與強大的聚合功能,適合儲存結構多變、需要複雜查詢的資料。使用 Go 時,務必搭配 context、適當的索引與批次寫入,以取得最佳效能。
  • Redis 則是高速的鍵值快取與即時訊息平台,支援多種資料結構。透過 pipeline、TTL、以及適當的資料結構選擇(String、Hash、ZSET、Stream),可以大幅降低延遲、提升系統吞吐。

在實務開發中,兩者往往結合使用:MongoDB 作為主要的永續存儲,Redis 作為快取層或即時通訊管道。只要遵循上述的最佳實踐與注意常見陷阱,就能在 Golang 生態系中構建出高效、可擴展的 NoSQL 解決方案。祝開發順利,期待看到你把這些技巧運用到自己的專案中! 🚀