当前位置: 首页 > news >正文

分布式限流不会用?一个注解简单搞定

这里是weihubeats,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党

背景

在一些高并发或者供外部访问的接口,在受服务器压力的背景下,我们需要根据自身服务器每分钟可提供的QPS对一些接口进行限流处理,来保障整个系统的稳定性,所以我们需要一个限流功能。
最简单的限流方式就是使用GuavaRateLimiter

public void testRateLimiter() {
 RateLimiter r = RateLimiter.create(10);
 while (true) {
 System.out.println("get 1 tokens: " + r.acquire() + "s");
 }

但是改方案不是一个分布式限流,现在都是分布式系统,多节点部署.我们希望基于IP或者自定义的key去分布式限流,比如一个用户在1分钟内只能访问接口100次。
入股是这种方式限流,有三个接口,实际访问的次数就是300次

Redis分布式限流

Redis分布式限流自己实现一般是使用Lua脚本去实现,但是实际编写Lua脚本还是比较费劲,庆幸的是Redisson直接提供了基于Lua脚本实现的分布式限流类RRateLimiter

分布式限流sdk编写

为了使用简单方便,我们还是对Redisson进行简单封装,封装一个注解来使用分布式限流

定义注解

  • Limiter
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter {

	/**
	 * 限流器的 key
	 *
	 * @return key
	 */
	String key() default "";

	/**
	 * 限制数量
	 *
	 * @return 许可数量
	 */
	long rate() default 100;

	/**
	 * 速率时间间隔
	 *
	 * @return 速率时间间隔
	 */
	long rateInterval() default 1;

	/**
	 * 时间单位
	 *
	 * @return 时间
	 */
	RateIntervalUnit rateIntervalUnit() default RateIntervalUnit.MINUTES;

	RateType rateType() default RateType.OVERALL;

}

IP工具类

由于需要获取IP,所以我们写一个IP获取工具类

  • IpUtil
public class IpUtil {


	public static String getIpAddr(HttpServletRequest request) {
		String ipAddress = null;
		try {
			ipAddress = request.getHeader("x-forwarded-for");
			if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
				ipAddress = request.getHeader("Proxy-Client-IP");
			}
			if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
				ipAddress = request.getHeader("WL-Proxy-Client-IP");
			}
			if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
				ipAddress = request.getRemoteAddr();
				if (ipAddress.equals("127.0.0.1")) {
					// 根据网卡取本机配置的IP
					InetAddress inet = null;
					try {
						inet = InetAddress.getLocalHost();
					}
					catch (UnknownHostException e) {
						e.printStackTrace();
					}
					ipAddress = inet.getHostAddress();
				}
			}
			// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
			if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
				if (ipAddress.indexOf(",") > 0) {
					ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
				}
			}
		}
		catch (Exception e) {
			ipAddress = "";
		}
		return ipAddress;
	}

}

AOP切面

  • AnnotationAdvisor
public class AnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final Advice advice;

    private final Pointcut pointcut;

    private final Class<? extends Annotation> annotation;

    public AnnotationAdvisor(@NonNull MethodInterceptor advice,
                                                 @NonNull Class<? extends Annotation> annotation) {
        this.advice = advice;
        this.annotation = annotation;
        this.pointcut = buildPointcut();
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (this.advice instanceof BeanFactoryAware) {
            ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
        }
    }

    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(annotation, true);
        Pointcut mpc = new AnnotationMethodPoint(annotation);
        return new ComposablePointcut(cpc).union(mpc);
    }

    /**
     * In order to be compatible with the spring lower than 5.0
     */
    private static class AnnotationMethodPoint implements Pointcut {

        private final Class<? extends Annotation> annotationType;

        public AnnotationMethodPoint(Class<? extends Annotation> annotationType) {
            Assert.notNull(annotationType, "Annotation type must not be null");
            this.annotationType = annotationType;
        }

        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE;
        }

        @Override
        public MethodMatcher getMethodMatcher() {
            return new AnnotationMethodMatcher(annotationType);
        }

        private static class AnnotationMethodMatcher extends StaticMethodMatcher {
            private final Class<? extends Annotation> annotationType;

            public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) {
                this.annotationType = annotationType;
            }

            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                if (matchesMethod(method)) {
                    return true;
                }
                // Proxy classes never have annotations on their redeclared methods.
                if (Proxy.isProxyClass(targetClass)) {
                    return false;
                }
                // The method may be on an interface, so let's check on the target class as well.
                Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
                return (specificMethod != method && matchesMethod(specificMethod));
            }

            private boolean matchesMethod(Method method) {
                return AnnotatedElementUtils.hasAnnotation(method, this.annotationType);
            }
        }
    }
}
  • LimiterAnnotationInterceptor

核心实现类

@RequiredArgsConstructor
@Slf4j
public class LimiterAnnotationInterceptor implements MethodInterceptor {


	private final RedissonClient redisson;

	private static final Map<RateIntervalUnit, String> INSTANCE = Map.ofEntries(
			entry(RateIntervalUnit.SECONDS, "秒"),
			entry(RateIntervalUnit.MINUTES, "分钟"),
			entry(RateIntervalUnit.HOURS, "小时"),
			entry(RateIntervalUnit.DAYS, "天"));


	@Nullable
	@Override
	public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {

		Method method = invocation.getMethod();
		Limiter limiter = method.getAnnotation(Limiter.class);
		long limitNum = limiter.rate();
		long limitTimeInterval = limiter.rateInterval();

		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();
		String ip = IpUtil.getIpAddr(request);

		String key = DataUtils.isEmpty(limiter.key()) ? "limit:" + ip + "-" + request.getRequestURI() : limiter.key();

		RateIntervalUnit rateIntervalUnit = limiter.rateIntervalUnit();
		RRateLimiter rateLimiter = redisson.getRateLimiter(key);
		if (rateLimiter.isExists()) {
			RateLimiterConfig config = rateLimiter.getConfig();
			if (!Objects.equals(limiter.rate(), config.getRate())
					|| !Objects.equals(limiter.rateIntervalUnit()
					.toMillis(limiter.rateInterval()), config.getRateInterval())
					|| !Objects.equals(limiter.rateType(), config.getRateType())) {
				rateLimiter.delete();
				rateLimiter.trySetRate(limiter.rateType(), limiter.rate(), limiter.rateInterval(), limiter.rateIntervalUnit());
			}
		}
		else {
			rateLimiter.trySetRate(RateType.OVERALL, limiter.rate(), limiter.rateInterval(), limiter.rateIntervalUnit());
		}

		boolean allow = rateLimiter.tryAcquire();
		if (!allow) {
			String url = request.getRequestURL().toString();
			String unit = getInstance().get(rateIntervalUnit);
			String tooManyRequestMsg = String.format("用户IP[%s]访问地址[%s]时间间隔[%s %s]超过了限定的次数[%s]", ip, url, limitTimeInterval, unit, limitNum);
			log.info(tooManyRequestMsg);
			throw new BizException("访问速度过于频繁,请稍后再试");
		}
		return invocation.proceed();
	}

	public static Map<RateIntervalUnit, String> getInstance() {
		return INSTANCE;
	}

}

自动装载AOP Bean

  • AutoConfiguration
@Slf4j
@Configuration(proxyBeanMethods = false)
public class AutoConfiguration {
    @Bean
    public Advisor limiterAdvisor(RedissonClient redissonClient) {
        LimiterAnnotationInterceptor advisor = new LimiterAnnotationInterceptor(redissonClient);
        return new AnnotationAdvisor(advisor, Limiter.class);
    }
}

定义一个开启功能的注解

  • EnableLimiter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(AutoConfiguration.class)
public @interface EnableLimiter {
}

使用

@SpringBootApplication
@EnableLimiter
public class Application {

    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
        SpringApplication.run(Application.class, args);
    }
}

配置一个RedissonClient

  • RedissonClient
@Configuration
public class RedissonConfig {

    @Value("${redis.host}")
    private String redisLoginHost;
    @Value("${redis.port}")
    private Integer redisLoginPort;
    @Value("${redis.password}")
    private String redisLoginPassword;


    @Bean
    public RedissonClient redissonClient() {
        return createRedis(redisLoginHost, redisLoginPort, redisLoginPassword);
    }

    private RedissonClient createRedis(String redisHost, Integer redisPort, String redisPassword) {
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://" + redisHost + ":" + redisPort + "");
        if (DataUtils.isNotEmpty(redisPassword)) {
            singleServerConfig.setPassword(redisPassword);
        }
        config.setCodec(new JsonJacksonCodec());
        return Redisson.create(config);
    }

}

controller使用注解

    @GetMapping("/testLimiter")
    @Limiter(rate = 2, rateInterval = 10, rateIntervalUnit = RateIntervalUnit.SECONDS)
    public ActionEnum testLimiter(String name) {
        log.info("testLimiter {}", name);
        return ActionEnum.SUCCESS;
    }

相关文章:

  • Linux系统运维排故思路参考手册
  • 华为OD机考:0030-0031-n*n数组中二进制的最大数、整数的连续自然数之和
  • Jmeter的应用
  • 软件流程和管理(八):Ethics
  • SkyWalking持久化追踪数据
  • 数据导入与预处理-第4章-pandas数据获取
  • 机器学习之线性规划原理详解、公式推导(手推)、以及简单实例
  • 计算机网络——OSI 参考模型
  • 【.Net实用方法总结】 整理并总结System.IO中StreamWriter类及其方法介绍
  • openGl坐标系统
  • 实用工具系列 - Pycharm安装下载使用
  • Pyecharts绘图笔记
  • SNARK性能及安全
  • 学会 Python 自动安装第三方库,从此跟pip说拜拜
  • 3.前端开发就业前景
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • Android框架之Volley
  • JAVA之继承和多态
  • mysql常用命令汇总
  • Object.assign方法不能实现深复制
  • SQL 难点解决:记录的引用
  • Vue.js-Day01
  • 关于Java中分层中遇到的一些问题
  • 官方新出的 Kotlin 扩展库 KTX,到底帮你干了什么?
  • 诡异!React stopPropagation失灵
  • 快速构建spring-cloud+sleuth+rabbit+ zipkin+es+kibana+grafana日志跟踪平台
  • 微信开放平台全网发布【失败】的几点排查方法
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • #07【面试问题整理】嵌入式软件工程师
  • #Java第九次作业--输入输出流和文件操作
  • (+4)2.2UML建模图
  • (PADS学习)第二章:原理图绘制 第一部分
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (转)eclipse内存溢出设置 -Xms212m -Xmx804m -XX:PermSize=250M -XX:MaxPermSize=356m
  • (转)h264中avc和flv数据的解析
  • (转)socket Aio demo
  • ******IT公司面试题汇总+优秀技术博客汇总
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .net core使用EPPlus设置Excel的页眉和页脚
  • .NET MVC第三章、三种传值方式
  • .NET Remoting学习笔记(三)信道
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .NET成年了,然后呢?
  • .net开发时的诡异问题,button的onclick事件无效
  • .secret勒索病毒数据恢复|金蝶、用友、管家婆、OA、速达、ERP等软件数据库恢复
  • .skip() 和 .only() 的使用
  • .考试倒计时43天!来提分啦!
  • /dev/sda2 is mounted; will not make a filesystem here!
  • /tmp目录下出现system-private文件夹解决方法
  • @entity 不限字节长度的类型_一文读懂Redis常见对象类型的底层数据结构