利用Redis进行分布式限流,实现令牌桶算法

10 篇文章 1 订阅
7 篇文章 0 订阅

分布式限流方案

计数:简单,双倍临界情况
漏桶:恒定速度,不能应对峰值
令牌桶:允许一定突然,丢掉部分请求有待商榷,令牌桶普遍用得多一些
成熟方案可见,阿里Sentinel:https://sentinelguard.io/zh-cn/docs/basic-implementation.html

计数实现

原理:没超出显示进行自增

local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]

local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
    if redis.call("INCR", key) > limit then
        return 0
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, expire_time)
    return 1
end

令牌桶方案实现

方案一、在提供给业务方的Controller层进行控制。
1、使用guava提供工具库里的RateLimiter类(内部采用令牌捅算法实现)或者信号量机制进行限流(基本只能用于单机)
2、使用Java自带delayqueue的延迟队列实现(编码过程相对麻烦,此处省略代码)
3、使用Redis实现,存储两个key,一个用于计时,一个用于计数。请求每调用一次,计数器增加1,若在计时器时间内计数器未超过阈值,则可以处理任务
方案二、在短信发送至服务商时做限流处理
方案三、同时使用方案一和方案二

令牌桶原理

用map保存最大值,当前值和最后修改时间,利用每次查询时,先加token,在减去需要的token数,满则暂停放入,能获取就减去获取值,够则直接返回

流程

具体实现放lua,用java类构造key和相关参数,工具类识别集群还是非集群进行相关调用

脚本实现如下
redis/ratelimit.lua:

if KEYS[1] == nil then
    return -1
end

if(redis.pcall("EXISTS",KEYS[1])==0) 
then
--第一次访问,初始化
redis.pcall("HMSET",KEYS[1],
        "last_mill_second",ARGV[1],
        "curr_permits",ARGV[4],
        "max_burst",ARGV[3],
        "rate",ARGV[4],
        "app",ARGV[5])
end 
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])


local local_curr_permits=max_burst;

if(type(last_mill_second) ~='boolean' and last_mill_second ~=nil) then
	--计算可以加入的最大令牌
    local reverse_permits=math.floor((ARGV[1]-last_mill_second)/1000)*rate
    if(reverse_permits>0) then
		--如果可以加入,则把当前时间作为最后加入令牌时间
        redis.pcall("HMSET",KEYS[1],"last_mill_second",ARGV[1])
    end
	--计算加入令牌后最大值
	--防止节点转发出现时间早的后出现!!!
	reverse_permits=math.max(reverse_permits,0);
    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[1])
end

local result=-1
if(local_curr_permits-ARGV[2]>0) then
    result=1
	--如果可以获取令牌,则去掉此时需要拿走的令牌
    redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits-ARGV[2])
else
	--如果不行,则把当前最新的令牌数写入内存
    redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits)
end

return result
 boolean getToken=redisUtil.limit(Common.SYSTEM_CODE,"1","20","2",Common.SYSTEM_CODE);

工具类:

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Qualifier("ratelimitLua")
    @Resource
    RedisScript<Long> ratelimitLua;
    @Qualifier("ratelimitInitLua")
    @Resource
    RedisScript<Long> ratelimitInitLua;
/**
     *
     * @param key
     * @param argus
     * last_mill_second 最后时间毫秒
     * curr_permits 当前可用的令牌
     * max_burst 令牌桶最大值
     * rate 每秒生成几个令牌
     * app 应用
     * @return
     */
    public boolean limit(String key,String... argus ) {
        if (argus==null||argus.length<4){
            logger.error("参数不合法:{}",argus);
            return  false;
        }
        //统一时间
        Long currMillSecond = redisTemplate.execute(
                (RedisCallback<Long>) redisConnection -> redisConnection.time()
        );
//        Long result = redisTemplate.execute(ratelimitLua,
//                Collections.singletonList(getKey(key)), currMillSecond.toString(), argus[0],argus[1],argus[2],argus[3]);
        List<String> argusList=new ArrayList<>();
        argusList.add(currMillSecond.toString());
        argusList.add(argus[0]);
        argusList.add(argus[1]);
        argusList.add(argus[2]);
        argusList.add(argus[3]);
        logger.info("list:"+argusList.toString());
        Long result =  redisTemplate.execute((RedisConnection connection)-> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(ratelimitLua.getScriptAsString(),  Collections.singletonList(getKey(key)) ,  argusList);
                }

                // 单点
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(ratelimitLua.getScriptAsString(),  Collections.singletonList(getKey(key)) , argusList);
                }
                return null;
        });
        logger.info("result:--{}",result);
        if (result == 0) {
            return  limit(key, argus);
        }
        if (result == 1) {
             return  true;
        }
        return false;
    }

bean注入:

@Configuration
public class RedisConfig {
    @Bean("ratelimitLua")
    public DefaultRedisScript getRedisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("redis/ratelimit.lua"));
        redisScript.setResultType(java.lang.Long.class);
        return redisScript;
    }
    @Bean("ratelimitInitLua")
    public DefaultRedisScript getInitRedisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("redis/ratelimitInit.lua"));
        redisScript.setResultType(java.lang.Long.class);
        return redisScript;
    }
}

lua优势

使用Lua脚本的好处
1、减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。

2、原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。

3、代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。

4、速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值