Go工程師體系課 007

10次閱讀

商品微服務

實體結構說明

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

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

1. 商品(Goods)

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

字段說明

字段名 類型 說明
name String 商品名稱,必填
brand Pointer -> Brands 商品所屬品牌,關聯 Brands 實體
category Pointer -> Category 商品所屬分類,關聯 Category 實體
GoodsSn String 商品編號
ShopPrice Int 商品售價

2. 商品分類(Category)

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

字段說明

字段名 類型 說明
name String 分類名稱,必填
level Int 分類層級,例如 1 爲一級分類
parent Pointer -> Category 上級分類,用於構建樹狀結構

3. 品牌(Brands)

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

字段說明

字段名 類型 說明
name String 品牌名稱,必填
logo String 品牌 Logo 地址

4. 輪播圖(Banner)

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

字段說明

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

5. 品牌分類關係(GoodsCategoryBrand)

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

字段說明

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

字段名 類型 說明
category Pointer -> Category 關聯的商品分類
brand Pointer -> 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 沒做(後繼補上)

正文完
 0