← 返回
后端开发 2026.03.06

Go工程師體系課 007

后端开发

實體結構說明

本模塊包含以下核心實體:

  • 商品(Goods)
  • 商品分類(Category)
  • 品牌(Brands)
  • 輪播圖(Banner)
  • 品牌分類(GoodsCategoryBrand)

1. 商品(Goods)

描述平臺中實際展示和銷售的商品信息。

字段說明

字段名類型說明
nameString商品名稱,必填
brandPointer -> Brands商品所屬品牌,關聯 Brands 實體
categoryPointer -> Category商品所屬分類,關聯 Category 實體
GoodsSnString商品編號
ShopPriceInt商品售價

2. 商品分類(Category)

用於管理商品的層級分類結構。

字段說明

字段名類型說明
nameString分類名稱,必填
levelInt分類層級,例如 1 爲一級分類
parentPointer -> Category上級分類,用於構建樹狀結構

3. 品牌(Brands)

用於表示商品的品牌信息。

字段說明

字段名類型說明
nameString品牌名稱,必填
logoString品牌 Logo 地址

4. 輪播圖(Banner)

用於首頁或頻道頁展示的推廣圖片。

字段說明

字段名類型說明
imageString圖片地址
urlString跳轉鏈接(商品頁面)

5. 品牌分類關係(GoodsCategoryBrand)

用於定義品牌與分類的綁定關係。

字段說明

⚠️ 注:該實體在圖中未列出字段細節,推測其可能包含以下內容:

字段名類型說明
categoryPointer -> Category關聯的商品分類
brandPointer -> Brands關聯的品牌

數據關係總結

  • 一個商品屬於一個品牌和一個分類(多對一)。
  • 一個分類可以有多個子分類(樹狀結構)。
  • 品牌與分類之間爲多對多關係,需通過中間表 GoodsCategoryBrand 管理。
  • 輪播圖與商品通過 URL 關聯,建議關聯實際商品 ID,提升跳轉準確性。

oss

阿里雲文檔

Go SDK V2 快速入門

/*
 * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @Date: 2025-06-01 11:50:08
 * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @LastEditTime: 2025-06-01 12:11:13
 * @FilePath: /RpcLearn/aliyun_oss_test/main.go
 * @Description: 這是默認設置,請設置`customMade`, 打開koroFileHeader查看配置 進行設置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
package main

import (
 "fmt"
 "os"

 "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func main() {
 // 從環境變量獲取配置
 accessKeyID := os.Getenv("OSS_ACCESS_KEY_ID")
 accessKeySecret := os.Getenv("OSS_ACCESS_KEY_SECRET")

 // 添加調試信息
 fmt.Printf("環境變量值:\nOSS_ACCESS_KEY_ID: %s\nOSS_ACCESS_KEY_SECRET: %s\n",
  accessKeyID, accessKeySecret)

 bucketName := "blog4me"
 endpoint := "oss-cn-beijing.aliyuncs.com"

 // 檢查必要的環境變量
 if accessKeyID == "" || accessKeySecret == "" {
  fmt.Println("請設置環境變量 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET")
  return
 }

 // 創建OSSClient實例
 client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
 if err != nil {
  fmt.Println("創建OSS客戶端失敗:", err)
  return
 }

 // 獲取存儲空間
 bucket, err := client.Bucket(bucketName)
 if err != nil {
  fmt.Println("獲取Bucket失敗:", err)
  return
 }

 // 上傳文件
 objectKey := "go_learn/upload_test.jpg"
 localFile := "upload_test.jpg"

 // 檢查文件是否存在
 if _, err := os.Stat(localFile); os.IsNotExist(err) {
  fmt.Printf("文件 %s 不存在\n", localFile)
  return
 }

 err = bucket.PutObjectFromFile(objectKey, localFile)
 if err != nil {
  fmt.Println("上傳文件失敗:", err)
  return
 }

 fmt.Printf("文件 %s 已成功上傳到 %s\n", localFile, objectKey)
}

服務端簽名直傳並設置上傳回調

使用續斷來完成內網穿透

s3 的上傳流程

用戶瀏覽器
|
| (1) 上傳文件到 S3(通過預簽名 URL)
|
v
Amazon S3
|
| (2) 上傳完成,前端調用你的 API Gateway
|
v
API Gateway --> Lambda 函數
|
| (3) Lambda 收到上傳完成事件
|
v
數據庫、通知服務、後端業務邏輯...

本示例演示如何使用 Go 和 AWS SDK v2 將本地文件上傳到 Amazon S3。

🧾 前提條件

  • 已擁有 AWS 賬號;
  • 已創建 S3 Bucket;
  • 已配置 AWS 憑證(通過 aws configure 或設置環境變量);
  • 已準備本地文件(如 test.jpg);

📦 安裝依賴

go mod init s3uploadtest
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3

🧑‍💻 示例代碼

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "path/filepath"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    bucket := "your-bucket-name"         // 替換爲你的 S3 桶名
    region := "ap-southeast-1"           // 替換爲你的區域
    key := "uploads/test.jpg"            // 上傳後在 S3 中的路徑
    filePath := "./test.jpg"             // 本地文件路徑

    // 加載 AWS 配置
    cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region))
    if err != nil {
        log.Fatalf("無法加載 AWS 配置: %v", err)
    }

    // 創建 S3 客戶端
    client := s3.NewFromConfig(cfg)

    // 打開文件
    file, err := os.Open(filePath)
    if err != nil {
        log.Fatalf("無法打開文件: %v", err)
    }
    defer file.Close()

    // 獲取文件大小與內容類型
    fileInfo, _ := file.Stat()
    size := fileInfo.Size()
    contentType := detectContentType(filePath)

    // 執行上傳
    _, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
        Bucket:        &bucket,
        Key:           &key,
        Body:          file,
        ContentLength: size,
        ContentType:   &contentType,
    })

    if err != nil {
        log.Fatalf("上傳失敗: %v", err)
    }

    fmt.Println("上傳成功 ✅")
    fmt.Printf("訪問地址: https://%s.s3.amazonaws.com/%s\n", bucket, key)
}

func detectContentType(path string) string {
    ext := filepath.Ext(path)
    switch ext {
    case ".jpg", ".jpeg":
        return "image/jpeg"
    case ".png":
        return "image/png"
    case ".gif":
        return "image/gif"
    case ".txt":
        return "text/plain"
    default:
        return "application/octet-stream"
    }
}

✅ 上傳驗證

上傳完成後,訪問 URL:

https://your-bucket-name.s3.amazonaws.com/uploads/test.jpg

📚 參考文檔

庫存

併發情況下,庫存無法正常扣減

更新完成之前,你連查詢都查詢不到,查詢之前先要獲取一把鎖,誰有誰操作,更新完釋放

商品服務:設置庫存 訂單:預扣庫存 訂單的超時機制(超時機制)【歸還庫存】 支付:支付完成扣減庫存

package handler

import (
 "context"
 "sync"
 "time"

 "inventory_srv/global"
 "inventory_srv/model"
 "inventory_srv/proto"

 "go.uber.org/zap"
 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/status"
 "google.golang.org/protobuf/types/known/emptypb"
 "gorm.io/gorm"
 "gorm.io/gorm/clause"
)

type InventoryServer struct {
 proto.UnimplementedInventoryServiceServer
 mu sync.Mutex
}

// SetInventory 設置庫存
func (s *InventoryServer) SetInventory(ctx context.Context, req *proto.GoodsInvInfo) (*emptypb.Empty, error) {
 s.mu.Lock()
 defer s.mu.Unlock()

 var inv model.Inventory
 result := global.DB.Where("goods_id = ?", req.GoodsId).First(&inv)
 if result.Error != nil {
  if result.Error == gorm.ErrRecordNotFound {
   // 如果記錄不存在,創建新記錄
   inv = model.Inventory{
    GoodsID: req.GoodsId,
    Stock:   req.Num,
    Version: 0,
   }
   if err := global.DB.Create(&inv).Error; err != nil {
    zap.S().Errorf("創建庫存記錄失敗: %v", err)
    return nil, status.Error(codes.Internal, "創建庫存記錄失敗")
   }
  } else {
   zap.S().Errorf("查詢庫存記錄失敗: %v", result.Error)
   return nil, status.Error(codes.Internal, "查詢庫存記錄失敗")
  }
 } else {
  // 更新現有記錄
  inv.Stock = req.Num
  if err := global.DB.Save(&inv).Error; err != nil {
   zap.S().Errorf("更新庫存記錄失敗: %v", err)
   return nil, status.Error(codes.Internal, "更新庫存記錄失敗")
  }
 }

 return &emptypb.Empty{}, nil
}

// GetInventory 獲取庫存
func (s *InventoryServer) GetInventory(ctx context.Context, req *proto.GoodsInvInfo) (*proto.GoodsInvInfo, error) {
 var inv model.Inventory
 result := global.DB.Where("goods_id = ?", req.GoodsId).First(&inv)
 if result.Error != nil {
  if result.Error == gorm.ErrRecordNotFound {
   return &proto.GoodsInvInfo{
    GoodsId: req.GoodsId,
    Num:     0,
   }, nil
  }
  zap.S().Errorf("查詢庫存記錄失敗: %v", result.Error)
  return nil, status.Error(codes.Internal, "查詢庫存記錄失敗")
 }

 return &proto.GoodsInvInfo{
  GoodsId: inv.GoodsID,
  Num:     inv.Stock,
 }, nil
}

// Sell 庫存扣減
func (s *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
 const maxRetries = 3
 const retryDelay = 100 * time.Millisecond

 for retry := 0; retry < maxRetries; retry++ {
  // 開啓事務
  tx := global.DB.Begin()
  if tx.Error != nil {
   return nil, status.Error(codes.Internal, "開啓事務失敗")
  }

  success := true
  // 遍歷所有商品
  for _, goodsInfo := range req.GoodsInvInfo {
   var inv model.Inventory
   // 使用行鎖查詢
   result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
    Where("goods_id = ?", goodsInfo.GoodsId).
    First(&inv)

   if result.Error != nil {
    tx.Rollback()
    if result.Error == gorm.ErrRecordNotFound {
     return nil, status.Error(codes.NotFound, "商品庫存不存在")
    }
    return nil, status.Error(codes.Internal, "查詢庫存失敗")
   }

   // 檢查庫存是否充足
   if inv.Stock < goodsInfo.Num {
    tx.Rollback()
    return nil, status.Error(codes.ResourceExhausted, "庫存不足")
   }

   // 使用樂觀鎖更新庫存
   updateResult := tx.Model(&model.Inventory{}).
    Where("goods_id = ? AND version = ?", goodsInfo.GoodsId, inv.Version).
    Updates(map[string]interface{}{
     "stock":   inv.Stock - goodsInfo.Num,
     "version": inv.Version + 1,
    })

   if updateResult.Error != nil {
    tx.Rollback()
    success = false
    break
   }

   if updateResult.RowsAffected == 0 {
    tx.Rollback()
    success = false
    break
   }
  }

  if success {
   // 提交事務
   if err := tx.Commit().Error; err != nil {
    return nil, status.Error(codes.Internal, "提交事務失敗")
   }
   return &emptypb.Empty{}, nil
  }

  // 如果失敗且還有重試次數,等待後重試
  if retry < maxRetries-1 {
   time.Sleep(retryDelay)
   continue
  }
 }

 return nil, status.Error(codes.Internal, "庫存更新失敗,請重試")
}

// Reback 庫存歸還
func (s *InventoryServer) Reback(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
 const maxRetries = 3
 const retryDelay = 100 * time.Millisecond

 for retry := 0; retry < maxRetries; retry++ {
  // 開啓事務
  tx := global.DB.Begin()
  if tx.Error != nil {
   return nil, status.Error(codes.Internal, "開啓事務失敗")
  }

  success := true
  // 遍歷所有商品
  for _, goodsInfo := range req.GoodsInvInfo {
   var inv model.Inventory
   // 使用行鎖查詢
   result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
    Where("goods_id = ?", goodsInfo.GoodsId).
    First(&inv)

   if result.Error != nil {
    tx.Rollback()
    if result.Error == gorm.ErrRecordNotFound {
     return nil, status.Error(codes.NotFound, "商品庫存不存在")
    }
    return nil, status.Error(codes.Internal, "查詢庫存失敗")
   }

   // 使用樂觀鎖更新庫存
   updateResult := tx.Model(&model.Inventory{}).
    Where("goods_id = ? AND version = ?", goodsInfo.GoodsId, inv.Version).
    Updates(map[string]interface{}{
     "stock":   inv.Stock + goodsInfo.Num,
     "version": inv.Version + 1,
    })

   if updateResult.Error != nil {
    tx.Rollback()
    success = false
    break
   }

   if updateResult.RowsAffected == 0 {
    tx.Rollback()
    success = false
    break
   }
  }

  if success {
   // 提交事務
   if err := tx.Commit().Error; err != nil {
    return nil, status.Error(codes.Internal, "提交事務失敗")
   }
   return &emptypb.Empty{}, nil
  }

  // 如果失敗且還有重試次數,等待後重試
  if retry < maxRetries-1 {
   time.Sleep(retryDelay)
   continue
  }
 }

 return nil, status.Error(codes.Internal, "庫存更新失敗,請重試")
}

鎖機制說明

悲觀鎖

悲觀鎖(Pessimistic Lock)是一種假設併發衝突經常發生的鎖機制。在操作數據之前,先對數據加鎖,其他事務只能等待鎖釋放後才能繼續操作。常見於數據庫的行鎖、表鎖等。

舉例:

  • 在 MySQL 中,SELECT ... FOR UPDATE 會對讀取的行加排他鎖,其他事務無法修改這些行,直到當前事務提交或回滾。
  • 在 GORM 中,可以通過 db.Clauses(clause.Locking{Strength: "UPDATE"}) 實現行級悲觀鎖。
  • for update 行鎖且只對主鍵和索引有效,如果沒有索引那麼行鎖會升級成表鎖,所只對更新有效(go 語言中的讀寫鎖)
// 在事務中使用 -  db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
tx.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE
// db.Clauses(clause.Locking{
tx.Clauses(clause.Locking{
  Strength: "SHARE",
  Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
// SELECT * FROM `users` FOR SHARE OF `users`

樂觀鎖

樂觀鎖(Optimistic Lock)假設併發衝突很少發生,不會在操作前加鎖,而是在更新時檢查數據是否被其他事務修改。常用版本號(version)或時間戳實現。

舉例:

  • 數據表中增加 version 字段,更新時帶上舊的 version,只有數據庫中 version 未變化時才更新成功,否則說明有併發衝突,需重試。
  • 代碼示例:

```go tx.Model(&Inventory{}). Where(“goods_id = ? AND version = ?”, goodsId, oldVersion). Updates(map[string]interface{}{ “stock”: newStock, “version”: oldVersion + 1, })

// 升級寫法:使用 Select 避免默認值和0值不更新的問題 tx.Model(&Inventory{}). Where(“goods_id = ? AND version = ?”, goodsId, oldVersion). Select(“stock”, “version”). Updates(Inventory{ Stock: newStock, Version: oldVersion + 1, }) ```

說明: 使用 Select 方法可以明確指定要更新的字段,即使字段值爲 0 或默認值也會被更新。這樣避免了 GORM 默認忽略零值字段的問題。


分佈式鎖簡介

什麼是分佈式鎖

分佈式鎖是一種用於分佈式系統中多進程/多節點間互斥訪問共享資源的機制。它保證在同一時刻,只有一個節點能獲得鎖並操作關鍵資源,防止數據競爭和不一致。

引入原因

  • 在單機環境下,進程間可以用本地鎖(如互斥鎖)保證互斥。
  • 在分佈式環境下,多個服務實例、節點或容器可能同時操作同一資源,本地鎖無法跨進程/跨主機生效。
  • 分佈式鎖通過 Redis、ZooKeeper、Etcd 等中間件實現全局互斥,常用於庫存扣減、訂單生成等需要強一致性的場景。

常見實現方式:

  • Redis 的 setnx+過期時間
  • ZooKeeper 的臨時順序節點
  • Etcd 的租約機制

mysql 的數據沒建索引的話,它的行鎖會升級到表鎖

// Sell 庫存扣減
func (s *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
 const maxRetries = 3
 const retryDelay = 100 * time.Millisecond

 for retry := 0; retry < maxRetries; retry++ {
  // 開啓事務
  tx := global.DB.Begin()
  if tx.Error != nil {
   return nil, status.Error(codes.Internal, "開啓事務失敗")
  }

  success := true
  // 遍歷所有商品
  for _, goodsInfo := range req.GoodsInvInfo {
   var inv model.Inventory
   // 使用行鎖查詢
   result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
    Where("goods_id = ?", goodsInfo.GoodsId).
    First(&inv)

   if result.Error != nil {
    tx.Rollback()
    if result.Error == gorm.ErrRecordNotFound {
     return nil, status.Error(codes.NotFound, "商品庫存不存在")
    }
    return nil, status.Error(codes.Internal, "查詢庫存失敗")
   }

   // 檢查庫存是否充足
   if inv.Stock < goodsInfo.Num {
    tx.Rollback()
    return nil, status.Error(codes.ResourceExhausted, "庫存不足")
   }

   // 使用樂觀鎖更新庫存
   updateResult := tx.Model(&model.Inventory{}).
    Where("goods_id = ? AND version = ?", goodsInfo.GoodsId, inv.Version).
    Updates(map[string]interface{}{
     "stock":   inv.Stock - goodsInfo.Num,
     "version": inv.Version + 1,
    })

   if updateResult.Error != nil {
    tx.Rollback()
    success = false
    break
   }

   if updateResult.RowsAffected == 0 {
    tx.Rollback()
    success = false
    break
   }
  }

  if success {
   // 提交事務
   if err := tx.Commit().Error; err != nil {
    return nil, status.Error(codes.Internal, "提交事務失敗")
   }
   return &emptypb.Empty{}, nil
  }

  // 如果失敗且還有重試次數,等待後重試
  if retry < maxRetries-1 {
   time.Sleep(retryDelay)
   continue
  }
 }

 return nil, status.Error(codes.Internal, "庫存更新失敗,請重試")
}

基於 redis 實現分佈式鎖

分析

redsync 源碼解讀

  1. setnx 的作用

  2. 將獲取和設置值變成原子性的操作

  3. 如果我的服務掛掉了 - 死鎖 a. 設置過期時間

b. 如果你設置了過期時間,那麼如果過期時間到了我的業務邏輯沒有執行完怎麼辦? i. 在過期之前刷新一下 ii. 需要自己去後臺協程完成延時的工作 1. 延時的接口可能會帶來負面影響 - 如果其中某一個服務 hung 住了,2s 就能執行完,但是你 hung 住那麼你就會一直去申請延長鎖,導致別人永遠獲取不到鎖,這個很要命

  1. 分佈式需要解決的問題 a. 互斥性 - setnx b. 死鎖 c. 安全性 i. 鎖只能被持有該鎖的用戶刪除,不能被其他用戶刪除 1. 當前設置的 value 值是多少隻有當的 g 才能知道 2. 在刪除的時候取出 redis 中的值和當前自己保存的值要做對比的

Redis 是分佈式鎖最常用的實現中間件之一。其核心思想是利用 Redis 的原子操作(如 SETNX)來保證同一時刻只有一個客戶端能獲得鎖。常見實現要點如下:

  • 利用 SET key value NX PX 過期時間 保證原子性和自動過期,避免死鎖。
  • 客戶端釋放鎖時需校驗 value,防止誤刪他人鎖(如使用 UUID 標識)。
  • 需要考慮鎖的自動過期、續約、異常情況下的容錯等問題。
  • 生產環境推薦使用 Redisson、RedLock 等成熟方案。

Go 代碼示例

以 go-redis 爲例,實現簡單的分佈式鎖 acquire 和 release:

import (
    "context"
    "github.com/redis/go-redis/v9"
    "time"
    "github.com/google/uuid"
)

type RedisLock struct {
    client *redis.Client
    key    string
    value  string
    ttl    time.Duration
}

func NewRedisLock(client *redis.Client, key string, ttl time.Duration) *RedisLock {
    return &RedisLock{
        client: client,
        key:    key,
        value:  uuid.NewString(), // 唯一標識
        ttl:    ttl,
    }
}

// 加鎖
func (l *RedisLock) Acquire(ctx context.Context) (bool, error) {
    ok, err := l.client.SetNX(ctx, l.key, l.value, l.ttl).Result()
    return ok, err
}

// 釋放鎖(僅刪除自己加的鎖)
func (l *RedisLock) Release(ctx context.Context) (bool, error) {
    // Lua 腳本保證原子性
    script := `
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end`
    res, err := l.client.Eval(ctx, script, []string{l.key}, l.value).Result()
    return res == int64(1), err
}

說明

  • 加鎖時用 SetNX 保證只有一個客戶端能成功。
  • 釋放鎖時用 Lua 腳本校驗 value,確保不會誤刪他人鎖。
  • 生產環境建議使用帶自動續約、容錯的分佈式鎖庫(如 redsync、redisson)。
  • 分佈式鎖適合短時、關鍵資源互斥,不適合長時間持有。

redlock 詳解

背景

單個 Redis 實例實現的分佈式鎖存在單點故障問題,當 Redis 實例宕機時,整個鎖服務不可用。爲了提高可用性,需要搭建 Redis 集羣來提供可用性。

RedLock 算法原理

RedLock 是 Redis 官方提出的分佈式鎖算法,它在多個獨立的 Redis 實例上實現分佈式鎖,以確保即使部分實例故障也能正常工作。

核心思想

  1. 多個獨立的 Redis 實例:通常使用 5 個獨立的 Redis 實例(奇數個,便於投票)
  2. 大多數原則:只有在大多數實例(超過一半)上成功獲取鎖,才認爲獲取鎖成功
  3. 時間窗口控制:設置獲取鎖的時間限制,避免長時間等待

算法步驟

  1. 獲取鎖

  2. 獲取當前時間戳(毫秒)

  3. 依次嘗試在所有 Redis 實例上獲取鎖,使用相同的 key 和隨機 value

  4. 每個實例的獲取操作都設置超時時間(遠小於鎖的過期時間)

  5. 如果在大多數實例上成功獲取鎖,且總耗時小於鎖的有效時間,則認爲獲取鎖成功

  6. 鎖的有效時間 = 原始有效時間 - 獲取鎖消耗的時間

  7. 釋放鎖

  8. 向所有 Redis 實例發送釋放鎖的請求

  9. 無論之前是否在該實例上成功獲取鎖

集羣配置示例

# redis-cluster.yml
version: '3'
services:
  redis1:
    image: redis:6-alpine
    ports:
      - '6379:6379'
    command: redis-server --appendonly yes

  redis2:
    image: redis:6-alpine
    ports:
      - '6380:6379'
    command: redis-server --appendonly yes

  redis3:
    image: redis:6-alpine
    ports:
      - '6381:6379'
    command: redis-server --appendonly yes

  redis4:
    image: redis:6-alpine
    ports:
      - '6382:6379'
    command: redis-server --appendonly yes

  redis5:
    image: redis:6-alpine
    ports:
      - '6383:6379'
    command: redis-server --appendonly yes

Go 代碼實現

package main

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "sync"
    "time"

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

type RedLock struct {
    clients []*redis.Client
    quorum  int
}

type Lock struct {
    key        string
    value      string
    expiration time.Duration
    clients    []*redis.Client
}

func NewRedLock(addrs []string) *RedLock {
    clients := make([]*redis.Client, len(addrs))
    for i, addr := range addrs {
        clients[i] = redis.NewClient(&redis.Options{
            Addr: addr,
        })
    }

    return &RedLock{
        clients: clients,
        quorum:  len(addrs)/2 + 1, // 大多數
    }
}

func (r *RedLock) Lock(key string, expiration time.Duration) (*Lock, error) {
    value := generateRandomValue()

    start := time.Now()
    successCount := 0

    // 併發向所有實例獲取鎖
    var wg sync.WaitGroup
    var mu sync.Mutex

    for _, client := range r.clients {
        wg.Add(1)
        go func(c *redis.Client) {
            defer wg.Done()

            ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
            defer cancel()

            result := c.SetNX(ctx, key, value, expiration)
            if result.Err() == nil && result.Val() {
                mu.Lock()
                successCount++
                mu.Unlock()
            }
        }(client)
    }

    wg.Wait()

    elapsed := time.Since(start)
    validTime := expiration - elapsed

    // 檢查是否獲取鎖成功
    if successCount >= r.quorum && validTime > 0 {
        return &Lock{
            key:        key,
            value:      value,
            expiration: validTime,
            clients:    r.clients,
        }, nil
    }

    // 獲取失敗,釋放已獲取的鎖
    r.unlock(key, value)
    return nil, fmt.Errorf("failed to acquire lock")
}

func (r *RedLock) unlock(key, value string) {
    script := `
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
    `

    var wg sync.WaitGroup
    for _, client := range r.clients {
        wg.Add(1)
        go func(c *redis.Client) {
            defer wg.Done()
            ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
            defer cancel()
            c.Eval(ctx, script, []string{key}, value)
        }(client)
    }
    wg.Wait()
}

func (l *Lock) Unlock() {
    script := `
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
    `

    var wg sync.WaitGroup
    for _, client := range l.clients {
        wg.Add(1)
        go func(c *redis.Client) {
            defer wg.Done()
            ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
            defer cancel()
            c.Eval(ctx, script, []string{l.key}, l.value)
        }(client)
    }
    wg.Wait()
}

func generateRandomValue() string {
    bytes := make([]byte, 16)
    rand.Read(bytes)
    return hex.EncodeToString(bytes)
}

// 使用示例
func main() {
    addrs := []string{
        "localhost:6379",
        "localhost:6380",
        "localhost:6381",
        "localhost:6382",
        "localhost:6383",
    }

    redlock := NewRedLock(addrs)

    lock, err := redlock.Lock("resource_key", 10*time.Second)
    if err != nil {
        fmt.Printf("獲取鎖失敗: %v\n", err)
        return
    }

    fmt.Println("成功獲取鎖,執行業務邏輯...")
    time.Sleep(5 * time.Second)

    lock.Unlock()
    fmt.Println("釋放鎖完成")
}

RedLock 的優缺點

優點

  1. 高可用性:即使部分 Redis 實例故障,仍可正常工作
  2. 容錯性:能夠容忍少數實例的網絡分區或故障
  3. 一致性:通過大多數原則保證鎖的一致性

缺點

  1. 複雜性:實現和運維比單實例複雜
  2. 性能開銷:需要與多個實例通信,延遲增加
  3. 時鐘依賴:依賴各節點時鐘 同步
  4. 資源消耗:需要維護多個 Redis 實例

生產環境建議

  1. 使用成熟庫:推薦使用 redsync 等經過驗證的庫
  2. 監控告警:監控各 Redis 實例狀態和鎖獲取成功率
  3. 合理配置:根據業務需求設置合適的超時時間和重試策略
  4. 備選方案:考慮使用 etcd、ZooKeeper 等其他分佈式協調服務

redis 集羣和基於 redis 的 sync 沒做(後繼補上)