令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。
令牌桶内令牌生成借鉴Guava-RateLimiter类的设计 ,每次getToken根据时间戳生成token,不超过最大值。
核心参数:
last_mill_second 最后时间毫秒
curr_permits 当前可用的令牌
max_burst 令牌桶最大值
rate 每秒生成几个令牌
app 应用名称
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
|
local ratelimit_info=redis.pcall("HMGET",KEYS[1],"last_mill_second","curr_permits","max_burst","rate","app") local last_mill_second=ratelimit_info[1] local curr_permits=tonumber(ratelimit_info[2]) local max_burst=tonumber(ratelimit_info[3]) local rate=tonumber(ratelimit_info[4]) local app=tostring(ratelimit_info[5]) if app == nil then return 0 end local local_curr_permits=max_burst; if(type(last_mill_second) ~='boolean' and last_mill_second ~=nil) then local reverse_permits=math.floor((ARGV[2]-last_mill_second)/1000)*rate if(reverse_permits>0) then redis.pcall("HMSET",KEYS[1],"last_mill_second",ARGV[2]) end local expect_curr_permits=reverse_permits+curr_permits local_curr_permits=math.min(expect_curr_permits,max_burst); else redis.pcall("HMSET",KEYS[1],"last_mill_second",ARGV[2]) end local result=-1 if(local_curr_permits-ARGV[1]>0) then result=1 redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits-ARGV[1]) else redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits) end return result |
Lua脚本在Redis中运行,保证了取令牌和生成令牌两个操作的原子性
springboot配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# REDIS (RedisProperties) # Redis数据库索引(默认为 0 ) spring.redis.database= 0 # Redis服务器地址 spring.redis.host= 127.0 . 0.1 # Redis服务器连接端口 spring.redis.port= 6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active= 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=- 1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle= 8 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle= 0 # 连接超时时间(毫秒) spring.redis.timeout= 2000 |
java 配置类:
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
|
@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Override @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { StringRedisTemplate template = new StringRedisTemplate(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object. class ); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } @Bean ( "ratelimitLua" ) public DefaultRedisScript getRedisScript() { DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setLocation( new ClassPathResource( "ratelimit.lua" )); redisScript.setResultType(java.lang.Long. class ); return redisScript; } @Bean ( "ratelimitInitLua" ) public DefaultRedisScript getInitRedisScript() { DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setLocation( new ClassPathResource( "ratelimitInit.lua" )); redisScript.setResultType(java.lang.Long. class ); return redisScript; } } public class Constants { public static final String RATE_LIMIT_KEY = "ratelimit:" ; } public enum Token { SUCCESS, FAILED; public boolean isSuccess() { return this .equals(SUCCESS); } public boolean isFailed() { return this .equals(FAILED); } } |
限流客户端:
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
|
@Service public class RateLimitClient { @Autowired StringRedisTemplate stringRedisTemplate; @Qualifier ( "getRedisScript" ) @Resource RedisScript<Long> ratelimitLua; @Qualifier ( "getInitRedisScript" ) @Resource RedisScript<Long> ratelimitInitLua; public Token initToken(String key) { Token token = Token.SUCCESS; Long currMillSecond = stringRedisTemplate .execute((RedisCallback<Long>) redisConnection -> redisConnection.time()); /** * redis.pcall("HMSET",KEYS[1], "last_mill_second",ARGV[1], * "curr_permits",ARGV[2], "max_burst",ARGV[3], "rate",ARGV[4], "app",ARGV[5]) */ Long accquire = stringRedisTemplate.execute(ratelimitInitLua, Collections.singletonList(getKey(key)), currMillSecond.toString(), "1" , "10" , "10" , "skynet" ); if (accquire == 1 ) { token = Token.SUCCESS; } else if (accquire == 0 ) { token = Token.SUCCESS; } else { token = Token.FAILED; } return token; } /** * 获得key操作 * * @param key * @return */ public Token accquireToken(String key) { return accquireToken(key, 1 ); } public Token accquireToken(String key, Integer permits) { Token token = Token.SUCCESS; Long currMillSecond = stringRedisTemplate .execute((RedisCallback<Long>) redisConnection -> redisConnection.time()); Long accquire = stringRedisTemplate.execute(ratelimitLua, Collections.singletonList(getKey(key)), permits.toString(), currMillSecond.toString()); if (accquire == 1 ) { token = Token.SUCCESS; } else { token = Token.FAILED; } return token; } public String getKey(String key) { return Constants.RATE_LIMIT_KEY + key; } } |
lua脚本:
1
2
3
|
local result= 1 redis.pcall( "HMSET" ,KEYS[ 1 ], "last_mill_second" ,ARGV[ 1 ], "curr_permits" ,ARGV[ 2 ], "max_burst" ,ARGV[ 3 ], "rate" ,ARGV[ 4 ], "app" ,ARGV[ 5 ]) return result |