← 返回
后端开发 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 没做(后继补上)