该种实现方案采用注解的方式,可根据需要灵活配置,但是缺点也很明显,不能均匀限流,配置不当的话,短时间可能会有大量请求涌入后端,具体效果也在摸索中,发现不足请多交流,好了,废话少说,放码过来!
环境:springboot redis
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis工具类
package com.huobao.c.buyer.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* Created by lemon on 2019-01-16.
*/
@Slf4j
@Component
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
/**
* 向 redis里面 放入值
* @param key
* @param value
* @return
*/
public Boolean setStrData(String key, Object value) {
return setStrData(key, value, null);
}
/**
* 向 redis里面设置 数据(可以指定 过期 时间)
* @param key
* @param value
* @param seconds
* @return
*/
public Boolean setStrData(String key, Object value, Long seconds) {
try {
ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue();
if (null != seconds) {
valueOperations.set(key, value,seconds, TimeUnit.MILLISECONDS);
}else{
valueOperations.set(key,value);
}
} catch (Exception e) {
e.printStackTrace();
log.error("向redis添加数据异常",e);
return false;
}
return true;
}
/**
* 获取 数据
* @param key
* @return
*/
public Object getData(String key) {
try {
ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
} catch (Exception e) {
e.printStackTrace();
log.error("redis获取数据异常",e);
return null;
}
}
/**
* 替换指定偏移量的值
* @param key
* @param value
* @param offset 偏移量
* @return
*/
public void offset(String key,Object value,Long offset){
try {
ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key,value,offset);
} catch (Exception e) {
e.printStackTrace();
log.error("redis替换指定偏移异常",e);
}
}
}
自定义限流注解
package com.huobao.c.buyer.config;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author lemon
* @Description: 自定义访问限流注解
* @date 2020/7/1 10:35
*/
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
/**
* 时间间隔 单位秒
* @return
*/
int seconds()default 2;
/**
* 访问次数
* @return
*/
int maxCount()default 2;
/**
* 是否需要登录
* @return
*/
boolean needLogin()default true;
}
定义拦截器
拦截器里需要登陆并且限流的接口,放的key是uri+用户标识,我这边放的是userId(过滤器已经把token转换为userId放入请求参数里),也可放入ip等,不建议直接放token,因为key长度太长的话,影响效率
package com.huobao.c.buyer.interceptors;
import com.huobao.c.buyer.config.AccessLimit;
import com.huobao.c.buyer.util.RedisUtils;
import com.huobao.common.exception.ValidationException;
import com.huobao.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by lemon on 2019/6/3.
*/
@Slf4j
@Configuration
public class AccessLimitInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取方法中的注解,看是否有限流注解
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
if (accessLimit != null) {
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean login = accessLimit.needLogin();
String key = request.getRequestURI();
//如果需要登录
if (login) {
String userId = request.getParameter("userId");
if (StringUtils.isNotEmpty(userId)) {
key += "_" + userId;
}
}
//从redis中获取用户访问的次数
Integer count = (Integer) redisUtils.getData(key);
if (count == null) {
//第一次访问
redisUtils.setStrData(key, 1, seconds * 1000L);
} else if (count < maxCount) {
//加1
count++;
redisUtils.offset(key, count, 0L);
} else {
log.info("访问频繁拦截,访问地址:{}", key);
throw new ValidationException("您的访问太频繁了,请稍后再试");
}
}
}
} catch (ValidationException e) {
throw e;
} catch (Exception e) {
log.info("访问限流异常:{}", e.getMessage());
}
return true;
}
}
上面代码中进行计数+1操作,我使用的是替换指定偏移量操作,不少博客说key放入数值,然后自增操作key,但是实际使用中,自增时候会报目标类型不是integer类型错误,这是因为spring boot 帮我们注入的 redisTemplate 类,泛型里面只能写<String, String>、<Object, Object>,要想Integer类型需要手动配置redis conifg,我这里也算偷个懒,用了上面的方法。
redisTemplate.opsForValue().set(key,1,3000l);
redisTemplate.opsForValue().increment(key);
配置拦截器
package com.huobao.c.buyer.config;
import com.huobao.c.buyer.interceptors.AccessLimitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Created by lemon on 2019/6/3.
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private AccessLimitInterceptor accessLimitInterceptor ;
/**
* 注册自定义的拦截器类
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 加入自定义拦截器,这里可以根据实际需要对各个url添加不同的拦截器
registry.addInterceptor(accessLimitInterceptor ).addPathPatterns("/**/**");
}
}
使用演示
使用很简单,只需在需要限流的api加上注解和参数即可,也可使用默认参数
@AccessLimit(seconds = 3,maxCount = 1,needLogin = true)
@ApiOperation(value = "用户提现",notes = "用户提现",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@RequestMapping(value = "save_accountwithdraw",method = RequestMethod.POST)
public Result saveAccountWithdraw(@ModelAttribute AccountWithDrawForm accountWithDrawForm){
Result.generateSuccess("提现成功")
}