题外话:这是我的第一篇CSDN博客,第一次去尝试把自己学到的东西以博客的形式写出,希望可以给需要的人一些帮助,也希望今后自己复习的时候可以及时找到
接口功能
实现了在分布式部署的情况下对不同机器不同接口采取不同的限流策略实现了对接口限流策略的动态更新通过AOP实现接口限流
AOP,面向切面思想,是Spring的三大核心思想之
设计思想
核心思想就是通过redis中存储的接口访问次数来判断该接口在某一时间段内被访问了多少次,然后决定是否关闭接口访问来缓解服务器的压力。
代码解析
项目结构整体预览
AccessLimiter限流接口
该接口中包含了四个方法isLimited(判断指定的key是否收到访问限制),addLimitInfo
代码如下:
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);
}
AccessLimiter接口实现类
代码如下:
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
Limit标签接口,实现注解方式
通过标签接口可以更加直接方便的在接口上实现限流
代码如下:
package com.ypf.accesslimit.limit;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {
}
LimitAspect切面实现
切面的实现,实现接口屏蔽和限流的逻辑其中涉及到两个产生redis的key值的方法,因为要实现不同设备不同接口的不同限流策略并且对每一个设备的不同的接口分别进行计数。所以我采用了两种不同的key值生成策略。第一种限流策略的key值,我采取的格式是interfaceIsLimit:(方法的请求路径将下划线去掉并把下一个字母换成大写)如:interfaceIsLimit:TestLimit其中TestLimit由请求路径/test/limit得到。第二种记录不同设备不同接口的访问次数我使用的规则是设备id±+包名+类名+.+方法名如:10001-coypaccesslimit.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;
}
}
}
GenerateRedisKey生成保存策略的key值
把请求路径转换为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;iConfigInfo类获取设备id
代码如下:
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 redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
RedisSerializer 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 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 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);
}
}
结果演示
redis中的策略存储和访问次数存储
当未对限制接口进行访问时redis中只存储了接口限流的策略该策略是一个hash类型里面存储了times(访问次数)per(间隔时长)unit(时间单位)当对限制接口进行访问时redis中会多加一个记录访问次数的key值
接口限流后控制台输出
由于没有异常类和截获异常类所以当访问次数达到限制后仅仅通过控制台输出语句
项目地址
总结
本项目参考了结合他人的代码再加上自己的一些理解来进行开发的。由于平时还得弄导师的项目,所以没有时间去完善很多细节,这个项目仅仅用了几个小时就完成了开发。所以真的还有很多毛刺,希望大家在阅读的时候不要太在意细节,只想给大家提供一下我对分布式限流算法的理解。计数器算法在限流算法中属于最简单的算法,等我有时间的话会写一篇关于令牌桶的限流算法来弥补算法过于简单的遗憾。读研生活真的很累,但是我希望我的努力可以让我过上我想要的时候。加油吧!大家。
文章为作者独立观点,不代表观点
northlife2022-10-15
私募股权基金投资项目最好的结果就是上市成功,二级市场卖出获利,肯定也有上不了的投资项目,这个政策针对的是能上市的那部分,可以先不卖,直接分股票给投资者,这样可以理解吗