精华内容
下载资源
问答
  • 基于令牌桶算法的Java限流实现。 项目需要使用限流措施,查阅后主要使用令牌桶算法实现,为了更灵活的实现限流,就自己实现了一个简单的基于令牌桶算法的限流实现。
  • bucket4j, 基于令牌桶算法Java速率限制库 Bucket4j - 基于令牌桶算法Java速率限制库。 的优点在已经知算法的基础上实现,它是in行业的速率限制的实际标准。有效的锁自由实现,Bucket4j可以用于多线程处理。绝对...
  • 【限流算法】java实现令牌桶算法

    千次阅读 2019-11-18 19:45:16
    本文实现了一种基本的令牌桶算法 令牌桶算法思想:以固定速率产生令牌,放入令牌桶,每次用户请求都得申请令牌,令牌不足则拒绝请求或等待。 代码逻辑:线程池每0.5s发送随机数量的请求,每次请求计算当前的令牌...

    本文实现了一种基本的令牌桶算法

    令牌桶算法思想:以固定速率产生令牌,放入令牌桶,每次用户请求都得申请令牌,令牌不足则拒绝请求或等待。

    代码逻辑:线程池每0.5s发送随机数量的请求,每次请求计算当前的令牌数量,请求令牌数量超出当前令牌数量,则产生限流。

    @Slf4j
    public class TokensLimiter {
    
        private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 最后一次令牌发放时间
        public long timeStamp = System.currentTimeMillis();
        // 桶的容量
        public int capacity = 10;
        // 令牌生成速度10/s
        public int rate = 10;
        // 当前令牌数量
        public int tokens;
    
        public void acquire() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                long now = System.currentTimeMillis();
                // 当前令牌数
                tokens = Math.min(capacity, (int) (tokens + (now - timeStamp) * rate / 1000));
                //每隔0.5秒发送随机数量的请求
                int permits = (int) (Math.random() * 9) + 1;
                log.info("请求令牌数:" + permits + ",当前令牌数:" + tokens);
                timeStamp = now;
                if (tokens < permits) {
                    // 若不到令牌,则拒绝
                    log.info("限流了");
                } else {
                    // 还有令牌,领取令牌
                    tokens -= permits;
                    log.info("剩余令牌=" + tokens);
                }
            }, 1000, 500, TimeUnit.MILLISECONDS);
        }
    
        public static void main(String[] args) {
            TokensLimiter tokensLimiter = new TokensLimiter();
            tokensLimiter.acquire();
        }
    
    }

    输出结果:

    16:13:20.042 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:1,当前令牌数:10
    16:13:20.045 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=9
    16:13:20.549 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:7,当前令牌数:10
    16:13:20.549 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=3
    16:13:21.054 [pool-1-thread-2] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:5,当前令牌数:8
    16:13:21.054 [pool-1-thread-2] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=3
    16:13:21.559 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:1,当前令牌数:8
    16:13:21.559 [pool-1-thread-1] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=7
    16:13:22.063 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:7,当前令牌数:10
    16:13:22.063 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=3
    16:13:22.568 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:7,当前令牌数:8
    16:13:22.568 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 剩余令牌=1
    16:13:23.072 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 请求令牌数:7,当前令牌数:6
    16:13:23.072 [pool-1-thread-3] INFO com.example.demo.limit.TokensLimiter - 限流了

    展开全文
  • 基于令牌桶算法实现的SpringBoot无锁限流插件,支持方法级别、系统级别的限流,提供快速失败与CAS阻塞两种方案,开箱即用!
  • 本demo适用于分布式环境的基于RateLimiter令牌桶算法的限速控制与基于计数器算法的限量控制,可应用于中小型项目中有相关需求的场景(注:本实现未做压力测试,如果用户并发量较大需验证效果)。
  • 二、Redis+lua脚本 + 令牌桶算法 实现限流控制 1、自定义一个注解,用来给限流的方法标注 2、编写lua脚本 3、读取lua脚本 4、创建拦截器拦截带有该注解的方法 5、在WebConfig中注册这个这个拦截器 6、注解使用...

    目录

    一、漏桶和令牌桶介绍

    二、Redis+ lua脚本 + 令牌桶算法 实现限流控制

    1、自定义一个注解,用来给限流的方法标注

    2、编写lua脚本

    3、读取lua脚本

    4、创建拦截器拦截带有该注解的方法

    5、在WebConfig中注册这个这个拦截器

    6、注解使用


    一、漏桶和令牌桶介绍

    漏桶算法令牌桶算法在表面看起来类似,很容易将两者混淆。但事实上,这两者具有截然不同的特性,且为不同的目的而使用。漏桶算法令牌桶算法的区别在于:l 漏桶算法能够强行限制数据的传输速率。l 令牌桶算法能够在限制数据的平均传输速率的同

    时还允许某种程度的突发传输。需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的

    流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

    常用的限流算法有两种:漏桶算法和令牌桶算法。

          漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

    漏桶算法示意图

    对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处

    理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

    令牌桶算法示意图

    并不能说明令牌桶一定比漏洞好,她们使用场景不一样。令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可

    以超过配置的限制。而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。

    这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

    总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。

    二、Redislua脚本 + 令牌桶算法 实现限流控制

    1、自定义一个注解,用来给限流的方法标注

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimit {
        //限流唯一标示
        String key() default "";
     
        //限流单位时间(单位为s)
        int time() default 1;
     
        //单位时间内限制的访问次数
        int count();
     
        //是否限制ip
        boolean ipLimit() default false;
    }

    2、编写lua脚本

    根据key(参数) 查询 对应的 value(令牌数)
    	如果为null 说明该key 是第一次进入 
    	{
    		初始化 令牌桶(参数)数量;记录初始化时间 ->返回 剩余令牌数
    	} 
    	
    	如果不为null
    	{
    		判断 value 是否大于1 
    		{
    			大于1  ->value - 1  -> 返回 剩余令牌数
    			小于1  -> 判断  补充令牌时间间隔是否足够
    			{
    				足够 -> 补充令牌;更新补充令牌时间-> 返回 剩余令牌数
    				不足够	-> 返回 -1 (说明超过限流访问次数)
    			}
    		}
    	}
    	
    redis.replicate_commands();
    -- 参数中传递的key
    local key = KEYS[1]
    -- 令牌桶填充 最小时间间隔
    local update_len = tonumber(ARGV[1])
    -- 记录 当前key上次更新令牌桶的时间的 key
    local key_time = 'ratetokenprefix'..key
    -- 获取当前时间(这里的curr_time_arr 中第一个是 秒数,第二个是 秒数后毫秒数),由于我是按秒计算的,这里只要curr_time_arr[1](注意:redis数组下标是从1开始的)
    --如果需要获得毫秒数 则为 tonumber(arr[1]*1000 + arr[2])
    local curr_time_arr = redis.call('TIME')
    -- 当前时间秒数
    local nowTime = tonumber(curr_time_arr[1])
    -- 从redis中获取当前key 对应的上次更新令牌桶的key 对应的value
    local curr_key_time = tonumber(redis.call('get',KEYS[1]) or 0)
    -- 获取当前key对应令牌桶中的令牌数
    local token_count = tonumber(redis.call('get',KEYS[1]) or -1)
    -- 当前令牌桶的容量
    local token_size = tonumber(ARGV[2])
    -- 令牌桶数量小于0 说明令牌桶没有初始化
    if token_count < 0 then
    	redis.call('set',key_time,nowTime)
    	redis.call('set',key,token_size -1)
    	return token_size -1
    else
    	if token_count > 0 then --当前令牌桶中令牌数够用
    		redis.call('set',key,token_count - 1)
    		return token_count -1   --返回剩余令牌数
    	else    --当前令牌桶中令牌数已清空
    		if curr_key_time + update_len < nowTime then    --判断一下,当前时间秒数 与上次更新时间秒数  的间隔,是否大于规定时间间隔数 (update_len)
    			redis.call('set',key,token_size -1)
    			return token_size - 1
    		else
    			return -1
    		end
    	end
    end

    3、读取lua脚本

    @Component
    public class CommonConfig {
        /**
         * 读取限流脚本
         */
        @Bean
        public DefaultRedisScript<Number> redisluaScript() {
            DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
            //这里脚本的路径为path for source root 路径
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLua.lua"))); 
            redisScript.setResultType(Number.class);
            return redisScript;
        }
        /**
         * RedisTemplate
         */
        @Bean
        public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }

    4、创建拦截器拦截带有该注解的方法

    @Component
    public class RateLimitInterceptor implements HandlerInterceptor {
        private final Logger LOG = LoggerFactory.getLogger(this.getClass());
        
        @Autowired
        private RedisTemplate<String, Serializable> limitRedisTemplate;
     
        @Autowired
        private DefaultRedisScript<Number> redisLuaScript;
        
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            assert handler instanceof HandlerMethod;
            HandlerMethod method = (HandlerMethod) handler;
            RateLimit rateLimit = method.getMethodAnnotation(RateLimit.class);
            //当前方法上有我们自定义的注解
            if (rateLimit != null) {
                //获得单位时间内限制的访问次数
                int count = rateLimit.count();
                String key = rateLimit.key();
                //获得限流单位时间(单位为s)
                int time = rateLimit.time();
                boolean ipLimit = rateLimit.ipLimit();
                //拼接 redis中的key
                StringBuilder sb = new StringBuilder();
                sb.append(Constants.RATE_LIMIT_KEY).append(key).append(":");
                //如果需要限制ip的话
                if(ipLimit){
                    sb.append(getIpAddress(request)).append(":");
                }
                List<String> keys = Collections.singletonList(sb.toString());
               //执行lua脚本
                Number execute = limitRedisTemplate.execute(redisLuaScript, keys, time, count);
                assert execute != null;
                if (-1 == execute.intValue()) {
                    ResultModel resultModel = ResultModel.error_900("接口调用超过限流次数");
                    response.setStatus(901);
                    response.setCharacterEncoding("utf-8");
                    response.setContentType("application/json");
                    response.getWriter().write(JSONObject.toJSONString(resultModel));
                    response.getWriter().flush();
                    response.getWriter().close();
                    LOG.info("当前接口调用超过时间段内限流,key:{}", sb.toString());
                    return false;
                } else {
                    LOG.info("当前访问时间段内剩余{}次访问次数", execute.toString());
                }
            }
            return true;
        }
     
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
     
        }
     
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
     
        }
        
        public static String getIpAddr(HttpServletRequest request) {
            String ipAddress = null;
            try {
                ipAddress = request.getHeader("x-forwarded-for");
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("WL-Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getRemoteAddr();
                }
                // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
                // "***.***.***.***".length()
                if (ipAddress != null && ipAddress.length() > 15) { 
                    // = 15
                    if (ipAddress.indexOf(",") > 0) {
                        ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                    }
                }
            } catch (Exception e) {
                ipAddress = "";
            }
            return ipAddress;
        }
     
    }

    一个自定义的常量用作redis前缀

    public class Constants {
        public static final String RATE_LIMIT_KEY = "rateLimit:";
    }

    5、在WebConfig中注册这个这个拦截器

    @Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {
     
        @Autowired
        private RateLimitInterceptor rateLimitInterceptor;
     
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(rateLimitInterceptor);
            super.addInterceptors(registry);
        }
    }

    6、注解使用

    @RestController
    @RequestMapping(value = "/test")
    public class TestController {
     
        //限流规则为 1秒内只允许同一个ip发送5次请求
        @RateLimit(key = "testGet",time = 1,count = 5,ipLimit = true)
        @RequestMapping(value = "/get")
        public ResultModel testGet(){
            return ResultModel.ok_200();
        }
     
    }

     

    展开全文
  • 令牌桶算法:min((当前时间-上次更新桶时间) * 速率 + 桶里剩余的令牌数量,桶大小),即min(((currentMills - last_mill_second) / 1000)*rate+current_permits,max_burst) 举例:在秒杀活动中

    注明:

    本文为整理记录笔记,不喜勿喷。有问题请留言。CSDN转载必须有原文链接,有些链接找不到了,原文看到了可以留言私我。

    描述:

    令牌桶:以一定的速率向一个固定大小的桶里扔令牌,然后每次去桶里取令牌,取的到说明有流量,反之没有

    令牌桶算法:min((当前时间-上次更新桶时间) * 速率 + 桶里剩余的令牌数量,桶大小),即min(((currentMills - last_mill_second) / 1000)*rate+current_permits,max_burst)

    举例:在秒杀活动中,用户的请求速率是不固定的,这里我们假定为10r/s,令牌按照5个每秒的速率(rate)放入令牌桶,桶中最多存放20个令牌(maxBurst),那系统就只会允许持续的每秒处理5个请求,或者每隔4秒,等桶中20 个令牌攒满后,一次处理20个请求的突发情况,保证系统稳定性。

    算法详细介绍参考文章:https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin

    分布式限流一次操作步骤:读数据-判断结果-写数据(更新桶)

    分布式限流要解决问题:保证操作的原子性(即上述步骤不可分割)

    lua好处:Redis使用同一个Lua解释器去运行所有脚本,并且,Redis也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或Redis命令被执行.这和使用 MULTI/EXEC 包围的事务很类似在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。参考文章https://redis.io/commands/eval

    lua相关知识

    语法参考文章:https://www.runoob.com/lua/lua-tutorial.html

    redis里的命令参考文章:https://www.runoob.com/redis/redis-scripting.html

    redis里调试lua脚本:类似于前端调试时的console.log进行打印日志调试

    redis里调试lua脚本步骤:

    1.查看redis的日志级别

        

    2.脚本添加代码(一定要保证二者日志级别一致才能打印成功)

    redis.log(redis.LOG_NOTICE, "Hi " .. "holidaylee")

    3.查看效果

    代码实现

    结构:

    初始化令牌桶lua脚本rateLimitInit.lua

    redis.replicate_commands()
    local result = -1
    local rate_limit_info = redis.pcall("HMGET", KEYS[1], "last_mill_second", "current_permits", "max_burst", "rate", "key")
    local last_mill_second = rate_limit_info[1]
    local current_permits = tonumber(rate_limit_info[2])
    local redisTime = redis.pcall('TIME')
    local currentMills = last_mill_second
    if last_mill_second == nil then
        currentMills = ARGV[1]
    end
    if (redisTime ~= nil) then
        currentMills = tonumber((redisTime[1] * 1000000 + redisTime[2])) / 1000
    end
    if current_permits ~= nil and last_mill_second ~= nil and current_permits < tonumber(ARGV[2]) then
        redis.call("HMSET", KEYS[1], "last_mill_second", last_mill_second, "current_permits", current_permits, "max_burst", ARGV[3], "rate", ARGV[4], "key", ARGV[5])
        result = 1
    else
        redis.call("HMSET", KEYS[1], "last_mill_second", currentMills, "current_permits", ARGV[2], "max_burst", ARGV[3], "rate", ARGV[4], "key", ARGV[5])
        result = 1
    end
    -- redis.log(redis.LOG_NOTICE, "Hi " .. "holidaylee")
    return result
    

    执行限流的lua脚本rateLimitExecute.lua

    redis.replicate_commands()
    local result = -1
    local redisTime = redis.pcall('TIME')
    local rate_limit_info = redis.pcall("HMGET", KEYS[1], "last_mill_second", "current_permits", "max_burst", "rate", "key")
    local last_mill_second = rate_limit_info[1]
    local current_permits = tonumber(rate_limit_info[2])
    local max_burst = tonumber(rate_limit_info[3])
    local rate = tonumber(rate_limit_info[4])
    local key = tostring(rate_limit_info[5])
    local currentMills = last_mill_second
    if key == nil then
        return result
    end
    if (redisTime ~= nil) then
        currentMills = tonumber((redisTime[1] * 1000000 + redisTime[2])) / 1000
    end
    local local_current_permits = max_burst;
    if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= nil) then
        local reverse_permits = math.floor((currentMills - last_mill_second) / 1000) * rate
        if (reverse_permits > 0) then
            redis.pcall("HMSET", KEYS[1], "last_mill_second", currentMills)
        end
        local expect_current_permits = reverse_permits + current_permits
        local_current_permits = math.min(expect_current_permits, max_burst);
    else
        redis.pcall("HMSET", KEYS[1], "last_mill_second", currentMills)
    end
    if (local_current_permits - ARGV[1] >= 0) then
        result = 1
        redis.pcall("HMSET", KEYS[1], "current_permits", local_current_permits - ARGV[1])
    else
        redis.pcall("HMSET", KEYS[1], "current_permits", local_current_permits)
    end
    return result

    加载限流脚本配置类

    package com.holidaylee.demo.ratelimit.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.scripting.support.ResourceScriptSource;
    
    import java.io.IOException;
    
    /**
     * @author : HolidayLee
     * @description : 加载限流脚本
     */
    @Configuration
    public class RateLimitLuaConfig {
        public static final String RATE_LIMIT_KEY_PREFIX = "rateLimitBuckets:";
        public static final String RATE_LIMIT_LUA_FILE_DIRECTORY = "rateLimit/";
    
        @Bean("initLua")
        public DefaultRedisScript getInitLua() throws IOException {
            DefaultRedisScript luaScript = new DefaultRedisScript();
            luaScript.setScriptText(new ResourceScriptSource(new ClassPathResource(RATE_LIMIT_LUA_FILE_DIRECTORY + "rateLimitInit.lua")).getScriptAsString());
            luaScript.setResultType(Long.class);
            return luaScript;
        }
    
        @Bean("executeLua")
        public DefaultRedisScript getLua() throws IOException {
            DefaultRedisScript luaScript = new DefaultRedisScript();
            luaScript.setScriptText(new ResourceScriptSource(new ClassPathResource(RATE_LIMIT_LUA_FILE_DIRECTORY + "rateLimitExecute.lua")).getScriptAsString());
            luaScript.setResultType(Long.class);
            return luaScript;
        }
    }

    限流工具类

    package com.holidaylee.demo.ratelimit.util;
    
    import com.holidaylee.demo.ratelimit.config.RateLimitLuaConfig;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.dao.DataAccessException;
    import org.springframework.data.redis.connection.RedisConnection;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    /**
     * @author : HolidayLee
     * @description : 限流工具类
     */
    @Component
    @ConditionalOnBean(RateLimitLuaConfig.class)
    public class RateLimitUtils {
        private final Logger log = LoggerFactory.getLogger(RateLimitUtils.class);
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        @Resource(name = "initLua")
        private RedisScript<Long> initLua;
        @Resource(name = "executeLua")
        private RedisScript<Long> executeLua;
    
        @Value("${rateLimit.key}")
        private String key;
        @Value("${rateLimit.currentPermits}")
        private String currentPermits;
        @Value("${rateLimit.maxBurst}")
        private String maxBurst;
        @Value("${rateLimit.rate}")
        private String rate;
    
        @PostConstruct
        private void initRateLimitBuckets() {
            List<String> paramList = initBucketParams();
            this.key = paramList.get(paramList.size() - 1);
            Long result = stringRedisTemplate.execute(initLua, getKeyList(key), paramList.toArray());
            if (result != null && result == 1) {
                log.info("初始化令牌桶成功");
            } else {
                throw new RuntimeException("初始化令牌桶失败");
            }
            log.info("sha1值为:" + executeLua.getSha1());
            // 预加载优化:
            // 1.x的stringRedisTemplate在redis cluster的时候不支持evalSha,JedisClusterConnection.scriptLoad()函数直接抛异常InvalidDataAccessApiUsageException("EvalSha is not supported in cluster environment.")
            // 2.x的stringRedisTemplate采用lettuce架构,在redis cluster的时候支持scriptLoad
            //String luaSha1 = Objects.requireNonNull(stringRedisTemplate.getConnectionFactory()).getConnection().scriptLoad(executeLua.getScriptAsString().getBytes());
        }
    
        private List<String> initBucketParams() {
            Long currentMills = getCurrentRedisTimeMills();
            // 参数顺序(要与lua中的参数顺序一致):currentMills,current_permits, max_burst, rate, key
            List<String> paramList = new ArrayList<>();
            paramList.add(currentMills.toString());
            // current_permits
            paramList.add(currentPermits);
            // max_burst
            paramList.add(maxBurst);
            // rate
            paramList.add(rate);
            // key
            paramList.add(key);
            return paramList;
        }
    
        /**
         * 是否允许放行数量为requestCount的请求
         *
         * @param requestCount 请求数量
         * @param key          限流桶key(网关id)
         * @return true:不超过流量;false:超过流量
         */
        public boolean canReleaseRequest(Integer requestCount, String key) {
            boolean success = false;
            Long result = executeRateLimit(requestCount, key);
            if (result == 1) {
                success = true;
            }
            return success;
        }
    
        private Long executeLuaScript(RedisScript<Long> script, List<String> keys, String... params) {
            return stringRedisTemplate.execute(script, keys, params);
        }
    
        /**
         * 执行lua脚本
         *
         * @param requestCount 当前请求的数量
         * @return 执行结果:-1代表请求数量超出当前允许的请求数量;1代表没有超过
         */
        private Long executeRateLimit(Integer requestCount, String key) {
            // 执行lua脚本获取当前请求的数量是否超过允许的请求数量
            return executeLuaScript(executeLua, getKeyList(key), requestCount.toString());
        }
    
        /**
         * 获取redis中的bucket的key列表
         *
         * @return key列表
         */
        private List<String> getKeyList(String key) {
            return Collections.singletonList(RateLimitLuaConfig.RATE_LIMIT_KEY_PREFIX + key);
        }
    
        /**
         * 获取redis的当前时间
         *
         * @return redis的当前时间(ms)
         */
        private Long getCurrentRedisTimeMills() {
            Long currentMills = stringRedisTemplate.execute(new RedisCallback<Long>() {
                @Override
                public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    return redisConnection.time();
                }
            });
            return currentMills;
        }
    }
    

    关于上面说的预加载优化:预加载到redis中每次执行evalSha的话,每次传输的大小就是一个32位的sha1值,能大幅度提升效率,当然stringRedisTemplate每次执行lua脚本的时候默认先执行evalSha,失败的时候会执行eval并且会加载脚本到redis中,要注意cluster模式下不同版本的stringRedisTemplate是不一样的

    可以通过如下查看是否加载到了redis里

    配置项介绍:此处只是演示用,只有一个桶,可以根据业务需要把这块的配置项移步到数据库,并且可以给每个业务(接口)设置一个桶达到多维度的限流控制,并且可以通过修改redis的相关数值达到动态修改的目的

    rateLimit.key=holidaylee
    #当前每秒允许发送的请求数量,初始化时此数值应与rate的值相同,单位(个/秒)
    rateLimit.currentPermits=100
    #一秒允许发送的最大请求数量,单位(个),即某个时刻瞬时的峰值
    rateLimit.maxBurst=100
    #生成令牌的速度(单位为个/秒),不能填0,速度为0时,请求数达到maxBurst时后续的所有请求会被拒绝
    rateLimit.rate=100

    展开全文
  • 最完整清晰的redis+ lua脚本 + 令牌桶算法 实现限流控制 在网上看了好多博客,感觉不是很清楚,于是决定自己手撸一个。 一、自定义一个注解,用来给限流的方法标注 @Target({ElementType.TYPE, ElementType.METHOD})...

    最完整清晰的redis+ lua脚本 + 令牌桶算法 实现限流控制

    在网上看了好多博客,感觉不是很清楚,于是决定自己手撸一个。

    一、自定义一个注解,用来给限流的方法标注

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimit {
        //限流唯一标示
        String key() default "";
    
        //限流单位时间(单位为s)
        int time() default 1;
    
        //单位时间内限制的访问次数
        int count();
    
        //是否限制ip
        boolean ipLimit() default false;
    }
    

    二、编写lua脚本

    重要的地方注释得非常详细了,这里就不多解释;

    主要功能是:

    根据key(参数) 查询 对应的 value(令牌数)
    	如果为null 说明该key 是第一次进入 
    	{
    		初始化 令牌桶(参数)数量;记录初始化时间 ->返回 剩余令牌数
    	} 
    	
    	如果不为null
    	{
    		判断 value 是否大于1 
    		{
    			大于1  ->value - 1  -> 返回 剩余令牌数
    			小于1  -> 判断  补充令牌时间间隔是否足够
    			{
    				足够 -> 补充令牌;更新补充令牌时间-> 返回 剩余令牌数
    				不足够	-> 返回 -1 (说明超过限流访问次数)
    			}
    		}
    	}
    	
    
    redis.replicate_commands();
    -- 参数中传递的key
    local key = KEYS[1]
    -- 令牌桶填充 最小时间间隔
    local update_len = tonumber(ARGV[1])
    -- 记录 当前key上次更新令牌桶的时间的 key
    local key_time = 'ratetokenprefix'..key
    -- 获取当前时间(这里的curr_time_arr 中第一个是 秒数,第二个是 秒数后毫秒数),由于我是按秒计算的,这里只要curr_time_arr[1](注意:redis数组下标是从1开始的)
    --如果需要获得毫秒数 则为 tonumber(arr[1]*1000 + arr[2])
    local curr_time_arr = redis.call('TIME')
    -- 当前时间秒数
    local nowTime = tonumber(curr_time_arr[1])
    -- 从redis中获取当前key 对应的上次更新令牌桶的key 对应的value
    local curr_key_time = tonumber(redis.call('get',KEYS[1]) or 0)
    -- 获取当前key对应令牌桶中的令牌数
    local token_count = tonumber(redis.call('get',KEYS[1]) or -1)
    -- 当前令牌桶的容量
    local token_size = tonumber(ARGV[2])
    -- 令牌桶数量小于0 说明令牌桶没有初始化
    if token_count < 0 then
    	redis.call('set',key_time,nowTime)
    	redis.call('set',key,token_size -1)
    	return token_size -1
    else
    	if token_count > 0 then --当前令牌桶中令牌数够用
    		redis.call('set',key,token_count - 1)
    		return token_count -1   --返回剩余令牌数
    	else    --当前令牌桶中令牌数已清空
    		if curr_key_time + update_len < nowTime then    --判断一下,当前时间秒数 与上次更新时间秒数  的间隔,是否大于规定时间间隔数 (update_len)
    			redis.call('set',key,token_size -1)
    			return token_size - 1
    		else
    			return -1
    		end
    	end
    end
    

    三、读取lua脚本

    @Component
    public class CommonConfig {
        /**
         * 读取限流脚本
         */
        @Bean
        public DefaultRedisScript<Number> redisluaScript() {
            DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
            //这里脚本的路径为path for source root 路径
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLua.lua"))); 
            redisScript.setResultType(Number.class);
            return redisScript;
        }
        /**
         * RedisTemplate
         */
        @Bean
        public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }
    

    四、创建拦截器拦截带有该注解的方法

    @Component
    public class RateLimitInterceptor implements HandlerInterceptor {
        private final Logger LOG = LoggerFactory.getLogger(this.getClass());
        
        @Autowired
        private RedisTemplate<String, Serializable> limitRedisTemplate;
    
        @Autowired
        private DefaultRedisScript<Number> redisLuaScript;
        
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            assert handler instanceof HandlerMethod;
            HandlerMethod method = (HandlerMethod) handler;
            RateLimit rateLimit = method.getMethodAnnotation(RateLimit.class);
            //当前方法上有我们自定义的注解
            if (rateLimit != null) {
                //获得单位时间内限制的访问次数
                int count = rateLimit.count();
                String key = rateLimit.key();
                //获得限流单位时间(单位为s)
                int time = rateLimit.time();
                boolean ipLimit = rateLimit.ipLimit();
                //拼接 redis中的key
                StringBuilder sb = new StringBuilder();
                sb.append(Constants.RATE_LIMIT_KEY).append(key).append(":");
                //如果需要限制ip的话
                if(ipLimit){
                    sb.append(getIpAddress(request)).append(":");
                }
                List<String> keys = Collections.singletonList(sb.toString());
               //执行lua脚本
                Number execute = limitRedisTemplate.execute(redisLuaScript, keys, time, count);
                assert execute != null;
                if (-1 == execute.intValue()) {
                    ResultModel resultModel = ResultModel.error_900("接口调用超过限流次数");
                    response.setStatus(901);
                    response.setCharacterEncoding("utf-8");
                    response.setContentType("application/json");
                    response.getWriter().write(JSONObject.toJSONString(resultModel));
                    response.getWriter().flush();
                    response.getWriter().close();
                    LOG.info("当前接口调用超过时间段内限流,key:{}", sb.toString());
                    return false;
                } else {
                    LOG.info("当前访问时间段内剩余{}次访问次数", execute.toString());
                }
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
        
        public static String getIpAddr(HttpServletRequest request) {
            String ipAddress = null;
            try {
                ipAddress = request.getHeader("x-forwarded-for");
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("WL-Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getRemoteAddr();
                }
                // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
                // "***.***.***.***".length()
                if (ipAddress != null && ipAddress.length() > 15) { 
                    // = 15
                    if (ipAddress.indexOf(",") > 0) {
                        ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                    }
                }
            } catch (Exception e) {
                ipAddress = "";
            }
            return ipAddress;
        }
    
    }
    
    一个自定义的常量

    用作redis前缀

    public class Constants {
        public static final String RATE_LIMIT_KEY = "rateLimit:";
    }
    

    五、在WebConfig中注册这个这个拦截器

    @Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {
    
        @Autowired
        private RateLimitInterceptor rateLimitInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(rateLimitInterceptor);
            super.addInterceptors(registry);
        }
    }
    

    六、注解使用

    @RestController
    @RequestMapping(value = "/test")
    public class TestController {
    
        //限流规则为 1秒内只允许同一个ip发送5次请求
        @RateLimit(key = "testGet",time = 1,count = 5,ipLimit = true)
        @RequestMapping(value = "/get")
        public ResultModel testGet(){
            return ResultModel.ok_200();
        }
    
    }
    

    如果觉得有问题,欢迎各位大佬指正
    觉得可以的话点个赞再走吧!!!!!!

    展开全文
  • 令牌桶Java实现

    2017-05-05 13:19:58
    令牌桶 Java 源码 不限制桶大小
  • 令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,...
  • 下面代码是仅仅做了登录接口防刷,使用了redis作为存储,将以登录名...在登录controller下做接口限制 今天看了关于限流知识,回过头发现此方法使用的是令牌桶算法,桶中只有限的令牌可以使用,用户用完了就不可使用了。
  • 漏桶算法与令牌桶算法漏桶算法令牌桶算法区别RateLimiter信号量 转载:本文转载自 linkedkeeper.com (文/张松然) 漏桶算法 漏桶算法:水(请求)先进入到漏桶里,漏桶以一定速度出水,当水流(请求)过大时会直接...
  • boolean true boolean true boolean true boolean true boolean true boolean false boolean false boolean false int 5 boolean true boolean true boolean true boolean true boolean true boolean false
  • 令牌桶算法

    2019-08-17 20:21:17
    令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。 令牌桶算法的基本过程如下: ...
  • Java漏桶算法和令牌桶算法

    千次阅读 2018-09-06 18:17:09
    https://blog.csdn.net/charleslei/article/details/53152883
  • 基于令牌桶算法实现的分布式无锁限流框架,支持动态配置规则,支持可视化监控,开箱即用。 Document 使用文档:| 功能概要 限流 降级 授权 注解 监控 黑名单 白名单 控制台 分布式 高可用 设计模式 单例模式 观察者...
  • 令牌桶算法限流

    万次阅读 2016-04-23 21:36:15
    今天观看QCon大会讲述了阿里线上管控体系,其中主要使用了令牌桶算法实现限流的目的。表示非常好奇,故此学习一下什么是令牌桶算法。 1. 简介 令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络...
  • (3)Redis队列 (4)SpringBoot RedisTemplate操作redis (5)jmeter测试工具 使用令牌桶算法实现接口限流 令牌桶算法(Token Bucket):随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里...
  • @Test void testLimitMore() { RateLimiter limiter = RateLimiter.create(5); for (int i = 0; i < 5; i++) { System.out.println(limiter.acquire(5)); System.out.println(limiter.acquire());...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,649
精华内容 1,859
关键字:

令牌桶算法实现java

java 订阅