Go工程师体系课 007【学习笔记】

商品微服务

实体结构说明

本模块包含以下核心实体:

  • 商品(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 没做(后继补上)

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4780

(0)
Walker的头像Walker
上一篇 2025年11月25日 07:00
下一篇 2025年11月25日 05:00

相关推荐

  • 深入理解ES6 006【学习笔记】

    Symbol和Symbol属性 第6种原始数据类型:Symbol。私有名称原本是为了让开发者们创建非字符串属性名称而设计的,但是一般的技术无法检测这些属性的私有名称 创建Symbol let firstName = Symbol(); let person = {} person[firstName] = "Nicholas"; cons…

    个人 2025年3月8日
    1.1K00
  • Node深入浅出(圣思园教育) 001【学习笔记】

    node 从异步编程范式理解 Node.js Node.js 的定位与核心思想 基于 V8 引擎 + libuv 事件驱动库,将 JavaScript 从浏览器带到服务器侧。 采用单线程事件循环处理 I/O,最大化利用 CPU 等待 I/O 的时间片,特别适合高并发、I/O 密集型场景。 “不要阻塞主线程”是设计哲学:尽量把耗时操作交给内核或线程池,回调结果…

    个人 2025年11月24日
    8000
  • Node深入浅出(圣思园教育) 002【学习笔记】

    node 的包管理机制和加载机制 npm search xxxnpm view xxxnpm install xxx nodejs 文件系统操作的 api Node.js 的 fs 模块提供同步(Sync)与基于回调/Promise 的异步 API,可以操作本地文件与目录。日常开发中常用的能力包括读取、写入、追加、删除、遍历目录、监听变化等。以下示例基于 C…

    个人 2025年11月24日
    6300
  • Node深入浅出(圣思园教育) 003【学习笔记】

    WebSocket 与 SSE 总览 WebSocket 基础 定位:WebSocket 是一条在 HTTP 握手后升级的全双工连接,允许客户端与服务器在同一 TCP 通道上双向推送数据,省去了反复轮询。 握手流程: 客户端通过 Upgrade: websocket 头发起 HTTP 请求; 服务器响应 101 Switching Protocols,双方协…

    个人 2025年11月24日
    6100
  • 【开篇】

    我是Walker,生于八十年代初,代码与生活的旅者。全栈开发工程师,游走于前端与后端的边界,执着于技术与艺术的交汇点。 代码,是我编织梦想的语言;项目,是我刻画未来的画布。在键盘的敲击声中,我探索技术的无尽可能,让灵感在代码里永恒绽放。 深度咖啡爱好者,迷恋每一杯手冲的诗意与仪式感。在咖啡的醇香与苦涩中,寻找专注与灵感,亦如在开发的世界中追求极致与平衡。 骑…

    2025年2月6日 个人
    1.9K00

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
欢迎🌹 Coding never stops, keep learning! 💡💻 光临🌹