赞
踩
网上找了很多redis分布式限流方案,要不就是太大,需要引入第三方jar,而且还无法正常运行,要不就是定时任务定时往key中放入数据,使用的时候调用,严重影响性能,所以着手自定义实现redis令牌桶。
只用到了spring-boot-starter-data-redis包,并且就几行代码。
a、idea新建springboot项目,引入spring-data-redis包
b、编写令牌桶实现方法RedisLimitExcutor
c、测试功能,创建全局拦截器,测试功能
maven添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
令牌桶实现方法RedisLimitExcutor
package com.example.redis_limit_demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * 令牌桶实现 */ @Component public class RedisLimitExcutor { private StringRedisTemplate stringRedisTemplate; @Autowired public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 令牌的 * * @param key key值 * @param limitCount 容量 * @param seconds 时间间隔 * @return */ public boolean tryAccess(String key, int limitCount, int seconds) { String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); List<String> keys = new ArrayList<>(); keys.add(key); Long count = stringRedisTemplate.execute(redisScript, keys, String.valueOf(limitCount), String.valueOf(seconds)); if (count != 0) { return true; } else { return false; } } /** * 脚本 * * @return */ private static final String buildLuaScript() { StringBuilder lua = new StringBuilder(); lua.append(" local key = KEYS[1]"); lua.append("\nlocal limit = tonumber(ARGV[1])"); lua.append("\nlocal curentLimit = tonumber(redis.call('get', key) or \"0\")"); lua.append("\nif curentLimit + 1 > limit then"); lua.append("\nreturn 0"); lua.append("\nelse"); lua.append("\n redis.call(\"INCRBY\", key, 1)"); lua.append("\nredis.call(\"EXPIRE\", key, ARGV[2])"); lua.append("\nreturn curentLimit + 1"); lua.append("\nend"); return lua.toString(); } }
拦截器配置WebAppConfig
package com.example.redis_limit_demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 拦截器配置 */ @Configuration public class WebAppConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getRequestInterceptor()).addPathPatterns("/**"); } @Bean public RequestInterceptor getRequestInterceptor() { return new RequestInterceptor(); } }
拦截器实现RequestInterceptor
package com.example.redis_limit_demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.InetAddress; import java.net.UnknownHostException; /** * 拦截器实现 */ @Configuration public class RequestInterceptor implements HandlerInterceptor { @Autowired private RedisLimitExcutor redisLimitExcutor; /** * 只有返回true才会继续向下执行,返回false取消当前请求 * * @param request * @param response * @param handler * @return */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { /** * 根据实际情况设置QPS */ String url = request.getRequestURI(); String ip = getIpAdd(request); //QPS设置为5,手动刷新接口可以测试出来 if (!redisLimitExcutor.tryAccess(ip+url, 5, 1)) { throw new RuntimeException("调用频繁"); } else { return true; } } public static final String getIpAdd(HttpServletRequest request) { String 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(); if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { return null; } ipAddress = inet.getHostAddress(); } } // 如果通过代理访问,可能获取2个IP,这时候去第二个(代理服务端IP) if (ipAddress.split(",").length > 1) { ipAddress = ipAddress.split(",")[1].trim(); } return ipAddress; } }
测试controller
package com.example.redis_limit_demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("demo") @RestController public class DemoController { @RequestMapping("limit") public String demo() { //todo 写业务逻辑 return "aaaaa"; } }
http://localhost:8080/demo/limit
当刷新频率高了以后,就会报错
5、码云地址(GitHub经常访问不到)
备注:
1、 redis的key可以根据实际情况设置,入例子中的ip+url,可以将全部流量进行控制,防止恶意刷接口,但需要注意的是,使用ip方式,要将QPS设置大一些,因为会出现整个大厦公用一个ip的情况。也可以使用url+userName,将QPS设置小一点,可以更加精准的限制api的访问。
2、可以将抛出异常进行全局捕获和统一返回。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。