商品微服務
實體結構說明
本模塊包含以下核心實體:
- 商品(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
/*
* @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
📚 參考文檔
- AWS Go SDK v2 文檔: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2
庫存
併發情況下,庫存無法正常扣減
更新完成之前,你連查詢都查詢不到,查詢之前先要獲取一把鎖,誰有誰操作,更新完釋放
商品服務:設置庫存
訂單:預扣庫存 訂單的超時機制(超時機制)【歸還庫存】
支付:支付完成扣減庫存
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 源碼解讀
-
setnx 的作用
-
將獲取和設置值變成原子性的操作
-
如果我的服務掛掉了 – 死鎖
a. 設置過期時間
b. 如果你設置了過期時間,那麼如果過期時間到了我的業務邏輯沒有執行完怎麼辦?
i. 在過期之前刷新一下
ii. 需要自己去後臺協程完成延時的工作 1. 延時的接口可能會帶來負面影響 – 如果其中某一個服務 hung 住了,2s 就能執行完,但是你 hung 住那麼你就會一直去申請延長鎖,導致別人永遠獲取不到鎖,這個很要命
- 分佈式需要解決的問題
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 實例上實現分佈式鎖,以確保即使部分實例故障也能正常工作。
核心思想
- 多個獨立的 Redis 實例 :通常使用 5 個獨立的 Redis 實例(奇數個,便於投票)
- 大多數原則 :只有在大多數實例(超過一半)上成功獲取鎖,才認爲獲取鎖成功
- 時間窗口控制 :設置獲取鎖的時間限制,避免長時間等待
算法步驟
-
獲取鎖 :
-
獲取當前時間戳(毫秒)
- 依次嘗試在所有 Redis 實例上獲取鎖,使用相同的 key 和隨機 value
- 每個實例的獲取操作都設置超時時間(遠小於鎖的過期時間)
- 如果在大多數實例上成功獲取鎖,且總耗時小於鎖的有效時間,則認爲獲取鎖成功
-
鎖的有效時間 = 原始有效時間 – 獲取鎖消耗的時間
-
釋放鎖 :
- 向所有 Redis 實例發送釋放鎖的請求
- 無論之前是否在該實例上成功獲取鎖
集羣配置示例
# 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 的優缺點
優點
- 高可用性 :即使部分 Redis 實例故障,仍可正常工作
- 容錯性 :能夠容忍少數實例的網絡分區或故障
- 一致性 :通過大多數原則保證鎖的一致性
缺點
- 複雜性 :實現和運維比單實例複雜
- 性能開銷 :需要與多個實例通信,延遲增加
- 時鐘依賴 :依賴各節點時鐘 同步
- 資源消耗 :需要維護多個 Redis 實例
生產環境建議
- 使用成熟庫 :推薦使用
redsync等經過驗證的庫 - 監控告警 :監控各 Redis 實例狀態和鎖獲取成功率
- 合理配置 :根據業務需求設置合適的超時時間和重試策略
- 備選方案 :考慮使用 etcd、ZooKeeper 等其他分佈式協調服務
redis 集羣和基於 redis 的 sync 沒做(後繼補上)