在最近接口的开发过程中,为了减小接口的压力和一些恶意的请求,做了接口限流,常用的限流算法有:令牌桶算法和漏桶算法。具体的原理这里不做分析了,可以谷歌一下。
大致实现原理就是访问者在一定的时间内对某一个接口只能访问多少次。
Redis实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static boolean accessLimit (String key, int limit, int time, Jedis jedis) { boolean result = true ; key = "rate.limit:" + key; if (jedis.exists(key)) { long afterValue = jedis.incr(key); if (afterValue > limit) { result = false ; } } else { Transaction transaction = jedis.multi(); transaction.incr(key); transaction.expire(key, time); transaction.exec(); } return result; }
以上这种方式存在竟争条件,测试所得,产生结果不准确, 解决方法是用 WATCH
监控 rate.limit:key
的变动, 相对比较麻烦些。
Redis+Lua实现
创建rateLimit.lua
脚本文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
Java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jedis: pool: host: 127.0 .0 .1 port: 6379 timeout: 10000 config: maxTotal: 10000 minIdle: 100 maxIdle: 1000 testOnBorrow: true maxWaitMillis: 10000 testOnReturn: true testWhileIdle: true timeBetweenEvictionRunsMillis: 10000
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 @Configuration public class RedisConfig { @Value ("${jedis.pool.host}" ) private String host; @Value ("${jedis.pool.port}" ) private int port; @Value ("${jedis.pool.timeout}" ) private int timeout; @Value ("${jedis.pool.config.maxTotal}" ) private int maxTotal; @Value ("${jedis.pool.config.maxIdle}" ) private int maxIdle; @Value ("${jedis.pool.config.minIdle}" ) private int minIdle; @Value ("${jedis.pool.config.testOnBorrow}" ) private boolean testOnBorrow, @Value ("${jedis.pool.config.testOnReturn}" ) private boolean testOnReturn, @Value ("${jedis.pool.config.testWhileIdle}" ) private boolean testWhileIdle, @Value ("${jedis.pool.config.timeBetweenEvictionRunsMillis}" ) private int timeBetweenEvictionRunsMillis, @Value ("${jedis.pool.config.maxWaitMillis}" ) private int maxWaitMillis @Bean (name = "jedis.pool" ) @Autowired public JedisPool shardedJedis (@Qualifier("jedis.pool.config" ) JedisPoolConfig redisConfig) { JedisPool pool = new JedisPool(redisConfig, host, port, timeout); Jedis jedis = pool.getResource(); String ping = jedis.ping(); log.info("jedis.ping:" + ping + ", jedis server :" + host + ":" + port); return pool; } @Bean (name = "jedis.pool.config" ) public JedisPoolConfig jedisPoolConfig () { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setTestOnBorrow(testOnBorrow); config.setTestOnReturn(testOnReturn); config.setTestWhileIdle(testWhileIdle); config.setMaxWaitMillis(maxWaitMillis); config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); return config; } }
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 public boolean accessLimit (String key, int limit, int timeout) throws IOException { long result = 0 ; Jedis jedis = getResource(); try { List<String> keys = Collections.singletonList(key); List<String> argv = Arrays.asList(String.valueOf(limit), String.valueOf(timeout)); Reader reader = new InputStreamReader( Client.class.getClassLoader().getResourceAsStream("rateLimit.lua")); result = (long ) jedis.eval(CharStreams.toString(reader), keys, argv); } catch (IOException e) { e.printStackTrace(); }finally { returnBrokenResource(jedis); } return 1 == result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Retention (RetentionPolicy.RUNTIME)@Target (ElementType.METHOD)@Documented public @interface RateLimit { int seconds () default 0 ; int maxCount () default 100 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping (value = "/user" , produces = "application/json;charset=utf-8" )public class UserController { @RateLimit (seconds = 2 , maxCount = 5 ) @PostMapping ("/getUserInfo" ) public RespData getUserInfo (@RequestBody String rawData) throws Exception { if (StringUtil.isEmpty(rawData)) { return RespData.errorMsg(RetCode.ERROR_PARAMS_NOT_NULL.getCode(), "用户信息不能为空" ); } RespData data = userService.save(rawData); return data; } }
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 @Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private RedisUtil redisUtil; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object o) throws Exception { String methodName = null ; if (o instanceof HandlerMethod) { HandlerMethod handler = (HandlerMethod) o; String className = handler.getBean().getClass().getName(); methodName = handler.getMethod().getName(); String key = className + "." + methodName + "_" + openId; RateLimit limit = handler.getMethodAnnotation(RateLimit.class ) ; if (limit != null ) { int seconds = limit.seconds(); int maxCount = limit.maxCount(); boolean result = redisUtil.accessLimit(key, maxCount, seconds); if (result) { response.setContentType("application/json;charset=utf-8" ); PrintWriter out = response.getWriter(); RespData data = new RespData(); data.setCode(RetCode.ACTIVE_FAILURE.getCode()); data.setMsg("请求太频繁" ); log.info("请求太频繁----wechat-imin" ); out.write(JSON.toJSONString(data)); out.flush(); out.close(); return false ; } } } } }
以上是基于最近开发的项目的实现过程,具体还得根据自己的相关业务实现。
Lua 嵌入 Redis 优势:
减少网络开销,不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
参考:Redis两种方式实现限流