商品微服务
实体结构说明
本模块包含以下核心实体:
- 商品(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 没做(后继补上)
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4780