通过Redis+自定义注解实现接口限流策略
目录
1、前言
通过自定义注解
+reids
+lua
实现,接口限流策略,其实质就是对redis的分布式锁的应用。
流程基本如下:
- 1、Controller接口的方法,实现自定义注解
@RateLimiter
。 - 2、自定义拦截
RateLimiterHandlerInterceptor
,拦截包含注解@RateLimiter
的接口,进行验证。 - 3、为了保证在并发请求下的精确性,使用
redis+lua
脚本进行加锁。 - 4、如果窗口时间内请求没有达到上限则放行,如果达到了上限,则返回错误。
2、代码实现
2.1 自定义注解
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface IpRateLimiter { //一个IP下请求的并发限制 int period() default 1; //限流时间周期,默认 1S int count() default 10; //周期内限制次数,默认 10次 boolean rateIP() default true; //默认限制IP,设置为false表示只限制接口请求次数}
rateIP
表示更加细致的控制,如果不需要限制ip,而是对接口设置一个统一的访问上限,则将rateIP
设置为flase
,比如:@RateLimiter(rateIP = false)
2.2 lua脚本配置
为了保证适应高并发,通过reids+lua脚本来实现加锁与解锁,达到一致性的目的。
lua脚本:
在resources
目录下创建lua
文件夹,在lua
创建rateLimit.lua
文件,内容如下:
local key = KEYS[1]local limit = tonumber(KEYS[2])local length = tonumber(KEYS[3])--redis.log(redis.LOG_NOTICE,' length: '..length)local current = redis.call('GET', key)if current == false then --redis.log(redis.LOG_NOTICE,key..' is nil ') redis.call('SET', key,1) redis.call('EXPIRE',key,length) --redis.log(redis.LOG_NOTICE,' set expire end') return '1'else --redis.log(redis.LOG_NOTICE,key..' value: '..current) local num_current = tonumber(current) if num_current+1 > limit thenreturn '0' elseredis.call('INCRBY',key,1)return '1' endend
其中key
就是请求的接口信息,length
为窗口时间,limit
为窗口时间内允许访问的最大并发数量,比如:key=test,length=1,limit=10,就表示接口test
在1秒
内最大的并发为10
,脚本中返回1
表示成功,0
表示失败。
rateLimit.lua逻辑如下:
- 1、根据
key
获取当前的请求数量; - 2、如果为空,则将
key
的值设置为1
,并且设置过期时间也就是窗口时间,并且返回1
; - 3、如果不为空,则判断
当前请求数+1
是否大于limit
(最大并发数),如果是则返回0
,如果不是则将key
的值自加1
,并且返回1
;
RedisLuaConfig:
创建Redis操作Lua的配置类,内容如下:
@Configurationpublic class RedisLuaConfig { @Resource private StringRedisTemplate stringRedisTemplate; /** * @param keyList redis得Lua脚本中的key列表,我们把参数放在这里面传递 * @return result 返回 1表示,正常,0表示限制访问 */ public String runLuaScript(List<String> keyList) { DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/rateLimit.lua"))); redisScript.setResultType(String.class); String args = "none"; try { return stringRedisTemplate.execute(redisScript, keyList); } catch (Exception e) { e.printStackTrace(); return "0"; } }}
2.3 拦截器配置
创建RateLimiterHandlerInterceptor
类并且实现HandlerInterceptor
接口,在该类中获取含有RateLimiter
的请求,并且取出注解的配置,然后通过ip + "@" + httpMethod + "@" + path
的形式生成key,再根据窗口时间和最大并发数,请求lua脚本,实现限流判断。
为了防止被暴力请求,默认情况下所有的接口进行限流,默认最大并发的10
。
@Component@Slf4jpublic class RateLimiterHandlerInterceptor implements HandlerInterceptor { @Resource private RedisLuaConfig redisLuaConfig; private static final int DEFAULT_PERIOD = 1; private static final int DEFAULT_COUNT = 10; private static final String LIMITER_KEY = "limiter:"; private static final String LIMITER_IP_KEY = "limiter-ip:"; private static final String SUCCESS_CODE = "1"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Method method = ((HandlerMethod) handler).getMethod(); String path = request.getServletPath(); String httpMethod = request.getMethod(); //GET、PUT、DELETE、POST //获取当前请求IP String ip = IpUtils.getIpAddr(request); //如果没有开启开启注解,也进行默认限流操作 int period = DEFAULT_PERIOD; //限流时间周期,默认 1S int count = DEFAULT_COUNT; //周期内限制次数,默认 10次 boolean rateIP = true;//默认限制IP,设置为false表示只限制接口请求次数 IpRateLimiter ipRateLimiter = method.getAnnotation(IpRateLimiter.class); if (ipRateLimiter != null) { period = ipRateLimiter.period(); count = ipRateLimiter.count(); //设置当前key,对同一个ip下同一个请求进行限流操作 rateIP = ipRateLimiter.rateIP(); } //不对ip进行限制 String key = LIMITER_KEY + httpMethod + "@" + path; if (rateIP) { //对ip进行次数限制 key = LIMITER_IP_KEY + ip + "@" + httpMethod + "@" + path; } String res = ""; try { List<String> keyList = new ArrayList<>(); keyList.add(key); //表示时间周期内运行访问得次数 keyList.add(String.valueOf(count)); //count keyList.add(String.valueOf(period));//period res = redisLuaConfig.runLuaScript(keyList); } catch (Exception e) {e.printStackTrace();throw new RuntimeException("你被限流了"); } //正常执行RedisLua if (!SUCCESS_CODE.equals(res)) { log.error("你被限流了"); throw new RuntimeException("你被限流了"); } return true; }}
3、测试
将Controller
中的一个接口,加上RateLimiter
注解,为了更好的验证限流,将请求的最大并发数设置为1
@RestController@RequestMapping("test")public class TestController { @GetMapping("/start") @RateLimiter(count = 1) public String start() { logger.info("成功访问接口"); return "操作成功"; }}
通过浏览器快速刷新访问接口,就会触发限流,结果如下: