题外话:这是我的第一篇CSDN博客,第一次去尝试把自己学到的东西以博客的形式写出,希望可以给需要的人一些帮助,也希望今后自己复习的时候可以及时找到
1)实现了在分布式部署的情况下对不同机器不同接口采取不同的限流策略
2)实现了对接口限流策略的动态更新
3)通过AOP实现接口限流
AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。
核心思想就是通过redis中存储的接口访问次数来判断该接口在某一时间段内被访问了多少次,然后决定是否关闭接口访问来缓解服务器的压力。
该接口中包含了四个方法 isLimited(判断指定的key是否收到访问限制),addLimitInfo(增加一条接口限流规则),getLimitInfo(查询接口限流规则), deleteLimited(删除接口限流规则)
代码如下:
package com.ypf.accesslimit.limit;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
public interface AccessLimiter {
/**
* 检测指定的key是否收到访问限制
* @param key 限制接口的标识
* @param times 访问次数
* @param per 一段时间
* @param unit 时间单位
* @return
*/
public boolean isLimited(String key, Long times, Long per, TimeUnit unit);
/**
* 增加一条接口限流规则
* @param redisKey
* @param map
* @return
*/
public void addLimitInfo(String redisKey, HashMap map);
/**
* 查询接口限流规则
* @param redisKey
* @return
*/
public HashMap getLimitInfo(String redisKey);
/**
* 删除接口限流规则
* @param key
*/
public void deleteLimited(String key);
}
代码如下:
package com.ypf.accesslimit.limit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class AccessLimiterImpl implements AccessLimiter{
@Autowired
private RedisTemplate redisTemplate;
/**
* 传入的是boundingKey 和 限流策略的key是不一样的
* @param key 限制接口的标识
* @param times 访问次数
* @param per 一段时间
* @param unit 时间单位
* @return
*/
@Override
public boolean isLimited(String key, Long times, Long per, TimeUnit unit) {
// 根据专门储存访问次数的key 将里面的数值加1
Long curTimes = redisTemplate.boundValueOps(key).increment(1);
log.info("curTimes: {}",curTimes);
if (curTimes>times){
log.error("超频访问:[{}]",key);
return true;
}else {
if (curTimes == 1){
log.info(" set expire");
redisTemplate.boundGeoOps(key).expire(per,unit);
}
return false;
}
}
@Override
public void addLimitInfo(String redisKey, HashMap map) {
redisTemplate.opsForHash().putAll(redisKey,map);
}
@Override
public HashMap getLimitInfo(String redisKey) {
HashMap<Object,Object> map = (HashMap<Object, Object>) redisTemplate.opsForHash().entries(redisKey);
return map;
}
@Override
public void deleteLimited(String key) {
// 因为不能直接删除这个hash 所以只能指定参数删除
redisTemplate.opsForHash().delete(key,"times","unit","per");
}
}
通过标签接口 可以更加直接方便的在接口上实现限流
代码如下:
package com.ypf.accesslimit.limit;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {
}
切面的实现,实现接口屏蔽和限流的逻辑 其中涉及到两个产生redis的key值的方法,因为要实现不同设备不同接口的不同限流策略 并且对每一个设备的不同的接口分别进行计数。所以我采用了两种不同的key值生成策略。
第一种 限流策略的key值,我采取的格式是interfaceIsLimit:(方法的请求路径 将下划线去掉 并把下一个字母换成大写)
如:interfaceIsLimit:TestLimit 其中TestLimit 由请求路径 /test/limit 得到。
第二种 记录不同设备不同接口的访问次数 我使用的规则是 设备id±+包名+类名+.+方法名
如:10001-com.ypf.accesslimit.controller.AccessLimitController.testAccessLimit
代码如下:
package com.ypf.accesslimit.limit;
import com.ypf.accesslimit.config.ConfigInfo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Component
public class LimitAspect {
@Autowired
private AccessLimiter limiter;
@Autowired
private GenerateRedisKey generateRedisKey;
@Autowired ConfigInfo configInfo;
@Pointcut("@annotation(com.ypf.accesslimit.limit.Limit)")
public void limitPointcut(){
}
/**
* redisKey中保存的是相关的限流政策 而bindingKey中保存的该接口访问的次数
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("limitPointcut()")
public Object doArround(ProceedingJoinPoint joinPoint) throws Throwable {
String redisKey = generateRedisKey.getMethodUrlConvertRedisKey(joinPoint);
System.out.println("redisKey = " + redisKey);
HashMap map = limiter.getLimitInfo(redisKey);
if (map!=null){ // 判断
String r_times = (String) map.get("times");
Long times = Long.parseLong(r_times);
String r_per = (String) map.get("per");
Long per = Long.parseLong(r_per);
TimeUnit unit = (TimeUnit) map.get("unit");
String bindingKey = genBindingKey(joinPoint);
System.out.println("bindingKey = " + bindingKey);
Boolean result = limiter.isLimited(bindingKey,times,per,unit);
if (result){
// 实际应该抛出异常阻止访问 这里只是模仿效果就不写异常类 和异常捕获类了
System.out.println("接口已被限制访问");
}
}
return null;
}
/**
* 根据不同微服务的limit.id(配置文件中写入) 来生成不同机器不同接口的唯一限制key
* @param joinPoint
* @return
*/
private String genBindingKey (ProceedingJoinPoint joinPoint){
try {
Method m =((MethodSignature)joinPoint.getSignature()).getMethod();
return configInfo.getId()+"-"+joinPoint.getTarget().getClass().getName()+"."+m.getName();
}catch (Throwable e){
return null;
}
}
}
把请求路径转换为Redis中存储的保存策略的key值
代码如下:
package com.ypf.accesslimit.limit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Component
public class GenerateRedisKey {
/**
* 把请求路径转换为Redis中存储的key值
* @param joinPoint
* @return
*/
public String getMethodUrlConvertRedisKey(ProceedingJoinPoint joinPoint){
StringBuilder redisKey = new StringBuilder("");
Method m = ((MethodSignature)joinPoint.getSignature()).getMethod();
// GetMapping methodAnnotation = m.getAnnotation(GetMapping.class);
String[] methodValue =null;
// 为了保证能在所有接口的类型上使用 所以要判断所有的类型
if (m.getAnnotation(GetMapping.class)!=null){
methodValue = m.getAnnotation(GetMapping.class).value();
}else if (m.getAnnotation(PostMapping.class)!=null){
methodValue = m.getAnnotation(PostMapping.class).value();
}else if (m.getAnnotation(PutMapping.class)!=null){
methodValue = m.getAnnotation(PutMapping.class).value();
}else if (m.getAnnotation(DeleteMapping.class)!=null){
methodValue = m.getAnnotation(DeleteMapping.class).value();
}else {
methodValue = m.getAnnotation(RequestMapping.class).value();
}
String decUrl = diagonalLineToCamel(methodValue[0]);
redisKey.append("interfaceIsLimit:").append(decUrl).toString();
return redisKey.toString();
}
/**
* 输入一个url 如 /hello/world 返回一个 HelloWorld
* @param param
* @return
*/
private String diagonalLineToCamel(String param){
char UNDERLINE = '/';
if (param==null||"".equals(param.trim())){
return "";
}
int len = param.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0;i<len;i++){
char c = param.charAt(i);
if (c==UNDERLINE){
if (++i<len){
// 只要不是最后一个就把下划线的下一个字母转换为大写
sb.append(Character.toUpperCase(param.charAt(i)));
}
}else {
sb.append(c);
}
}
return sb.toString();
}
}
代码如下:
package com.ypf.accesslimit.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "limit")
@Component
public class ConfigInfo {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
代码如下:
package com.ypf.accesslimit.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(redisSerializer);
// value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
限制接口测试,和对接口规则的增加 修改 查询 和更新
代码如下:
package com.ypf.accesslimit.controller;
import com.ypf.accesslimit.limit.AccessLimiter;
import com.ypf.accesslimit.limit.Limit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("access")
@CrossOrigin
public class AccessLimitController {
@Autowired
private AccessLimiter accessLimiter;
/**
* 限制接口测试
* @return
*/
@GetMapping ("/test/limit")
@Limit
public String testAccessLimit(){
return "hello world";
}
/**
* 添加一条新的接口限流规则(接口名称写死 如有需要可以从请求路径中获取)
*/
@GetMapping("/add")
public void addAccessLimit(){
String redisKey = "interfaceIsLimit:TestLimit";
// 默认是秒 可以通过将传入 ms s min hour 用判断语句改为 TimeUnit.SECONDS
TimeUnit unit = TimeUnit.SECONDS;
HashMap<String,Object> map = new HashMap<>();
map.put("times","10");
map.put("per","10");
map.put("unit",unit);
accessLimiter.addLimitInfo(redisKey,map);
}
/**
* 查询某接口限流规则(接口名称写死 如有需要可以从请求路径中获取)
* @return
*/
@GetMapping("/get")
public HashMap getAccessLimit(){
String redisKey = "interfaceIsLimit:TestLimit";
HashMap map = accessLimiter.getLimitInfo(redisKey);
return map;
}
/**
* 更新某一接口限流规则
* @param redisKey
* @param times
* @param per
*/
@GetMapping("/update/{redisKey}/{times}/{per}")
public void updateAccessLimit(@PathVariable String redisKey,@PathVariable String times,@PathVariable String per){
// 默认是秒 可以通过将传入 ms s min hour 用判断语句改为 TimeUnit.SECONDS
TimeUnit unit = TimeUnit.SECONDS;
HashMap<String,Object> map = new HashMap<>();
map.put("times",times);
map.put("per",per);
map.put("unit",unit);
// 先删除后更新 (应该可以写lua脚本去实现这个功能会更好 只需要访问一次redis 而且操作是原子性的)
accessLimiter.deleteLimited(redisKey);
accessLimiter.addLimitInfo(redisKey,map);
}
/**
* 删除某一接口限流规则
* @param redisKey
*/
@GetMapping("delete/{redisKey}")
public void deleteAccessLimit(@PathVariable String redisKey){
accessLimiter.deleteLimited(redisKey);
}
}
1.当未对限制接口进行访问时 redis中只存储了 接口限流的策略 该策略是一个hash类型 里面存储了 times(访问次数) per (间隔时长) unit(时间单位)
2.当对限制接口进行访问时 redis中会多加一个记录访问次数的key值
由于没有异常类和截获异常类 所以当访问次数达到限制后 仅仅通过控制台输出语句
https://gitee.com/yang-pengfei1999/springcloud-accesslimit
本项目参考了结合他人的代码 再加上自己的一些理解来进行开发的。由于平时还得弄导师的项目,所以没有时间去完善很多细节,这个项目仅仅用了几个小时就完成了开发。所以真的还有很多毛刺,希望大家在阅读的时候不要太在意细节,只想给大家提供一下 我对分布式限流算法的理解。计数器算法在限流算法中属于最简单的算法,等我有时间的话会写一篇关于令牌桶的限流算法来弥补算法过于简单的遗憾。读研生活真的很累,但是我希望我的努力可以让我过上我想要的时候。加油吧!大家。