-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlua_script.go
More file actions
355 lines (289 loc) · 10.5 KB
/
Copy pathlua_script.go
File metadata and controls
355 lines (289 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// Copyright(C) 2024 Github Inc. All Rights Reserved.
// Author: metrue8@gmail.com
// Date: 2024/01/03
package ratelimiter
import (
"bufio"
"fmt"
"strings"
)
var luaScriptMap, luaScriptOptMap map[string]string
func init() {
luaScriptMap = make(map[string]string, 4)
// 固定窗口限流脚本
luaScriptMap["FixedWindowScript"] = `
--[[
Description: 基于 Reids String 实现, 可指定时间窗口作为限流周期
1. key - [V] 限流 key
2. limit - [V] 限流大小
3. unitTime - [-] 窗口大小, 默认窗口1s
4. expiration - [-] Key的过期时间, 默认过期2s
--]]
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local unitTime = 1
if ARGV[2] ~= nil then
unitTime = tonumber(ARGV[2])
end
-- 设定过期周期(300~3600s)
local expiration = math.ceil(unitTime * 2)
if ARGV[3] ~= nil then
expiration = tonumber(ARGV[3])
end
local current = tonumber(redis.call('GET', key) or "0")
-- 超出限流大小
if current and current >= limit then
return 0
end
current = redis.call('INCR', key)
-- 第一次请求, 则设置过期时间
if current == 1 then
redis.call('EXPIRE', key, expiration)
end
-- 返回剩余可用请求数
return limit - current + 1
`
// 滑动窗口限流脚本
luaScriptMap["SlideWindowScript"] = `
--[[
Description: 基于 Reids Hash 实现, 最小窗口限制为1s, 最大窗口限制为3600s
流量会在时间窗口基础上再拆分为更细粒度的窗口进行存储, 流量的放开会随着时间的滚动而逐步放开流量限制
1. key - [V] 限流 key
2. limitCount - [V] 单个时间窗口限制数量
3. curTime - [V] 当前时间, 单位ms
4. unitTime - [V] 时间窗口范围, 传参单位秒, 默认窗口1秒
5. expiration - [V] 集合key过期时间, 当key过期时会存在瞬时并发的情况, 因此过期时间不能太短或者改用定时清除
--]]
local key = KEYS[1]
local limitCount = tonumber(ARGV[1])
local curTime = tonumber(ARGV[2])
local unitTime = tonumber(ARGV[3]) * 1000
local expiration = tonumber(ARGV[4])
local newTime = curTime
local diffVal = unitTime
local constKeyCnt = 1000
if unitTime > 1000 then
local littleWin = math.ceil(unitTime / constKeyCnt)
newTime = math.floor(curTime / littleWin)
diffVal = math.floor(unitTime / littleWin)
end
-- 已访问的次数
local beforeCount = 0
local flatMap = redis.call('HGETALL', key)
if table.maxn(flatMap) > 0 then
for i = 1, #flatMap, 2 do
local ftime = tonumber(flatMap[i])
if newTime - ftime < diffVal then
beforeCount = beforeCount + tonumber(flatMap[i + 1])
else
redis.call('HDEL', key, tostring(ftime))
end
end
end
local result = 0
if limitCount <= beforeCount then
return result
end
result = limitCount - beforeCount
redis.call('HINCRBY', key, tostring(newTime), '1')
redis.call('EXPIRE', key, expiration)
-- 返回剩余可用请求量,含本次请求
return result
`
// 令牌桶限流脚本
luaScriptMap["TokenBucketScript"] = `
--[[
Description: 基于 Reids Hash 实现
1. key - [V] 令牌桶的 key
2. intervalPerPermit - [V] 生成令牌的间隔(ms)
3. curTime - [V] 当前时间(ms)
4. bucketMaxTokens - [V] 令牌桶的上限
5. resetBucketInterval - [V] 重置桶内令牌的时间间隔(ms)
6. initTokens - [-] 令牌桶初始化的令牌数
7. currentTokens - 当前桶内令牌数
8. bucket - 当前 key 的令牌桶对象
--]]
local key = KEYS[1]
local intervalPerPermit = tonumber(ARGV[1])
local curTime = tonumber(ARGV[2])
local bucketMaxTokens = tonumber(ARGV[3])
local resetBucketInterval = tonumber(ARGV[4])
local initTokens = 0
if ARGV[5] ~= nil then
initTokens = tonumber(ARGV[5])
end
local currentTokens = 0
local bucket = redis.call('HGETALL', key)
-- 若当前桶未初始化,先初始化令牌桶
if table.maxn(bucket) == 0 then
-- 初始桶内令牌
currentTokens = initTokens
-- 设置桶最近的填充时间是当前
redis.call('HSET', key, 'lastRefillTime', curTime)
-- 如果当前令牌 == 0 ,更新桶内令牌, 返回 0
redis.call('HSET', key, 'tokensRemaining', currentTokens)
-- 初始化令牌桶的过期时间, 设置为间隔的 10 倍
redis.call('PEXPIRE', key, resetBucketInterval * 10)
-- 返回令牌数
return math.max(1, currentTokens)
end
-- 上次填充时间
local lastRefillTime = tonumber(bucket[2])
-- 剩余的令牌数
local tokensRemaining = tonumber(bucket[4])
-- 如果当前时间小于或等于上次更新的时间, 当前令牌数量等于桶内令牌数(幂等性)
if curTime <= lastRefillTime then
currentTokens = tokensRemaining
-- 当前时间大于上次填充时间
else
-- 拿到当前时间与上次填充时间的时间间隔
local intervalSinceLast = curTime - lastRefillTime
-- 如果当前时间间隔 大于 令牌的生成间隔
if intervalSinceLast > resetBucketInterval then
-- 将当前令牌填充满
currentTokens = initTokens
-- 更新重新填充时间
redis.call('HSET', key, 'lastRefillTime', curTime)
-- 如果当前时间间隔 小于 令牌的生成间隔
else
-- 可用的令牌数 = 向下取整数( 上次填充时间与当前时间的时间间隔 / 两个令牌许可之间的时间间隔 )
local availableTokens = math.floor(intervalSinceLast / intervalPerPermit)
-- 可授予的令牌 > 0 时
if availableTokens > 0 then
-- 生成的令牌 = 上次填充时间与当前时间的时间间隔 % 两个令牌许可之间的时间间隔
local padMillis = math.fmod(intervalSinceLast, intervalPerPermit)
-- 将当前令牌桶更新到上一次生成时间
redis.call('HSET', key, 'lastRefillTime', curTime - padMillis)
end
-- 更新当前令牌桶中的令牌数
currentTokens = math.min(availableTokens + tokensRemaining, bucketMaxTokens)
end
end
local tokensCount = currentTokens
if (currentTokens > 0) then
currentTokens = currentTokens - 1
redis.call('HSET', key, 'tokensRemaining', currentTokens)
end
return tokensCount
`
// 漏桶限流脚本
luaScriptMap["LeakyBucketScript"] = `
--[[
Description: 主要逻辑是判断当前请求是否可以被放入桶中,如果可以放入则可以执行本次请求,否则拒绝本次请求 - 基于 Redis Hash 实现
漏桶算法的实现主要分为三个步骤:
1. 未满加水:通过代码 water += 1 进行不停加水的动作。
2. 漏桶漏水:通过时间差来计算漏水量
3. 剩余水量:总水量-漏水量
注解:
1. 当请求速率小于漏水速率时,几乎所有请求都会通过
2. 当请求速率大于漏水速率时,超出桶容量的请求会被拒绝
3. 即使在突发流量下,也能保证处理速率不超过漏水速率
4. 桶容量决定了系统能处理的突发流量大小
场景:
1. API 调用限制
2. 消息处理队列
3. 数据库写入限制
1. key - [V] 漏桶 Key
2. capacity - [V] 桶的容量
4. leakRate - [V] 漏水速率, 单位是每秒漏多少个请求
4. curTime - [V] 当前时间, 单位s
--]]
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local leakRate = tonumber(ARGV[2])
local curTime = tonumber(ARGV[3])
-- 参数校验
if not capacity or not leakRate or not curTime then
return 0
end
-- 获取桶中当前水量和上次漏水时间
local mresult = redis.call('HMGET', key, 'currentWater', 'lastLeakTime')
local currentWater = mresult[1]
local lastLeakTime = mresult[2]
-- 获取桶中水量
currentWater = tonumber(currentWater) or 0
-- 获取上次漏水时间
lastLeakTime = tonumber(lastLeakTime) or curTime
-- 计算距离上次漏水经过的时间
local elapsedTime = curTime - tonumber(lastLeakTime)
-- 漏水操作,更新桶中水量 (时间间隔 * 漏水速率 = 漏水水量)
local leakedWater = math.floor(elapsedTime * leakRate)
-- 计算桶中剩余水量, 并保证不小于0 (桶中水量 - 漏水水量 = 桶中剩余水量)
local newWater = math.max(0, tonumber(currentWater) - leakedWater)
-- 更新桶中水量和上次漏水时间
redis.call('HMSET', key, 'currentWater', newWater, 'lastLeakTime', curTime)
-- 定义返回结果 0 表示不允许, 1 表示允许
local result = 0
-- 判断是否允许请求通过
if newWater < capacity then
-- 这里是将当前返回的水量加1, 代表桶中水量增加了一个请求的量
local re = redis.call('HINCRBY', key, 'currentWater', 1)
result = 1
end
return result
`
// 将脚本注释去除,并折叠为一行
luaScriptOptMap = make(map[string]string, len(luaScriptMap))
for k, v := range luaScriptMap {
luaScriptOptMap[k] = compressCode(v)
}
}
func removeComments(code string) string {
var output strings.Builder
inMultilineComment := false
scanner := bufio.NewScanner(strings.NewReader(code))
for scanner.Scan() {
line := scanner.Text()
if inMultilineComment {
if strings.HasSuffix(line, "]]") {
inMultilineComment = false
line = strings.TrimSuffix(line, "]]")
}
continue
}
parts := strings.Split(line, "--[[")
if len(parts) > 1 {
output.WriteString(parts[0])
output.WriteString(strings.TrimSuffix(parts[1], "]]"))
inMultilineComment = true
continue
}
parts = strings.SplitN(line, "--", 2)
if len(parts) > 1 {
output.WriteString(parts[0])
continue
}
output.WriteString(line)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading input:", err)
}
return output.String()
}
// compressCode compresss the Lua code into a single line
func compressCode(code string) string {
code = removeComments(code)
for _, item := range []string{"\n", "\t", " ", " ", " ", " "} {
code = strings.ReplaceAll(code, item, " ")
}
return strings.TrimSpace(code)
}
// getLuaScript 根据限流类型获取对应的 Lua 脚本
func getLuaScript(limitType LimiterType, flag bool) string {
var result string
luaScript := luaScriptMap
if flag {
luaScript = luaScriptOptMap
}
switch limitType {
case FixedWindowType:
result = luaScript["FixedWindowScript"]
case SlideWindowType:
result = luaScript["SlideWindowScript"]
case TokenBucketType:
result = luaScript["TokenBucketScript"]
case LeakyBucketType:
result = luaScript["LeakyBucketScript"]
}
return result
}