其他
分布式接口幂等性、分布式限流总结整理
上一篇:突发!Gitee 倒下了?
大家好,我是顶级架构师。
1、Update操作的幂等性
1)根据唯一业务号去更新数据
2、使用Token机制,保证update、insert操作的幂等性
1)没有唯一业务号的update与insert操作
1、分布式限流的几种维度
时间 限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定 资源 基于可用资源的限制,比如设定最大访问次数,或最高可用连接数
1)QPS和连接数控制
2)传输速率
3)黑白名单
4)分布式环境
网关层限流
中间件限流
2、限流方案常用算法讲解
1)令牌桶算法
令牌 获取到令牌的Request才会被处理,其他Requests要么排队要么被直接丢弃 桶用来装令牌的地方,所有Request都从这个桶里面获取令牌
2)漏桶算法
3、分布式限流的主流方案
1)Guava RateLimiter客户端限流
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
@RestController
@Slf4j
public class Controller{
//每秒钟可以创建两个令牌
RateLimiter limiter = RateLimiter.create(2.0);
//非阻塞限流
@GetMapping("/tryAcquire")
public String tryAcquire(Integer count){
//count 每次消耗的令牌
if(limiter.tryAcquire(count)){
log.info("成功,允许通过,速率为{}",limiter.getRate());
return "success";
}else{
log.info("错误,不允许通过,速率为{}",limiter.getRate());
return "fail";
}
}
//限定时间的非阻塞限流
@GetMapping("/tryAcquireWithTimeout")
public String tryAcquireWithTimeout(Integer count, Integer timeout){
//count 每次消耗的令牌 timeout 超时等待的时间
if(limiter.tryAcquire(count,timeout,TimeUnit.SECONDS)){
log.info("成功,允许通过,速率为{}",limiter.getRate());
return "success";
}else{
log.info("错误,不允许通过,速率为{}",limiter.getRate());
return "fail";
}
}
//同步阻塞限流
@GetMapping("/acquire")
public String acquire(Integer count){
limiter.acquire(count);
log.info("成功,允许通过,速率为{}",limiter.getRate());
return "success";
}
}
2)基于Nginx的限流
1.iP限流
@RestController
@Slf4j
public class Controller{
//nginx测试使用
@GetMapping("/nginx")
public String nginx(){
log.info("Nginx success");
}
}
127.0.0.1 www.test.com
vim /usr/local/nginx/conf/nginx.conf
#根据IP地址限制速度
#1)$binary_remote_addr binary_目的是缩写内存占用,remote_addr表示通过IP地址来限流
#2)zone=iplimit:20m iplimit是一块内存区域(记录访问频率信息),20m是指这块内存区域的大小
#3)rate=1r/s 每秒放行1个请求
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;
server{
server_name www.test.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
#基于ip地址的限制
#1)zone=iplimit 引用limit_rep_zone中的zone变量
#2)burst=2 设置一个大小为2的缓冲区域,当大量请求到来,请求数量超过限流频率时,将其放入缓冲区域
#3)nodelay 缓冲区满了以后,直接返回503异常
limit_req zone=iplimit burst=2 nodelay;
}
}
www.test.com/access-limit/nginx
2.多维度限流
#根据IP地址限制速度
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=10r/s;
#根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=1r/s;
#根据ip地址的链接数量做限流
limit_conn_zone $binary_remote_addr zone=perip:20m;
#根据服务器的连接数做限流
limit_conn_zone $server_name zone=perserver:20m;
server{
server_name www.test.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
#基于ip地址的限制
limit_req zone=iplimit burst=2 nodelay;
#基于服务器级别做限流
limit_req zone=serverlimit burst=2 nodelay;
#基于ip地址的链接数量做限流 最多保持100个链接
limit_conn zone=perip 100;
#基于服务器的连接数做限流 最多保持100个链接
limit_conn zone=perserver 1;
#配置request的异常返回504(默认为503)
limit_req_status 504;
limit_conn_status 504;
}
location /download/ {
#前100m不限制速度
limit_rate_affer 100m;
#限制速度为256k
limit_rate 256k;
}
}
3)基于Redis+Lua的分布式限流
1.Lua脚本
2.Lua安装
参考http://www.lua.org/ftp/教程,下载5.3.5_1版本,本地安装 如果你使用的是Mac,那建议用brew工具直接执行brew install lua就可以顺利安装, 有关brew工具的安装可以参考https://brew.sh/网站,建议翻墙否则会很慢。 使用brew安装后的目录在/usr/local/Cellar/lua/5.3.5_1 安装IDEA插件,在IDEA->Preferences面板,Plugins, 里面Browse repositories,在里面搜索lua,然后就选择同名插件lua。安装好后重启IDEA 另外,搜索公众号GitHub猿后台回复“监控系统”,获取一份惊喜礼包。 配置Lua SDK的位置:IDEA->File->Project Structure, 选择添加Lua,路径指向Lua SDK的bin文件 都配置好之后,在项目中右键创建Module,左侧栏选择lua,点下一步,选择lua的sdk,下一步,输入lua项目名,完成
3.编写hello lua
print 'Hello Lua'
4.编写模拟限流
-- 模拟限流
-- 用作限流的key
local key = 'my key'
-- 限流的最大阈值
local limit = 2
-- 当前限流大小
local currentLimit = 2
-- 是否超过限流标准
if currentLimit + 1 > limit then
print 'reject'
return false
else
print 'accept'
return true
end
5.限流组件封装
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
server.port=8080
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6376
-- 获取方法签名特征
local methodKey = KEYS[1]
redis.log(redis.LOG_DEBUG,'key is',methodKey)
-- 调用脚本传入的限流大小
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
local count = tonumber(redis.call('get',methodKey) or "0")
--是否超出限流值
if count + 1 >limit then
-- 拒绝访问
return false
else
-- 没有超过阈值
-- 设置当前访问数量+1
redis.call('INCRBY',methodKey,1)
-- 设置过期时间
redis.call('EXPIRE',methodKey,1)
-- 放行
return true
end
@Service
@Slf4j
public class AccessLimiter{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Boolean> rateLimitLua;
public void limitAccess(String key,Integer limit){
boolean acquired = stringRedisTemplate.execute(
rateLimitLua,//lua脚本的真身
Lists.newArrayList(key),//lua脚本中的key列表
limit.toString()//lua脚本的value列表
);
if(!acquired){
log.error("Your access is blocked,key={}",key);
throw new RuntimeException("Your access is blocked");
}
}
}
@Configuration
public class RedisConfiguration{
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
return new StringRedisTemplate(factory);
}
public DefaultRedisScript loadRedisScript(){
DefaultRedisScript redisScript = new DefaultRedisScript();
redisScript.setLocation(new ClassPathResource("rateLimiter.lua"));
redisScript.setResultType(java.lang.Boolean.class);
return redisScript;
}
}
@RestController
@Slf4j
public class Controller{
@Autowired
private AccessLimiter accessLimiter;
@GetMapping("test")
public String test(){
accessLimiter.limitAccess("ratelimiter-test",1);
return "success";
}
}
6.编写限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiterAop{
int limit();
String methodKey() default "";
}
@Slf4j
@Aspect
@Component
public class AccessLimiterAspect{
@Autowired
private AccessLimiter accessLimiter;
//根据注解的位置,自己修改
@Pointcut("@annotation(com.gyx.demo.annotation.AccessLimiter)")
public void cut(){
log.info("cut");
}
@Before("cut()")
public void before(JoinPoint joinPoint){
//获取方法签名,作为methodkey
MethodSignature signature =(MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AccessLimiterAop annotation = method.getAnnotation(AccessLimiterAop.class);
if(annotation == null){
return;
}
String key = annotation.methodKey();
Integer limit = annotation.limit();
//如果没有设置methodKey,就自动添加一个
if(StringUtils.isEmpty(key)){
Class[] type = method.getParameterType();
key = method.getName();
if (type != null){
String paramTypes=Arrays.stream(type)
.map(Class::getName)
.collect(Collectors.joining(","));
key += "#"+paramTypes;
}
}
//调用redis
return accessLimiter.limitAccess(key,limit);
}
}
@RestController
@Slf4j
public class Controller{
@Autowired
private AccessLimiter accessLimiter;
@GetMapping("test")
@AccessLImiterAop(limit =1)
public String test(){
return "success";
}
}
欢迎大家进行观点的探讨和碰撞,各抒己见。如果你有疑问,也可以找我沟通和交流。
最后给读者整理了一份BAT大厂面试真题,需要的可扫码回复“面试题”即可获取。
「顶级架构师」建立了读者架构师交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。
扫描添加好友邀你进架构师群,加我时注明【姓名+公司+职位】
版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
猜你还想看
别再自己瞎写工具类了,Spring Boot 内置工具类应有尽有, 建议收藏!!
JetBrains 宣布:IntelliJ 平台彻底停用 Log4j 组件,建议切换至 java.util.logging