【golang】基于redis zset实现并行流量控制(计数锁)

在业务开发中,有时需要对某个操作在整个集群中限制并发度,例如限制大模型对话的并行数。基于redis zset实现计数锁,做个笔记。

关键词:并行流量控制、计数锁

package redisutil

import (
	"context"
	"fmt"
	"math"
	"time"

	"github.com/go-redis/redis/v9"
)

// AcquireZSetLock 借助redis zset数据结构实现分布式计数锁。可用于计数任务运行数,防止超限。返回值:zset大小、释放锁的函数、错误信息
func AcquireZSetLock(ctx context.Context, c redis.Client, key string, element string, zsetMaxSize int,
	expiresIn time.Duration, syncWait time.Duration) (int, func() error, error) {
	ctx, cancel := context.WithTimeout(ctx, syncWait)
	defer cancel()

	for i := 0; ; i++ {
		select {
		case <-ctx.Done(): // 接到取消信号,按插入失败处理
			return -1, func() error { return nil }, ctx.Err()
		default:
		}

		size, err := insertElementToZsetLock(ctx, c, key, element, zsetMaxSize, expiresIn)
		if err != nil {
			second := 0.4 + 0.6*math.Exp(-0.17*float64(i)) // f(i=0) = 1.0; f(i=10) = 0.5096,即第10次就会衰减到0.5096秒
			second = max(second, 0.5)                      // 最小间隔0.5秒,防止过于频繁的请求
			time.Sleep(time.Duration(second*1000) * time.Millisecond)
		}

		releaseFunc := func() error {
			result, err := c.ZRem(context.Background(), key, element).Result()
			if err != nil {
				return fmt.Errorf("redis zrem error: %v. return=%d", err, result)
			}
			return nil
		}
		return size, releaseFunc, nil
	}
}

// insertElementToZsetLock 插入元素到zset,并删除已过期的元素
func insertElementToZsetLock(ctx context.Context, c redis.Client, key string, element string, zsetMaxSize int, expiresIn time.Duration) (int, error) {
	luaScript := `
		local zsetName = KEYS[1]
		local memberName = ARGV[1]
		local currentTime = tonumber(ARGV[2])
		local deadTime = tonumber(ARGV[3])
		local sizeLimit = tonumber(ARGV[4])

		-- 删除已过期的元素
		redis.call("ZREMRANGEBYSCORE", zsetName, "-inf", currentTime)

		-- 获取集合的大小
		local setSize = redis.call('ZCard', zsetName)

		-- 如果集合大小小于限制值,则添加元素,并返回集合大小
		if setSize < sizeLimit then
			redis.call('ZAdd', zsetName, deadTime, memberName)
			local expireTime = deadTime - currentTime
			if expireTime > 0 then
				redis.call('EXPIRE', zsetName, expireTime)
			end
			return setSize+1
		end
		return -1
	`
	currentTime := time.Now().Unix()
	deadTime := time.Now().Add(expiresIn).Unix() // 过期时间 Unix秒
	ret, err := c.Do(ctx, "EVAL", luaScript, 1, key, element, currentTime, deadTime, zsetMaxSize).Result()
	if err != nil {
		return -1, err
	}
	if ret.(int64) < 0 {
		return zsetMaxSize, fmt.Errorf("zset size reach max size: %d", zsetMaxSize)
	}
	return int(ret.(int64)), nil
}

使用示例:

size, release, err := AcquireZSetLock(ctx, client, key, element, 10, 10*time.Second, 3*time.Second)
defer release()
if err != nil {
    fmt.Println(err)
}
### Golang 使用 Redis ZSet 实现排行榜 为了实现基于 Redis 的 `ZSet` 排行榜功能,在 Go 中通常借助于第三方库来简化与 Redis 数据库之间的交互。对于此目的,推荐使用 `go-redis/redis` 库[^1]。 下面是一个简单的例子,展示了如何初始化客户端连接到 Redis 并执行基本的 `ZSet` 操作: ```go package main import ( "fmt" "log" "github.com/go-redis/redis/v8" "context" ) var ctx = context.Background() func initRedis() *redis.Client { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Redis服务器地址 Password: "", // 密码,默认为空字符串表示无密码认证 DB: 0, // 默认数据库编号为0 }) return rdb } // 添加成员至ZSet func addToLeaderboard(rdb *redis.Client, key string, member string, score float64) error { err := rdb.ZAdd(ctx, key, &redis.Z{Member: member, Score: score}).Err() if err != nil { return fmt.Errorf("failed to add %s with score %.2f into leaderboard '%s': %v", member, score, key, err) } return nil } // 获取指定范围内的排名列表 func getRankingList(rdb *redis.Client, key string, start int64, stop int64) ([]string, error) { result, err := rdb.ZRevRangeWithScores(ctx, key, start, stop).Result() if err != nil { return nil, fmt.Errorf("failed to fetch ranking list from leaderboard '%s': %v", key, err) } var rankings []string for i, item := range result { rankings = append(rankings, fmt.Sprintf("%d. %s (Score:%.2f)", i+start+1, item.Member, item.Score)) } return rankings, nil } func main() { client := initRedis() defer func(client *redis.Client) { err := client.Close() if err != nil { log.Fatal(err) } }(client) leaderBoardKey := "my_leader_board" // 向排行榜中添加几个条目作为示例 addToLeaderboard(client, leaderBoardKey, "Alice", 95.5) addToLeaderboard(client, leaderBoardKey, "Bob", 88.0) addToLeaderboard(client, leaderBoardKey, "Charlie", 92.3) // 查询前两名的成绩 topTwo, _ := getRankingList(client, leaderBoardKey, 0, 1) fmt.Println(topTwo...) } ``` 上述代码片段实现了两个主要的功能函数:一个是用于向给定键名对应的 `ZSet` 结构里增加新成员及其评分;另一个则是获取特定范围内按照评分降序排列后的成员名单。此外还提供了一个简单的入口程序来进行测试验证。 #### 关于性能优化和实际应用中的注意事项 当涉及到高频率更新或查询操作时,应该考虑到可能存在的热点问题以及网络延迟等因素的影响。因此建议采取诸如批量处理命令、合理设置过期时间等方式提高效率减少资源消耗[^2]。 另外值得注意的是,虽然这里只提供了最基础版本的操作接口,但在真实业务场景下还需要针对具体需求做出适当调整和完善,例如加入错误重试机制、支持分页显示等功能特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪的期许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值