- RedisLimiter 基于 Lua 脚本保证原子性 - Lua 脚本实现完整令牌桶算法(填充、消耗、TTL) - fail-open 策略:Redis 故障时允许请求通过 - FormatKey 辅助函数格式化限流 key - 完整单元测试(10 个测试用例,使用 miniredis) - 测试覆盖:首次请求、耗尽、不同用户、补充、容量上限、零速率、TTL、故障降级
229 lines
5.3 KiB
Go
229 lines
5.3 KiB
Go
package ratelimit
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/alicebob/miniredis/v2"
|
||
"github.com/hhs/camtalk/internal/config"
|
||
"github.com/redis/go-redis/v9"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// setupMiniRedis 创建一个内存 Redis 实例用于测试。
|
||
func setupMiniRedis(t *testing.T) (*miniredis.Miniredis, *redis.Client) {
|
||
mr, err := miniredis.Run()
|
||
require.NoError(t, err)
|
||
|
||
client := redis.NewClient(&redis.Options{
|
||
Addr: mr.Addr(),
|
||
})
|
||
|
||
t.Cleanup(func() {
|
||
client.Close()
|
||
mr.Close()
|
||
})
|
||
|
||
return mr, client
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_FirstRequest(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 5, Rate: 0.2},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
allowed, retryAfter := limiter.Allow(ctx, "user1:query")
|
||
|
||
assert.True(t, allowed)
|
||
assert.Equal(t, time.Duration(0), retryAfter)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_ConsumeUntilEmpty(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 3, Rate: 0.2},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
key := "user1:query"
|
||
|
||
// 连续消耗 3 个令牌
|
||
for i := 0; i < 3; i++ {
|
||
allowed, _ := limiter.Allow(ctx, key)
|
||
assert.True(t, allowed, "request %d should be allowed", i+1)
|
||
}
|
||
|
||
// 第 4 个请求应被拒绝
|
||
allowed, retryAfter := limiter.Allow(ctx, key)
|
||
assert.False(t, allowed)
|
||
assert.Greater(t, retryAfter, time.Duration(0))
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_DifferentKeys(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 2, Rate: 1.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
|
||
// user1 消耗 2 个令牌
|
||
allowed, _ := limiter.Allow(ctx, "user1:query")
|
||
assert.True(t, allowed)
|
||
allowed, _ = limiter.Allow(ctx, "user1:query")
|
||
assert.True(t, allowed)
|
||
|
||
// user1 第 3 个被拒绝
|
||
allowed, _ = limiter.Allow(ctx, "user1:query")
|
||
assert.False(t, allowed)
|
||
|
||
// user2 应该不受影响
|
||
allowed, _ = limiter.Allow(ctx, "user2:query")
|
||
assert.True(t, allowed)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_RefillAfterWait(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 2, Rate: 10.0}, // 每秒 10 个令牌
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
key := "user1:query"
|
||
|
||
// 消耗 2 个令牌
|
||
limiter.Allow(ctx, key)
|
||
limiter.Allow(ctx, key)
|
||
|
||
// 真实等待 150ms(Lua 脚本使用系统时间)
|
||
time.Sleep(150 * time.Millisecond)
|
||
|
||
// 应该补充了至少 1 个令牌
|
||
allowed, _ := limiter.Allow(ctx, key)
|
||
assert.True(t, allowed)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_CapacityLimit(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 3, Rate: 1.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
key := "user1:query"
|
||
|
||
// 真实等待让桶"溢出"
|
||
time.Sleep(100 * time.Millisecond)
|
||
|
||
// 但最多只能消耗 capacity 个令牌
|
||
for i := 0; i < 3; i++ {
|
||
allowed, _ := limiter.Allow(ctx, key)
|
||
assert.True(t, allowed, "request %d should be allowed", i+1)
|
||
}
|
||
|
||
// 第 4 个应被拒绝
|
||
allowed, _ := limiter.Allow(ctx, key)
|
||
assert.False(t, allowed)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_ZeroRate(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 1, Rate: 0.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
key := "user1:query"
|
||
|
||
// 第一个通过
|
||
allowed, _ := limiter.Allow(ctx, key)
|
||
assert.True(t, allowed)
|
||
|
||
// 第二个被拒绝,retryAfter 应该很大
|
||
allowed, retryAfter := limiter.Allow(ctx, key)
|
||
assert.False(t, allowed)
|
||
assert.Greater(t, retryAfter, 1*time.Hour)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_KeyTTL(t *testing.T) {
|
||
mr, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 5, Rate: 1.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
key := "user1:query"
|
||
|
||
// 第一次请求
|
||
limiter.Allow(ctx, key)
|
||
|
||
// 验证 key 已设置 TTL
|
||
ttl := mr.TTL(key)
|
||
assert.Greater(t, ttl, time.Duration(0))
|
||
assert.LessOrEqual(t, ttl, 600*time.Second)
|
||
}
|
||
|
||
func TestRedisLimiter_Allow_FailOpen(t *testing.T) {
|
||
mr, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 1, Rate: 1.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
ctx := context.Background()
|
||
|
||
// 关闭 Redis 模拟故障
|
||
mr.Close()
|
||
|
||
// 应该 fail-open(允许请求)
|
||
allowed, retryAfter := limiter.Allow(ctx, "user1:query")
|
||
assert.True(t, allowed)
|
||
assert.Equal(t, time.Duration(0), retryAfter)
|
||
}
|
||
|
||
func TestRedisLimiter_Stop(t *testing.T) {
|
||
_, client := setupMiniRedis(t)
|
||
|
||
cfg := config.RateLimitConfig{
|
||
Enabled: true,
|
||
Query: config.BucketConfig{Capacity: 1, Rate: 1.0},
|
||
}
|
||
limiter := NewRedisLimiter(client, cfg)
|
||
|
||
// Stop 应该不会 panic(即使多次调用)
|
||
limiter.Stop()
|
||
limiter.Stop()
|
||
}
|
||
|
||
func TestFormatKey(t *testing.T) {
|
||
key := FormatKey("user123", "query")
|
||
assert.Equal(t, "ratelimit:user123:query", key)
|
||
}
|