SpringBoot 服务接口限流,搞定!
来源:blog.csdn.net/qq_34217386/article/details/122100904
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。限流可以认为服务降级的一种,限流通过限制请求的流量以达到保护系统的目的。 一般来说,系统的吞吐量是可以计算出一个阈值的,为了保证系统的稳定运行,一旦达到这个阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。否则,很容易导致服务器的宕机
前言
常见限流算法
- 计数器限流
计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。
计数器限流算法
如:使用 AomicInteger
来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。
- 漏桶算法
漏桶算法思路很简单,我们把水比作是 请求,漏桶比作是 系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
漏桶算法
- 令牌桶算法
令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
令牌桶算法
单机模式
Google
开源工具包 Guava
提供了限流工具类 RateLimiter
,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效
引入依赖 pom
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
package com.example.demo.common.annotation; import java.lang.annotation.*;
import java.util.concurrent.TimeUnit; @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit { // 资源key
String key() default ""; // 最多访问次数
double permitsPerSecond(); // 时间
long timeout(); // 时间类型
TimeUnit timeunit() default TimeUnit.MILLISECONDS; // 提示信息
String msg() default "系统繁忙,请稍后再试"; }
创建注解 Limit
package com.example.demo.common.aspect; import com.example.demo.common.annotation.Limit;
import com.example.demo.common.dto.R;
import com.example.demo.common.exception.LimitException;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
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.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map; @Slf4j
@Aspect
@Component
public class LimitAspect { private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.example.demo.common.annotation.Limit)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature)pjp.getSignature();
Method method = signature.getMethod();
//拿limit的注解
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
//key作用:不同的接口,不同的流量控制
String key=limit.key();
RateLimiter rateLimiter;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key, rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
}
rateLimiter = limitMap.get(key);
// 拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
// 拿不到命令,直接返回异常提示
if (!acquire) {
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(limit.msg());
}
}
return pjp.proceed();
} }
注解 aop 实现
注解使用permitsPerSecond
代表请求总数量timeout
代表限制时间
即 timeout
时间内,只允许有 permitsPerSecond
个请求总数量访问,超过的将被限制不能访问
测试
启动项目,快读刷新访问 /cachingTest
请求
分布式模式--基于 redis + lua
脚本的分布式限流
分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用 redis+lua 或者 nginx+lua 技术进行实现,通过这两种技术可以实现的高并发和高性能。
首先我们来使用 redis+lua 实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。lua 本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。
因操作是在一个 lua 脚本中(相当于原子操作),又因 redis 是单线程模型,因此是线程安全的。
相比 redis 事务来说,lua 脚本有以下优点
减少网络开销:不使用 lua 的代码需要向 redis 发送多次请求,而脚本只需一次即可,减少网络传输;
原子操作:redis 将整个脚本作为一个原子执行,无需担心并发,也就无需事务;
复用:脚本会永久保存 redis 中,其他客户端可继续使用。
package com.example.demo.common.annotation; import com.example.demo.common.enums.LimitType; import java.lang.annotation.*; @Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit { // 资源名称
String name() default ""; // 资源key
String key() default ""; // 前缀
String prefix() default ""; // 时间
int period(); // 最多访问次数
int count(); // 类型
LimitType limitType() default LimitType.CUSTOMER; // 提示信息
String msg() default "系统繁忙,请稍后再试"; }
创建注解 RedisLimit
package com.example.demo.common.aspect; import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.enums.LimitType;
import com.example.demo.common.exception.LimitException;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects; @Slf4j
@Aspect
@Configuration
public class RedisLimitAspect { private final RedisTemplate<String, Object> redisTemplate; public RedisLimitAspect(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
} @Around("@annotation(com.example.demo.common.annotation.RedisLimit)")
public Object around(ProceedingJoinPoint pjp){
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
LimitType limitType = annotation.limitType(); String name = annotation.name();
String key; int period = annotation.period();
int count = annotation.count(); switch (limitType){
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = annotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number number = redisTemplate.execute(redisScript, keys, count, period);
log.info("Access try count is {} for name = {} and key = {}", number, name, key);
if(number != null && number.intValue() == 1){
return pjp.proceed();
}
throw new LimitException(annotation.msg());
}catch (Throwable e){
if(e instanceof LimitException){
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(e.getLocalizedMessage());
}
e.printStackTrace();
throw new RuntimeException("服务器异常");
}
} public String buildLuaScript(){
return "redis.replicate_commands(); local listLen,time" +
"\nlistLen = redis.call('LLEN', KEYS[1])" +
// 不超过最大值,则直接写入时间
"\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then" +
"\nlocal a = redis.call('TIME');" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nelse" +
// 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
"\ntime = redis.call('LINDEX', KEYS[1], -1)" +
"\nlocal a = redis.call('TIME');" +
"\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then" +
// 访问频率超过了限制,返回0表示失败
"\nreturn 0;" +
"\nelse" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)" +
"\nend" +
"\nend" +
"\nreturn 1;";
} public String getIpAddress(){
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
} }
注解 aop 实现
注解使用count
代表请求总数量period
代表限制时间
即 period
时间内,只允许有 count
个请求总数量访问,超过的将被限制不能访问
package com.example.demo.module.test; import com.example.demo.common.annotation.Limit;
import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.dto.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList;
import java.util.List; @Slf4j
@RestController
public class TestController { @RedisLimit(key = "cachingTest", count = 2, period = 2, msg = "当前排队人数较多,请稍后再试!")
// @Limit(key = "cachingTest", permitsPerSecond = 1, timeout = 500, msg = "当前排队人数较多,请稍后再试!")
@GetMapping("cachingTest")
public R cachingTest(){
log.info("------读取本地------");
List<String> list = new ArrayList<>();
list.add("蜡笔小新");
list.add("哆啦A梦");
list.add("四驱兄弟"); return R.ok(list);
} }
使用
测试
启动项目,快读刷新访问 /cachingTest
请求
可以看到访问已经有被成功限制
这只是其中一种实现方式,尚有许多实现方案,经供参考
SpringBoot 服务接口限流,搞定!的更多相关文章
- 服务限流 -- 自定义注解基于RateLimiter实现接口限流
1. 令牌桶限流算法 令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个 ...
- Spring Cloud(7):Zuul自定义过滤器和接口限流
上文讲到了Zuul的基本使用: https://www.cnblogs.com/xuyiqing/p/10884860.html 自定义Zuul过滤器: package org.dreamtech.a ...
- 【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!
写在前面 在互联网应用中,高并发系统会面临一个重大的挑战,那就是大量流高并发访问,比如:天猫的双十一.京东618.秒杀.抢购促销等,这些都是典型的大流量高并发场景.关于秒杀,小伙伴们可以参见我的另一篇 ...
- Guava-RateLimiter实现令牌桶控制接口限流方案
一.前言 对于一个应用系统来说,我们有时会遇到极限并发的情况,即有一个TPS/QPS阀值,如果超了阀值可能会导致服务器崩溃宕机,因此我们最好进行过载保护,防止大量请求涌入击垮系统.对服务接口进行限流可 ...
- SpringBoot 如何进行限流?老鸟们都这么玩的!
大家好,我是飘渺.SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算 ...
- 为什么说要搞定微服务架构,先搞定RPC框架?
今天开始聊一些微服务的实践,第一块,RPC框架的原理及实践,为什么说要搞定微服务架构,先搞定RPC框架呢? 一.需求缘起 服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队 ...
- Spring Cloud Alibaba基础教程:使用Sentinel实现接口限流
最近管点闲事浪费了不少时间,感谢网友libinwalan的留言提醒.及时纠正路线,继续跟大家一起学习Spring Cloud Alibaba. Nacos作为注册中心和配置中心的基础教程,到这里先告一 ...
- SpringCloud(8)---zuul权限校验、接口限流
zuul权限校验.接口限流 一.权限校验搭建 正常项目开发时,权限校验可以考虑JWT和springSecurity结合进行权限校验,这个后期会总结,这里做个基于ZuulFilter过滤器进行一个简单的 ...
- 【58沈剑架构系列】为什么说要搞定微服务架构,先搞定RPC框架?
第一章聊了[“为什么要进行服务化,服务化究竟解决什么问题”] 第二章聊了[“微服务的服务粒度选型”] 今天开始聊一些微服务的实践,第一块,RPC框架的原理及实践,为什么说要搞定微服务架构,先搞定RPC ...
- 【Dnc.Api.Throttle】适用于.Net Core WebApi接口限流框架
Dnc.Api.Throttle 适用于Dot Net Core的WebApi接口限流框架 使用Dnc.Api.Throttle可以使您轻松实现WebApi接口的限流管理.Dnc.Api.Thr ...
随机推荐
- 【转帖】数据库篇-MySql架构介绍
https://zhuanlan.zhihu.com/p/147161770 公众号-坚持原创,码字不易.加微信 : touzinv 关注分享,手有余香~ 本篇咱们也来聊聊mysql物理和逻辑架构,还 ...
- [转帖]APIServer dry-run and kubectl diff
https://kubernetes.io/blog/2019/01/14/apiserver-dry-run-and-kubectl-diff/ Monday, January 14, 2019 A ...
- JDK内嵌指令的简单学习
java 可以使用 java -jar的方式启动服务 日常工作中用到的比较少 javac 可以将.java 文件编译成 .class中间代码 这个工具开发编写代码中是经常需要使用的, jenkins ...
- 物联网浏览器(IoTBrowser)-Web串口自定义开发
物联网浏览器(IoTBrowser)-Web串口自定义开发 工控系统中绝大部分硬件使用串口通讯,不论是原始串口通讯协议还是基于串口的Modbus-RTU协议,在代码成面都是使用System.IO.Po ...
- 深入浅出RPC服务 | 不同层的网络协议
导读: 本系列文章从RPC产生的历史背景开始讲解,涉及RPC核心原理.RPC实现.JSF的实现等,通过图文类比的方式剖析它的内部世界,让大家对RPC的设计思想有一个宏观的认识. 作者:王禹展 京东 ...
- OpenIM (Open-Source Instant Messaging) Mac Deployment Guide
This guide provides step-by-step instructions for deploying OpenIM on a Mac, including both source c ...
- 开源IM项目OpenIM每周迭代版本发布-群管理 阅后即焚等-v2.0.6
新特性介绍 OpenIM每周五发布新版,包括新特性发布,bug修复,同时合并PR,解决issue等 一个完善的IM系统,非常复杂,功能繁多,需求不一,比如对象存储有云端oss,cos,s3,私有化存储 ...
- 设计模式学习-使用go实现状态模式
状态模式 定义 优点 缺点 适用范围 代码实现 参考 状态模式 定义 状态模式(state):当一个条件的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. 状态模式主要解决的是当控制一个对 ...
- 《Spring 手撸专栏》| 开篇介绍,我要带新人撸 Spring 啦!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 不正经!写写面经,去撸Spring源码啦? 是的,在写了4篇关于Spring核心源码 ...
- 分享四个实用的vue自定义指令
v-drag 需求:鼠标拖动元素 思路: 元素偏移量 = 鼠标滑动后的坐标 - 鼠标初始点击元素时的坐标 + 初始点击时元素距离可视区域的top.left 将可视区域作为边界,限制在可视区域里面拖拽 ...