Redis限流实现

 

 

令牌桶算法(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

 

转载于:https://www.cnblogs.com/zyy1688/p/10985593.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值