├── README.md ├── redis ├── error.go ├── sliding_log_limiter_test.go ├── fixed_window_limiter_test.go ├── token_bucket_limiter_test.go ├── leaky_bucket_limiter_test.go ├── fixed_window_limiter.go ├── sliding_window_limiter_test.go ├── token_bucket_limiter.go ├── leaky_bucket_limiter.go ├── sliding_window_limiter.go └── sliding_log_limiter.go ├── go.mod ├── .gitignore ├── sliding_log_limiter_test.go ├── _sliding_log_limiter_test └── main.go ├── fixed_window_limiter.go ├── token_bucket_limiter.go ├── fixed_window_limiter_test.go ├── leaky_bucket_limiter.go ├── token_bucket_limiter_test.go ├── leaky_bucket_limiter_test.go ├── sliding_window_limiter.go ├── sliding_window_limiter_test.go ├── sliding_log_limiter.go ├── go.sum └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # limiter 2 | 使用Go实现各种限流算法 3 | -------------------------------------------------------------------------------- /redis/error.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "errors" 4 | 5 | // ErrAcquireFailed 获取失败 6 | var ErrAcquireFailed = errors.New("acquire failed") 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jiaxwu/limiter 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 8 | github.com/go-redis/redis/v8 v8.11.4 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go _sliding_log_limiter_test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea -------------------------------------------------------------------------------- /sliding_log_limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewSlidingLogLimiter(t *testing.T) { 9 | type args struct { 10 | smallWindow time.Duration 11 | strategies []*SlidingLogLimiterStrategy 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *SlidingLogLimiter 17 | wantErr bool 18 | }{ 19 | { 20 | name: "60_5seconds", 21 | args: args{ 22 | smallWindow: time.Second, 23 | strategies: []*SlidingLogLimiterStrategy{ 24 | NewSlidingLogLimiterStrategy(10, time.Minute), 25 | NewSlidingLogLimiterStrategy(100, time.Hour), 26 | }, 27 | }, 28 | want: nil, 29 | }, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | NewSlidingLogLimiter(tt.args.smallWindow, tt.args.strategies...) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_sliding_log_limiter_test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | limiter "github.com/jiaxwu/limiter/redis" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | client := redis.NewClient(&redis.Options{ 14 | Addr: "127.0.0.1:6379", 15 | }) 16 | l, _ := limiter.NewSlidingLogLimiter(client, time.Second, 17 | limiter.NewSlidingLogLimiterStrategy(10, time.Second*30), 18 | limiter.NewSlidingLogLimiterStrategy(15, time.Minute)) 19 | count := 0 20 | http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { 21 | err := l.TryAcquire(context.Background(), "test") 22 | if err == nil { 23 | w.Write([]byte("请求成功" + time.Now().String())) 24 | count++ 25 | fmt.Println(count) 26 | } else { 27 | w.Write([]byte(err.Error() + time.Now().String())) 28 | } 29 | }) 30 | http.ListenAndServe("127.0.0.1:8080", nil) 31 | } 32 | -------------------------------------------------------------------------------- /fixed_window_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // FixedWindowLimiter 固定窗口限流器 9 | type FixedWindowLimiter struct { 10 | limit int // 窗口请求上限 11 | window time.Duration // 窗口时间大小 12 | counter int // 计数器 13 | lastTime time.Time // 上一次请求的时间 14 | mutex sync.Mutex // 避免并发问题 15 | } 16 | 17 | func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter { 18 | return &FixedWindowLimiter{ 19 | limit: limit, 20 | window: window, 21 | lastTime: time.Now(), 22 | } 23 | } 24 | 25 | func (l *FixedWindowLimiter) TryAcquire() bool { 26 | l.mutex.Lock() 27 | defer l.mutex.Unlock() 28 | // 获取当前时间 29 | now := time.Now() 30 | // 如果当前窗口失效,计数器清0,开启新的窗口 31 | if now.Sub(l.lastTime) > l.window { 32 | l.counter = 0 33 | l.lastTime = now 34 | } 35 | // 若到达窗口请求上限,请求失败 36 | if l.counter >= l.limit { 37 | return false 38 | } 39 | // 若没到窗口请求上限,计数器+1,请求成功 40 | l.counter++ 41 | return true 42 | } 43 | -------------------------------------------------------------------------------- /redis/sliding_log_limiter_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewSlidingLogLimiter(t *testing.T) { 10 | type args struct { 11 | smallWindow time.Duration 12 | strategies []*SlidingLogLimiterStrategy 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *SlidingLogLimiter 18 | wantErr bool 19 | }{ 20 | { 21 | name: "60_5seconds", 22 | args: args{ 23 | smallWindow: time.Second, 24 | strategies: []*SlidingLogLimiterStrategy{ 25 | NewSlidingLogLimiterStrategy(10, time.Minute), 26 | NewSlidingLogLimiterStrategy(100, time.Hour), 27 | }, 28 | }, 29 | want: nil, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | client := redis.NewClient(&redis.Options{ 35 | Addr: "127.0.0.1:6379", 36 | }) 37 | NewSlidingLogLimiter(client, tt.args.smallWindow, tt.args.strategies...) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /token_bucket_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // TokenBucketLimiter 令牌桶限流器 9 | type TokenBucketLimiter struct { 10 | capacity int // 容量 11 | currentTokens int // 令牌数量 12 | rate int // 发放令牌速率/秒 13 | lastTime time.Time // 上次发放令牌时间 14 | mutex sync.Mutex // 避免并发问题 15 | } 16 | 17 | func NewTokenBucketLimiter(capacity, rate int) *TokenBucketLimiter { 18 | return &TokenBucketLimiter{ 19 | capacity: capacity, 20 | rate: rate, 21 | lastTime: time.Now(), 22 | } 23 | } 24 | 25 | func (l *TokenBucketLimiter) TryAcquire() bool { 26 | l.mutex.Lock() 27 | defer l.mutex.Unlock() 28 | 29 | // 尝试发放令牌 30 | now := time.Now() 31 | // 距离上次发放令牌的时间 32 | interval := now.Sub(l.lastTime) 33 | if interval >= time.Second { 34 | // 当前令牌数量+距离上次发放令牌的时间(秒)*发放令牌速率 35 | l.currentTokens = minInt(l.capacity, l.currentTokens+int(interval/time.Second)*l.rate) 36 | l.lastTime = now 37 | } 38 | 39 | // 如果没有令牌,请求失败 40 | if l.currentTokens == 0 { 41 | return false 42 | } 43 | // 如果有令牌,当前令牌-1,请求成功 44 | l.currentTokens-- 45 | return true 46 | } 47 | 48 | func minInt(a, b int) int { 49 | if a < b { 50 | return a 51 | } 52 | return b 53 | } 54 | -------------------------------------------------------------------------------- /fixed_window_limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewFixedWindowLimiter(t *testing.T) { 9 | type args struct { 10 | limit int 11 | window time.Duration 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *FixedWindowLimiter 17 | }{ 18 | { 19 | name: "100_second", 20 | args: args{ 21 | limit: 100, 22 | window: time.Second, 23 | }, 24 | want: nil, 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | l := NewFixedWindowLimiter(tt.args.limit, tt.args.window) 30 | successCount := 0 31 | for i := 0; i < tt.args.limit*2; i++ { 32 | if l.TryAcquire() { 33 | successCount++ 34 | } 35 | } 36 | if successCount != tt.args.limit { 37 | t.Errorf("NewFixedWindowLimiter() = %v, want %v", successCount, tt.args.limit) 38 | } 39 | time.Sleep(time.Second) 40 | successCount = 0 41 | for i := 0; i < tt.args.limit*2; i++ { 42 | if l.TryAcquire() { 43 | successCount++ 44 | } 45 | } 46 | if successCount != tt.args.limit { 47 | t.Errorf("NewFixedWindowLimiter() = %v, want %v", successCount, tt.args.limit) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /leaky_bucket_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // LeakyBucketLimiter 漏桶限流器 9 | type LeakyBucketLimiter struct { 10 | peakLevel int // 最高水位 11 | currentLevel int // 当前水位 12 | currentVelocity int // 水流速度/秒 13 | lastTime time.Time // 上次放水时间 14 | mutex sync.Mutex // 避免并发问题 15 | } 16 | 17 | func NewLeakyBucketLimiter(peakLevel, currentVelocity int) *LeakyBucketLimiter { 18 | return &LeakyBucketLimiter{ 19 | peakLevel: peakLevel, 20 | currentVelocity: currentVelocity, 21 | lastTime: time.Now(), 22 | } 23 | } 24 | 25 | func (l *LeakyBucketLimiter) TryAcquire() bool { 26 | l.mutex.Lock() 27 | defer l.mutex.Unlock() 28 | 29 | // 尝试放水 30 | now := time.Now() 31 | // 距离上次放水的时间 32 | interval := now.Sub(l.lastTime) 33 | if interval >= time.Second { 34 | // 当前水位-距离上次放水的时间(秒)*水流速度 35 | l.currentLevel = maxInt(0, l.currentLevel-int(interval/time.Second)*l.currentVelocity) 36 | l.lastTime = now 37 | } 38 | 39 | // 若到达最高水位,请求失败 40 | if l.currentLevel >= l.peakLevel { 41 | return false 42 | } 43 | // 若没有到达最高水位,当前水位+1,请求成功 44 | l.currentLevel++ 45 | return true 46 | } 47 | 48 | func maxInt(a, b int) int { 49 | if a > b { 50 | return a 51 | } 52 | return b 53 | } 54 | -------------------------------------------------------------------------------- /token_bucket_limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewTokenBucketLimiter(t *testing.T) { 9 | type args struct { 10 | capacity int 11 | rate int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *TokenBucketLimiter 17 | }{ 18 | { 19 | name: "60", 20 | args: args{ 21 | capacity: 60, 22 | rate: 10, 23 | }, 24 | want: nil, 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | l := NewTokenBucketLimiter(tt.args.capacity, tt.args.rate) 30 | time.Sleep(time.Second) 31 | successCount := 0 32 | for i := 0; i < tt.args.rate; i++ { 33 | if l.TryAcquire() { 34 | successCount++ 35 | } 36 | } 37 | if successCount != tt.args.rate { 38 | t.Errorf("NewTokenBucketLimiter() got = %v, want %v", successCount, tt.args.rate) 39 | return 40 | } 41 | 42 | successCount = 0 43 | for i := 0; i < tt.args.capacity; i++ { 44 | if l.TryAcquire() { 45 | successCount++ 46 | } 47 | time.Sleep(time.Second / 10) 48 | } 49 | if successCount != tt.args.capacity-tt.args.rate { 50 | t.Errorf("NewTokenBucketLimiter() got = %v, want %v", successCount, tt.args.capacity-tt.args.rate) 51 | return 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /leaky_bucket_limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewLeakyBucketLimiter(t *testing.T) { 9 | type args struct { 10 | peakLevel int 11 | currentVelocity int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *LeakyBucketLimiter 17 | wantErr bool 18 | }{ 19 | { 20 | name: "60", 21 | args: args{ 22 | peakLevel: 60, 23 | currentVelocity: 10, 24 | }, 25 | want: nil, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | l := NewLeakyBucketLimiter(tt.args.peakLevel, tt.args.currentVelocity) 31 | successCount := 0 32 | for i := 0; i < tt.args.peakLevel; i++ { 33 | if l.TryAcquire() { 34 | successCount++ 35 | } 36 | } 37 | if successCount != tt.args.peakLevel { 38 | t.Errorf("NewLeakyBucketLimiter() got = %v, want %v", successCount, tt.args.peakLevel) 39 | return 40 | } 41 | 42 | successCount = 0 43 | for i := 0; i < tt.args.peakLevel; i++ { 44 | if l.TryAcquire() { 45 | successCount++ 46 | } 47 | time.Sleep(time.Second / 10) 48 | } 49 | if successCount != tt.args.peakLevel-tt.args.currentVelocity { 50 | t.Errorf("NewLeakyBucketLimiter() got = %v, want %v", successCount, tt.args.peakLevel-tt.args.currentVelocity) 51 | return 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /redis/fixed_window_limiter_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewFixedWindowLimiter(t *testing.T) { 11 | type args struct { 12 | limit int 13 | window time.Duration 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want *FixedWindowLimiter 19 | }{ 20 | { 21 | name: "100_second", 22 | args: args{ 23 | limit: 100, 24 | window: time.Second, 25 | }, 26 | want: nil, 27 | }, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | client := redis.NewClient(&redis.Options{ 32 | Addr: "127.0.0.1:6379", 33 | }) 34 | l, _ := NewFixedWindowLimiter(client, tt.args.limit, tt.args.window) 35 | successCount := 0 36 | for i := 0; i < tt.args.limit*2; i++ { 37 | if l.TryAcquire(context.Background(), "test") == nil { 38 | successCount++ 39 | } 40 | } 41 | if successCount != tt.args.limit { 42 | t.Errorf("NewFixedWindowLimiter() = %v, want %v", successCount, tt.args.limit) 43 | } 44 | time.Sleep(time.Second) 45 | successCount = 0 46 | for i := 0; i < tt.args.limit*2; i++ { 47 | if l.TryAcquire(context.Background(), "test") == nil { 48 | successCount++ 49 | } 50 | } 51 | if successCount != tt.args.limit { 52 | t.Errorf("NewFixedWindowLimiter() = %v, want %v", successCount, tt.args.limit) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /redis/token_bucket_limiter_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewTokenBucketLimiter(t *testing.T) { 11 | type args struct { 12 | capacity int 13 | rate int 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want *TokenBucketLimiter 19 | }{ 20 | { 21 | name: "60", 22 | args: args{ 23 | capacity: 60, 24 | rate: 10, 25 | }, 26 | want: nil, 27 | }, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | client := redis.NewClient(&redis.Options{ 32 | Addr: "127.0.0.1:6379", 33 | }) 34 | l := NewTokenBucketLimiter(client, tt.args.capacity, tt.args.rate) 35 | successCount := 0 36 | for i := 0; i < tt.args.capacity; i++ { 37 | if l.TryAcquire(context.Background(), "test") == nil { 38 | successCount++ 39 | } 40 | } 41 | if successCount != tt.args.capacity { 42 | t.Errorf("NewTokenBucketLimiter() got = %v, want %v", successCount, tt.args.capacity) 43 | return 44 | } 45 | 46 | time.Sleep(time.Second) 47 | successCount = 0 48 | for i := 0; i < tt.args.rate; i++ { 49 | if l.TryAcquire(context.Background(), "test") == nil { 50 | successCount++ 51 | } 52 | } 53 | if successCount != tt.args.rate { 54 | t.Errorf("NewTokenBucketLimiter() got = %v, want %v", successCount, tt.args.rate) 55 | return 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /redis/leaky_bucket_limiter_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewLeakyBucketLimiter(t *testing.T) { 11 | type args struct { 12 | peakLevel int 13 | currentVelocity int 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want *LeakyBucketLimiter 19 | wantErr bool 20 | }{ 21 | { 22 | name: "60", 23 | args: args{ 24 | peakLevel: 60, 25 | currentVelocity: 10, 26 | }, 27 | want: nil, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | client := redis.NewClient(&redis.Options{ 33 | Addr: "127.0.0.1:6379", 34 | }) 35 | l := NewLeakyBucketLimiter(client, tt.args.peakLevel, tt.args.currentVelocity) 36 | successCount := 0 37 | for i := 0; i < tt.args.peakLevel*2; i++ { 38 | if l.TryAcquire(context.Background(), "test") == nil { 39 | successCount++ 40 | } 41 | } 42 | if successCount != tt.args.peakLevel { 43 | t.Errorf("NewLeakyBucketLimiter() got = %v, want %v", successCount, tt.args.peakLevel) 44 | return 45 | } 46 | 47 | time.Sleep(time.Second * time.Duration(tt.args.peakLevel/tt.args.currentVelocity) / 2) 48 | successCount = 0 49 | for i := 0; i < tt.args.peakLevel; i++ { 50 | if l.TryAcquire(context.Background(), "test") == nil { 51 | successCount++ 52 | } 53 | } 54 | if successCount != tt.args.peakLevel/2 { 55 | t.Errorf("NewLeakyBucketLimiter() got = %v, want %v", successCount, tt.args.peakLevel/2) 56 | return 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sliding_window_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // SlidingWindowLimiter 滑动窗口限流器 10 | type SlidingWindowLimiter struct { 11 | limit int // 窗口请求上限 12 | window int64 // 窗口时间大小 13 | smallWindow int64 // 小窗口时间大小 14 | smallWindows int64 // 小窗口数量 15 | counters map[int64]int // 小窗口计数器 16 | mutex sync.Mutex // 避免并发问题 17 | } 18 | 19 | func NewSlidingWindowLimiter(limit int, window, smallWindow time.Duration) (*SlidingWindowLimiter, error) { 20 | // 窗口时间必须能够被小窗口时间整除 21 | if window%smallWindow != 0 { 22 | return nil, errors.New("window cannot be split by integers") 23 | } 24 | 25 | return &SlidingWindowLimiter{ 26 | limit: limit, 27 | window: int64(window), 28 | smallWindow: int64(smallWindow), 29 | smallWindows: int64(window / smallWindow), 30 | counters: make(map[int64]int), 31 | }, nil 32 | } 33 | 34 | func (l *SlidingWindowLimiter) TryAcquire() bool { 35 | l.mutex.Lock() 36 | defer l.mutex.Unlock() 37 | 38 | // 获取当前小窗口值 39 | currentSmallWindow := time.Now().UnixNano() / l.smallWindow * l.smallWindow 40 | // 获取起始小窗口值 41 | startSmallWindow := currentSmallWindow - l.smallWindow*(l.smallWindows-1) 42 | 43 | // 计算当前窗口的请求总数 44 | var count int 45 | for smallWindow, counter := range l.counters { 46 | if smallWindow < startSmallWindow { 47 | delete(l.counters, smallWindow) 48 | } else { 49 | count += counter 50 | } 51 | } 52 | 53 | // 若到达窗口请求上限,请求失败 54 | if count >= l.limit { 55 | return false 56 | } 57 | // 若没到窗口请求上限,当前小窗口计数器+1,请求成功 58 | l.counters[currentSmallWindow]++ 59 | return true 60 | } 61 | -------------------------------------------------------------------------------- /redis/fixed_window_limiter.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/go-redis/redis/v8" 7 | "time" 8 | ) 9 | 10 | const fixedWindowLimiterTryAcquireRedisScript = ` 11 | -- ARGV[1]: 窗口时间大小 12 | -- ARGV[2]: 窗口请求上限 13 | 14 | local window = tonumber(ARGV[1]) 15 | local limit = tonumber(ARGV[2]) 16 | 17 | -- 获取原始值 18 | local counter = tonumber(redis.call("get", KEYS[1])) 19 | if counter == nil then 20 | counter = 0 21 | end 22 | -- 若到达窗口请求上限,请求失败 23 | if counter >= limit then 24 | return 0 25 | end 26 | -- 窗口值+1 27 | redis.call("incr", KEYS[1]) 28 | if counter == 0 then 29 | redis.call("pexpire", KEYS[1], window) 30 | end 31 | return 1 32 | ` 33 | 34 | // FixedWindowLimiter 固定窗口限流器 35 | type FixedWindowLimiter struct { 36 | limit int // 窗口请求上限 37 | window int // 窗口时间大小 38 | client *redis.Client // Redis客户端 39 | script *redis.Script // TryAcquire脚本 40 | } 41 | 42 | func NewFixedWindowLimiter(client *redis.Client, limit int, window time.Duration) (*FixedWindowLimiter, error) { 43 | // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除 44 | if window%time.Millisecond != 0 { 45 | return nil, errors.New("the window uint must not be less than millisecond") 46 | } 47 | 48 | return &FixedWindowLimiter{ 49 | limit: limit, 50 | window: int(window / time.Millisecond), 51 | client: client, 52 | script: redis.NewScript(fixedWindowLimiterTryAcquireRedisScript), 53 | }, nil 54 | } 55 | 56 | func (l *FixedWindowLimiter) TryAcquire(ctx context.Context, resource string) error { 57 | success, err := l.script.Run(ctx, l.client, []string{resource}, l.window, l.limit).Bool() 58 | if err != nil { 59 | return err 60 | } 61 | // 若到达窗口请求上限,请求失败 62 | if !success { 63 | return ErrAcquireFailed 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /sliding_window_limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewSlidingWindowLimiter(t *testing.T) { 9 | type args struct { 10 | limit int 11 | window time.Duration 12 | smallWindow time.Duration 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *SlidingWindowLimiter 18 | wantErr bool 19 | }{ 20 | { 21 | name: "60_5seconds", 22 | args: args{ 23 | limit: 60, 24 | window: time.Second * 5, 25 | smallWindow: time.Second, 26 | }, 27 | want: nil, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | l, err := NewSlidingWindowLimiter(tt.args.limit, tt.args.window, tt.args.smallWindow) 33 | if err != nil { 34 | t.Errorf("NewSlidingWindowLimiter() error = %v", err) 35 | return 36 | } 37 | successCount := 0 38 | for i := 0; i < tt.args.limit/2; i++ { 39 | if l.TryAcquire() { 40 | successCount++ 41 | } 42 | } 43 | if successCount != tt.args.limit/2 { 44 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit/2) 45 | return 46 | } 47 | 48 | time.Sleep(time.Second * 2) 49 | successCount = 0 50 | for i := 0; i < tt.args.limit-tt.args.limit/2; i++ { 51 | if l.TryAcquire() { 52 | successCount++ 53 | } 54 | } 55 | if successCount != tt.args.limit-tt.args.limit/2 { 56 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit-tt.args.limit/2) 57 | } 58 | 59 | time.Sleep(time.Second * 3) 60 | successCount = 0 61 | for i := 0; i < tt.args.limit/2; i++ { 62 | if l.TryAcquire() { 63 | successCount++ 64 | } 65 | } 66 | if successCount != tt.args.limit/2 { 67 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit/2) 68 | return 69 | } 70 | 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /redis/sliding_window_limiter_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewSlidingWindowLimiter(t *testing.T) { 11 | type args struct { 12 | limit int 13 | window time.Duration 14 | smallWindow time.Duration 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want *SlidingWindowLimiter 20 | wantErr bool 21 | }{ 22 | { 23 | name: "60_5seconds", 24 | args: args{ 25 | limit: 60, 26 | window: time.Second * 5, 27 | smallWindow: time.Second, 28 | }, 29 | want: nil, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | client := redis.NewClient(&redis.Options{ 35 | Addr: "127.0.0.1:6379", 36 | }) 37 | l, err := NewSlidingWindowLimiter(client, tt.args.limit, tt.args.window, tt.args.smallWindow) 38 | if err != nil { 39 | t.Errorf("NewSlidingWindowLimiter() error = %v", err) 40 | return 41 | } 42 | successCount := 0 43 | for i := 0; i < tt.args.limit/2; i++ { 44 | if l.TryAcquire(context.Background(), "test") == nil { 45 | successCount++ 46 | } 47 | } 48 | if successCount != tt.args.limit/2 { 49 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit/2) 50 | return 51 | } 52 | 53 | time.Sleep(time.Second * 2) 54 | successCount = 0 55 | for i := 0; i < tt.args.limit/3; i++ { 56 | if l.TryAcquire(context.Background(), "test") == nil { 57 | successCount++ 58 | } 59 | } 60 | if successCount != tt.args.limit/3 { 61 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit/3) 62 | } 63 | 64 | time.Sleep(time.Second * 3) 65 | successCount = 0 66 | for i := 0; i < tt.args.limit; i++ { 67 | if l.TryAcquire(context.Background(), "test") == nil { 68 | successCount++ 69 | } 70 | } 71 | if successCount != tt.args.limit/3*2 { 72 | t.Errorf("NewSlidingWindowLimiter() got = %v, want %v", successCount, tt.args.limit/3*2) 73 | return 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /redis/token_bucket_limiter.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "time" 7 | ) 8 | 9 | const tokenBucketLimiterTryAcquireRedisScript = ` 10 | -- ARGV[1]: 最高水位 11 | -- ARGV[2]: 水流速度/秒 12 | -- ARGV[3]: 当前时间(秒) 13 | 14 | local capacity = tonumber(ARGV[1]) 15 | local rate = tonumber(ARGV[2]) 16 | local now = tonumber(ARGV[3]) 17 | 18 | local lastTime = tonumber(redis.call("hget", KEYS[1], "lastTime")) 19 | local currentTokens = tonumber(redis.call("hget", KEYS[1], "currentTokens")) 20 | -- 初始化 21 | if lastTime == nil then 22 | lastTime = now 23 | currentTokens = capacity 24 | redis.call("hmset", KEYS[1], "currentTokens", currentTokens, "lastTime", lastTime) 25 | end 26 | 27 | -- 尝试发放令牌 28 | -- 距离上次发放令牌的时间 29 | local interval = now - lastTime 30 | if interval > 0 then 31 | -- 当前令牌数量+距离上次发放令牌的时间(秒)*发放令牌速率 32 | local newTokens = currentTokens + interval * rate 33 | if newTokens > capacity then 34 | newTokens = capacity 35 | end 36 | currentTokens = newTokens 37 | redis.call("hmset", KEYS[1], "currentTokens", newTokens, "lastTime", now) 38 | end 39 | 40 | -- 如果没有令牌,请求失败 41 | if currentTokens == 0 then 42 | return 0 43 | end 44 | -- 果有令牌,当前令牌-1,请求成功 45 | redis.call("hincrby", KEYS[1], "currentTokens", -1) 46 | redis.call("expire", KEYS[1], capacity / rate) 47 | return 1 48 | ` 49 | 50 | // TokenBucketLimiter 令牌桶限流器 51 | type TokenBucketLimiter struct { 52 | capacity int // 容量 53 | rate int // 发放令牌速率/秒 54 | client *redis.Client // Redis客户端 55 | script *redis.Script // TryAcquire脚本 56 | } 57 | 58 | func NewTokenBucketLimiter(client *redis.Client, capacity, rate int) *TokenBucketLimiter { 59 | return &TokenBucketLimiter{ 60 | capacity: capacity, 61 | rate: rate, 62 | client: client, 63 | script: redis.NewScript(tokenBucketLimiterTryAcquireRedisScript), 64 | } 65 | } 66 | 67 | func (l *TokenBucketLimiter) TryAcquire(ctx context.Context, resource string) error { 68 | // 当前时间 69 | now := time.Now().Unix() 70 | success, err := l.script.Run(ctx, l.client, []string{resource}, l.capacity, l.rate, now).Bool() 71 | if err != nil { 72 | return err 73 | } 74 | // 若到达窗口请求上限,请求失败 75 | if !success { 76 | return ErrAcquireFailed 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /redis/leaky_bucket_limiter.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | "time" 7 | ) 8 | 9 | const leakyBucketLimiterTryAcquireRedisScript = ` 10 | -- ARGV[1]: 最高水位 11 | -- ARGV[2]: 水流速度/秒 12 | -- ARGV[3]: 当前时间(秒) 13 | 14 | local peakLevel = tonumber(ARGV[1]) 15 | local currentVelocity = tonumber(ARGV[2]) 16 | local now = tonumber(ARGV[3]) 17 | 18 | local lastTime = tonumber(redis.call("hget", KEYS[1], "lastTime")) 19 | local currentLevel = tonumber(redis.call("hget", KEYS[1], "currentLevel")) 20 | -- 初始化 21 | if lastTime == nil then 22 | lastTime = now 23 | currentLevel = 0 24 | redis.call("hmset", KEYS[1], "currentLevel", currentLevel, "lastTime", lastTime) 25 | end 26 | 27 | -- 尝试放水 28 | -- 距离上次放水的时间 29 | local interval = now - lastTime 30 | if interval > 0 then 31 | -- 当前水位-距离上次放水的时间(秒)*水流速度 32 | local newLevel = currentLevel - interval * currentVelocity 33 | if newLevel < 0 then 34 | newLevel = 0 35 | end 36 | currentLevel = newLevel 37 | redis.call("hmset", KEYS[1], "currentLevel", newLevel, "lastTime", now) 38 | end 39 | 40 | -- 若到达最高水位,请求失败 41 | if currentLevel >= peakLevel then 42 | return 0 43 | end 44 | -- 若没有到达最高水位,当前水位+1,请求成功 45 | redis.call("hincrby", KEYS[1], "currentLevel", 1) 46 | redis.call("expire", KEYS[1], peakLevel / currentVelocity) 47 | return 1 48 | ` 49 | 50 | // LeakyBucketLimiter 漏桶限流器 51 | type LeakyBucketLimiter struct { 52 | peakLevel int // 最高水位 53 | currentVelocity int // 水流速度/秒 54 | client *redis.Client // Redis客户端 55 | script *redis.Script // TryAcquire脚本 56 | } 57 | 58 | func NewLeakyBucketLimiter(client *redis.Client, peakLevel, currentVelocity int) *LeakyBucketLimiter { 59 | return &LeakyBucketLimiter{ 60 | peakLevel: peakLevel, 61 | currentVelocity: currentVelocity, 62 | client: client, 63 | script: redis.NewScript(leakyBucketLimiterTryAcquireRedisScript), 64 | } 65 | } 66 | 67 | func (l *LeakyBucketLimiter) TryAcquire(ctx context.Context, resource string) error { 68 | // 当前时间 69 | now := time.Now().Unix() 70 | success, err := l.script.Run(ctx, l.client, []string{resource}, l.peakLevel, l.currentVelocity, now).Bool() 71 | if err != nil { 72 | return err 73 | } 74 | // 若到达窗口请求上限,请求失败 75 | if !success { 76 | return ErrAcquireFailed 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /sliding_log_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // ViolationStrategyError 违背策略错误 12 | type ViolationStrategyError struct { 13 | Limit int // 窗口请求上限 14 | Window time.Duration // 窗口时间大小 15 | } 16 | 17 | func (e *ViolationStrategyError) Error() string { 18 | return fmt.Sprintf("violation strategy that limit = %d and window = %d", e.Limit, e.Window) 19 | } 20 | 21 | // SlidingLogLimiterStrategy 滑动日志限流器的策略 22 | type SlidingLogLimiterStrategy struct { 23 | limit int // 窗口请求上限 24 | window int64 // 窗口时间大小 25 | smallWindows int64 // 小窗口数量 26 | } 27 | 28 | func NewSlidingLogLimiterStrategy(limit int, window time.Duration) *SlidingLogLimiterStrategy { 29 | return &SlidingLogLimiterStrategy{ 30 | limit: limit, 31 | window: int64(window), 32 | } 33 | } 34 | 35 | // SlidingLogLimiter 滑动日志限流器 36 | type SlidingLogLimiter struct { 37 | strategies []*SlidingLogLimiterStrategy // 滑动日志限流器策略列表 38 | smallWindow int64 // 小窗口时间大小 39 | counters map[int64]int // 小窗口计数器 40 | mutex sync.Mutex // 避免并发问题 41 | } 42 | 43 | func NewSlidingLogLimiter(smallWindow time.Duration, strategies ...*SlidingLogLimiterStrategy) (*SlidingLogLimiter, error) { 44 | // 复制策略避免被修改 45 | strategies = append(make([]*SlidingLogLimiterStrategy, 0, len(strategies)), strategies...) 46 | 47 | // 不能不设置策略 48 | if len(strategies) == 0 { 49 | return nil, errors.New("must be set strategies") 50 | } 51 | 52 | // 排序策略,窗口时间大的排前面,相同窗口上限大的排前面 53 | sort.Slice(strategies, func(i, j int) bool { 54 | a, b := strategies[i], strategies[j] 55 | if a.window == b.window { 56 | return a.limit > b.limit 57 | } 58 | return a.window > b.window 59 | }) 60 | 61 | for i, strategy := range strategies { 62 | // 随着窗口时间变小,窗口上限也应该变小 63 | if i > 0 { 64 | if strategy.limit >= strategies[i-1].limit { 65 | return nil, errors.New("the smaller window should be the smaller limit") 66 | } 67 | } 68 | // 窗口时间必须能够被小窗口时间整除 69 | if strategy.window%int64(smallWindow) != 0 { 70 | return nil, errors.New("window cannot be split by integers") 71 | } 72 | strategy.smallWindows = strategy.window / int64(smallWindow) 73 | } 74 | 75 | return &SlidingLogLimiter{ 76 | strategies: strategies, 77 | smallWindow: int64(smallWindow), 78 | counters: make(map[int64]int), 79 | }, nil 80 | } 81 | 82 | func (l *SlidingLogLimiter) TryAcquire() error { 83 | l.mutex.Lock() 84 | defer l.mutex.Unlock() 85 | 86 | // 获取当前小窗口值 87 | currentSmallWindow := time.Now().UnixNano() / l.smallWindow * l.smallWindow 88 | // 获取每个策略的起始小窗口值 89 | startSmallWindows := make([]int64, len(l.strategies)) 90 | for i, strategy := range l.strategies { 91 | startSmallWindows[i] = currentSmallWindow - l.smallWindow*(strategy.smallWindows-1) 92 | } 93 | 94 | // 计算每个策略当前窗口的请求总数 95 | counts := make([]int, len(l.strategies)) 96 | for smallWindow, counter := range l.counters { 97 | if smallWindow < startSmallWindows[0] { 98 | delete(l.counters, smallWindow) 99 | continue 100 | } 101 | for i := range l.strategies { 102 | if smallWindow >= startSmallWindows[i] { 103 | counts[i] += counter 104 | } 105 | } 106 | } 107 | 108 | // 若到达对应策略窗口请求上限,请求失败,返回违背的策略 109 | for i, strategy := range l.strategies { 110 | if counts[i] >= strategy.limit { 111 | return &ViolationStrategyError{ 112 | Limit: strategy.limit, 113 | Window: time.Duration(strategy.window), 114 | } 115 | } 116 | } 117 | 118 | // 若没到窗口请求上限,当前小窗口计数器+1,请求成功 119 | l.counters[currentSmallWindow]++ 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /redis/sliding_window_limiter.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/go-redis/redis/v8" 7 | "time" 8 | ) 9 | 10 | const slidingWindowLimiterTryAcquireRedisScriptHashImpl = ` 11 | -- ARGV[1]: 窗口时间大小 12 | -- ARGV[2]: 窗口请求上限 13 | -- ARGV[3]: 当前小窗口值 14 | -- ARGV[4]: 起始小窗口值 15 | 16 | local window = tonumber(ARGV[1]) 17 | local limit = tonumber(ARGV[2]) 18 | local currentSmallWindow = tonumber(ARGV[3]) 19 | local startSmallWindow = tonumber(ARGV[4]) 20 | 21 | -- 计算当前窗口的请求总数 22 | local counters = redis.call("hgetall", KEYS[1]) 23 | local count = 0 24 | for i = 1, #(counters) / 2 do 25 | local smallWindow = tonumber(counters[i * 2 - 1]) 26 | local counter = tonumber(counters[i * 2]) 27 | if smallWindow < startSmallWindow then 28 | redis.call("hdel", KEYS[1], smallWindow) 29 | else 30 | count = count + counter 31 | end 32 | end 33 | 34 | -- 若到达窗口请求上限,请求失败 35 | if count >= limit then 36 | return 0 37 | end 38 | 39 | -- 若没到窗口请求上限,当前小窗口计数器+1,请求成功 40 | redis.call("hincrby", KEYS[1], currentSmallWindow, 1) 41 | redis.call("pexpire", KEYS[1], window) 42 | return 1 43 | ` 44 | 45 | const slidingWindowLimiterTryAcquireRedisScriptListImpl = ` 46 | -- ARGV[1]: 窗口时间大小 47 | -- ARGV[2]: 窗口请求上限 48 | -- ARGV[3]: 当前小窗口值 49 | -- ARGV[4]: 起始小窗口值 50 | 51 | local window = tonumber(ARGV[1]) 52 | local limit = tonumber(ARGV[2]) 53 | local currentSmallWindow = tonumber(ARGV[3]) 54 | local startSmallWindow = tonumber(ARGV[4]) 55 | 56 | -- 获取list长度 57 | local len = redis.call("llen", KEYS[1]) 58 | -- 如果长度是0,设置counter,长度+1 59 | local counter = 0 60 | if len == 0 then 61 | redis.call("rpush", KEYS[1], 0) 62 | redis.call("pexpire", KEYS[1], window) 63 | len = len + 1 64 | else 65 | -- 如果长度大于1,获取第二第个元素 66 | local smallWindow1 = tonumber(redis.call("lindex", KEYS[1], 1)) 67 | counter = tonumber(redis.call("lindex", KEYS[1], 0)) 68 | -- 如果该值小于起始小窗口值 69 | if smallWindow1 < startSmallWindow then 70 | local count1 = redis.call("lindex", KEYS[1], 2) 71 | -- counter-第三个元素的值 72 | counter = counter - count1 73 | -- 长度-2 74 | len = len - 2 75 | -- 删除第二第三个元素 76 | redis.call("lrem", KEYS[1], 1, smallWindow1) 77 | redis.call("lrem", KEYS[1], 1, count1) 78 | end 79 | end 80 | 81 | -- 若到达窗口请求上限,请求失败 82 | if counter >= limit then 83 | return 0 84 | end 85 | 86 | -- 如果长度大于1,获取倒数第二第一个元素 87 | if len > 1 then 88 | local smallWindown = tonumber(redis.call("lindex", KEYS[1], -2)) 89 | -- 如果倒数第二个元素小窗口值大于等于当前小窗口值 90 | if smallWindown >= currentSmallWindow then 91 | -- 把倒数第二个元素当成当前小窗口(因为它更新),倒数第一个元素值+1 92 | local countn = redis.call("lindex", KEYS[1], -1) 93 | redis.call("lset", KEYS[1], -1, countn + 1) 94 | else 95 | -- 否则,添加新的窗口值,添加新的计数(1),更新过期时间 96 | redis.call("rpush", KEYS[1], currentSmallWindow, 1) 97 | redis.call("pexpire", KEYS[1], window) 98 | end 99 | else 100 | -- 否则,添加新的窗口值,添加新的计数(1),更新过期时间 101 | redis.call("rpush", KEYS[1], currentSmallWindow, 1) 102 | redis.call("pexpire", KEYS[1], window) 103 | end 104 | 105 | -- counter + 1并更新 106 | redis.call("lset", KEYS[1], 0, counter + 1) 107 | return 1 108 | ` 109 | 110 | // SlidingWindowLimiter 滑动窗口限流器 111 | type SlidingWindowLimiter struct { 112 | limit int // 窗口请求上限 113 | window int64 // 窗口时间大小 114 | smallWindow int64 // 小窗口时间大小 115 | smallWindows int64 // 小窗口数量 116 | client *redis.Client // Redis客户端 117 | script *redis.Script // TryAcquire脚本 118 | } 119 | 120 | func NewSlidingWindowLimiter(client *redis.Client, limit int, window, smallWindow time.Duration) ( 121 | *SlidingWindowLimiter, error) { 122 | // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除 123 | if window%time.Millisecond != 0 || smallWindow%time.Millisecond != 0 { 124 | return nil, errors.New("the window uint must not be less than millisecond") 125 | } 126 | 127 | // 窗口时间必须能够被小窗口时间整除 128 | if window%smallWindow != 0 { 129 | return nil, errors.New("window cannot be split by integers") 130 | } 131 | 132 | return &SlidingWindowLimiter{ 133 | limit: limit, 134 | window: int64(window / time.Millisecond), 135 | smallWindow: int64(smallWindow / time.Millisecond), 136 | smallWindows: int64(window / smallWindow), 137 | client: client, 138 | script: redis.NewScript(slidingWindowLimiterTryAcquireRedisScriptListImpl), 139 | }, nil 140 | } 141 | 142 | func (l *SlidingWindowLimiter) TryAcquire(ctx context.Context, resource string) error { 143 | // 获取当前小窗口值 144 | currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow 145 | // 获取起始小窗口值 146 | startSmallWindow := currentSmallWindow - l.smallWindow*(l.smallWindows-1) 147 | 148 | success, err := l.script.Run( 149 | ctx, l.client, []string{resource}, l.window, l.limit, currentSmallWindow, startSmallWindow).Bool() 150 | if err != nil { 151 | return err 152 | } 153 | // 若到达窗口请求上限,请求失败 154 | if !success { 155 | return ErrAcquireFailed 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /redis/sliding_log_limiter.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/go-redis/redis/v8" 8 | "sort" 9 | "time" 10 | ) 11 | 12 | const slidingLogLimiterTryAcquireRedisScriptHashImpl = ` 13 | -- ARGV[1]: 当前小窗口值 14 | -- ARGV[2]: 第一个策略的窗口时间大小 15 | -- ARGV[i * 2 + 1]: 每个策略的起始小窗口值 16 | -- ARGV[i * 2 + 2]: 每个策略的窗口请求上限 17 | 18 | local currentSmallWindow = tonumber(ARGV[1]) 19 | -- 第一个策略的窗口时间大小 20 | local window = tonumber(ARGV[2]) 21 | -- 第一个策略的起始小窗口值 22 | local startSmallWindow = tonumber(ARGV[3]) 23 | local strategiesLen = #(ARGV) / 2 - 1 24 | 25 | -- 计算每个策略当前窗口的请求总数 26 | local counters = redis.call("hgetall", KEYS[1]) 27 | local counts = {} 28 | -- 初始化counts 29 | for j = 1, strategiesLen do 30 | counts[j] = 0 31 | end 32 | 33 | for i = 1, #(counters) / 2 do 34 | local smallWindow = tonumber(counters[i * 2 - 1]) 35 | local counter = tonumber(counters[i * 2]) 36 | if smallWindow < startSmallWindow then 37 | redis.call("hdel", KEYS[1], smallWindow) 38 | else 39 | for j = 1, strategiesLen do 40 | if smallWindow >= tonumber(ARGV[j * 2 + 1]) then 41 | counts[j] = counts[j] + counter 42 | end 43 | end 44 | end 45 | end 46 | 47 | -- 若到达对应策略窗口请求上限,请求失败,返回违背的策略下标 48 | for i = 1, strategiesLen do 49 | if counts[i] >= tonumber(ARGV[i * 2 + 2]) then 50 | return i - 1 51 | end 52 | end 53 | 54 | -- 若没到窗口请求上限,当前小窗口计数器+1,请求成功 55 | redis.call("hincrby", KEYS[1], currentSmallWindow, 1) 56 | redis.call("pexpire", KEYS[1], window) 57 | return -1 58 | ` 59 | 60 | // ViolationStrategyError 违背策略错误 61 | type ViolationStrategyError struct { 62 | Limit int // 窗口请求上限 63 | Window time.Duration // 窗口时间大小 64 | } 65 | 66 | func (e *ViolationStrategyError) Error() string { 67 | return fmt.Sprintf("violation strategy that limit = %d and window = %d", e.Limit, e.Window) 68 | } 69 | 70 | // SlidingLogLimiterStrategy 滑动日志限流器的策略 71 | type SlidingLogLimiterStrategy struct { 72 | limit int // 窗口请求上限 73 | window int64 // 窗口时间大小 74 | smallWindows int64 // 小窗口数量 75 | } 76 | 77 | func NewSlidingLogLimiterStrategy(limit int, window time.Duration) *SlidingLogLimiterStrategy { 78 | return &SlidingLogLimiterStrategy{ 79 | limit: limit, 80 | window: int64(window), 81 | } 82 | } 83 | 84 | // SlidingLogLimiter 滑动日志限流器 85 | type SlidingLogLimiter struct { 86 | strategies []*SlidingLogLimiterStrategy // 滑动日志限流器策略列表 87 | smallWindow int64 // 小窗口时间大小 88 | client *redis.Client // Redis客户端 89 | script *redis.Script // TryAcquire脚本 90 | } 91 | 92 | func NewSlidingLogLimiter(client *redis.Client, smallWindow time.Duration, strategies ...*SlidingLogLimiterStrategy) ( 93 | *SlidingLogLimiter, error) { 94 | // 复制策略避免被修改 95 | strategies = append(make([]*SlidingLogLimiterStrategy, 0, len(strategies)), strategies...) 96 | 97 | // 不能不设置策略 98 | if len(strategies) == 0 { 99 | return nil, errors.New("must be set strategies") 100 | } 101 | 102 | // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除 103 | if smallWindow%time.Millisecond != 0 { 104 | return nil, errors.New("the window uint must not be less than millisecond") 105 | } 106 | smallWindow = smallWindow / time.Millisecond 107 | for _, strategy := range strategies { 108 | if strategy.window%int64(time.Millisecond) != 0 { 109 | return nil, errors.New("the window uint must not be less than millisecond") 110 | } 111 | strategy.window = strategy.window / int64(time.Millisecond) 112 | } 113 | 114 | // 排序策略,窗口时间大的排前面,相同窗口上限大的排前面 115 | sort.Slice(strategies, func(i, j int) bool { 116 | a, b := strategies[i], strategies[j] 117 | if a.window == b.window { 118 | return a.limit > b.limit 119 | } 120 | return a.window > b.window 121 | }) 122 | 123 | for i, strategy := range strategies { 124 | // 随着窗口时间变小,窗口上限也应该变小 125 | if i > 0 { 126 | if strategy.limit >= strategies[i-1].limit { 127 | return nil, errors.New("the smaller window should be the smaller limit") 128 | } 129 | } 130 | // 窗口时间必须能够被小窗口时间整除 131 | if strategy.window%int64(smallWindow) != 0 { 132 | return nil, errors.New("window cannot be split by integers") 133 | } 134 | strategy.smallWindows = strategy.window / int64(smallWindow) 135 | } 136 | 137 | return &SlidingLogLimiter{ 138 | strategies: strategies, 139 | smallWindow: int64(smallWindow), 140 | client: client, 141 | script: redis.NewScript(slidingLogLimiterTryAcquireRedisScriptHashImpl), 142 | }, nil 143 | } 144 | 145 | func (l *SlidingLogLimiter) TryAcquire(ctx context.Context, resource string) error { 146 | // 获取当前小窗口值 147 | currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow 148 | args := make([]interface{}, len(l.strategies)*2+2) 149 | args[0] = currentSmallWindow 150 | args[1] = l.strategies[0].window 151 | // 获取每个策略的起始小窗口值 152 | for i, strategy := range l.strategies { 153 | args[i*2+2] = currentSmallWindow - l.smallWindow*(strategy.smallWindows-1) 154 | args[i*2+3] = strategy.limit 155 | } 156 | 157 | index, err := l.script.Run( 158 | ctx, l.client, []string{resource}, args...).Int() 159 | if err != nil { 160 | return err 161 | } 162 | // 若到达窗口请求上限,请求失败 163 | if index != -1 { 164 | return &ViolationStrategyError{ 165 | Limit: l.strategies[index].limit, 166 | Window: time.Duration(l.strategies[index].window), 167 | } 168 | } 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 10 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 11 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 14 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 15 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 16 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 17 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 18 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 19 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 20 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 28 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 31 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 32 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 33 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 34 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 38 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 41 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 42 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 43 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 44 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 45 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 46 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 47 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 48 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 49 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 64 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 65 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 66 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 69 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 70 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 75 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 76 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 77 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 78 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 79 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 80 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 81 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 84 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 85 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 88 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------