优雅解决分布式限流(代码片段)

lywj lywj     2023-03-14     322

关键词:

SpringBoot 是为了简化 Spring 应用的创建、运行、调试、部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖就可以轻易的搭建出一个 WEB 工程

在前面的两篇文章中,介绍了一些限流的类型和策略,本篇从 Spring BootRedis 应用层面来实现分布式的限流….

分布式限流

单机版中我们了解到 AtomicIntegerRateLimiterSemaphore 这几种解决方案,但它们也仅仅是单机的解决手段,在集群环境下就透心凉了,后面又讲述了 Nginx 的限流手段,可它又属于网关层面的策略之一,并不能解决所有问题。例如供短信接口,你无法保证消费方是否会做好限流控制,所以自己在应用层实现限流还是很有必要的。

本章目标

利用 自定义注解Spring AopRedis Cache 实现分布式限流….

具体代码

很简单…

导入依赖

在 pom.xml 中添加上 starter-webstarter-aopstarter-data-redis 的依赖即可,习惯了使用 commons-lang3 和 guava 中的一些工具包…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<dependencies>
<!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>

属性配置

在 application.properites 资源文件中添加 redis 相关的配置项

1
2
3
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=battcn

Limit 注解

创建一个 Limit 注解,不多说注释都给各位写齐全了….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.battcn.limiter.annotation;


import com.battcn.limiter.LimitType;

import java.lang.annotation.*;

/**
* 限流
*
* @author Levin
* @since 2018-02-05
*/
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit

/**
* 资源的名字
*
* @return String
*/
String name() default "";

/**
* 资源的key
*
* @return String
*/
String key() default "";

/**
* Key的prefix
*
* @return String
*/
String prefix() default "";

/**
* 给定的时间段
* 单位秒
*
* @return int
*/
int period();

/**
* 最多的访问限制次数
*
* @return int
*/
int count();

/**
* 类型
*
* @return LimitType
*/
LimitType limitType() default LimitType.CUSTOMER;


public enum LimitType
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;

RedisTemplate

默认情况下 spring-boot-data-redis 为我们提供了StringRedisTemplate 但是满足不了其它类型的转换,所以还是得自己去定义其它类型的模板….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.battcn.limiter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

/**
* @author Levin
* @since 2018/8/2 0002
*/
@Configuration
public class RedisLimiterHelper

@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory)
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;


Limit 拦截器(AOP)

熟悉 Redis 的朋友都知道它是线程安全的,我们利用它的特性可以实现分布式锁、分布式限流等组件,在一起来学SpringBoot | 第二十三篇:轻松搞定重复提交(分布式锁)中讲述了分布式锁的实现,限流相比它稍微复杂一点,官方虽然没有提供相应的API,但却提供了支持 Lua 脚本的功能,我们可以通过编写 Lua 脚本实现自己的API,同时他是满足原子性的….

下面核心就是调用 execute 方法传入我们的 Lua 脚本内容,然后通过返回值判断是否超出我们预期的范围,超出则给出错误提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.battcn.limiter;


import com.battcn.limiter.annotation.Limit;
import com.google.common.collect.ImmutableList;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;

/**
* @author Levin
* @since 2018/2/5 0005
*/
@Aspect
@Configuration
public class LimitInterceptor

private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

private final RedisTemplate<String, Serializable> limitRedisTemplate;

@Autowired
public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate)
this.limitRedisTemplate = limitRedisTemplate;



@Around("execution(public * *(..)) && @annotation(com.battcn.limiter.annotation.Limit)")
public Object interceptor(ProceedingJoinPoint pjp)
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType)
case IP:
key = getIpAddress();
break;
case CUSTOMER:
// TODO 如果此处想根据表达式或者一些规则生成 请看 一起来学Spring Boot | 第二十三篇:轻松搞定重复提交(分布式锁)
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());

ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
try
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
logger.info("Access try count is for name= and key = ", count, name, key);
if (count != null && count.intValue() <= limitCount)
return pjp.proceed();
else
throw new RuntimeException("You have been dragged into the blacklist");

catch (Throwable e)
if (e instanceof RuntimeException)
throw new RuntimeException(e.getLocalizedMessage());

throw new RuntimeException("server exception");



/**
* 限流 脚本
*
* @return lua脚本
*/
public String buildLuaScript()
StringBuilder lua = new StringBuilder();
lua.append("local c");
lua.append(" c = redis.call(‘get‘,KEYS[1])");
// 调用不超过最大值,则直接返回
lua.append(" if c and tonumber(c) > tonumber(ARGV[1]) then");
lua.append(" return c;");
lua.append(" end");
// 执行计算器自加
lua.append(" c = redis.call(‘incr‘,KEYS[1])");
lua.append(" if tonumber(c) == 1 then");
// 从第一次调用开始限流,设置对应键值的过期
lua.append(" redis.call(‘expire‘,KEYS[1],ARGV[2])");
lua.append(" end");
lua.append(" return c;");
return lua.toString();


private static final String UNKNOWN = "unknown";

public String getIpAddress()
HttpServletRequest request = ((ServletRequestAttributes) 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-Proxy-Client-IP");

if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip))
ip = request.getRemoteAddr();

return ip;


控制层

在接口上添加 @Limit() 注解,如下代码会在 Redis 中生成过期时间为 100s 的 key = test 的记录,特意定义了一个 AtomicInteger 用作测试…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.battcn.controller;

import com.battcn.limiter.annotation.Limit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author Levin
* @since 2018/8/2 0002
*/
@RestController
public class LimiterController

private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();

@Limit(key = "test", period = 100, count = 10)
@GetMapping("/test")
public int testLimiter()
// 意味著 100S 内最多允許訪問10次
return ATOMIC_INTEGER.incrementAndGet();


主函数

就一个普通的不能在普通的主函数类了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.battcn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Levin
*/
@SpringBootApplication
public class Chapter27Application

public static void main(String[] args)
SpringApplication.run(Chapter27Application.class, args);


测试

完成准备事项后,启动 Chapter27Application 自行测试即可,测试手段相信大伙都不陌生了,如 浏览器postmanjunitswagger,此处基于 postman,如果你觉得自带的异常信息不够友好,那么配上一起来学SpringBoot | 第十八篇:轻松搞定全局异常 可以轻松搞定…

未达设定的阀值时

技术图片正确响应

达到设置的阀值时

技术图片错误响应

详解redisson分布式限流的实现原理(代码片段)

...还是有可能压垮下游。所以目前唯一可行的解决方案就是分布式限流。  我目前是选择直接使用Redisson库中的RRateLimiter实现了分布式限流,关于Redission可能很多人都有所耳闻,它其实是在Redis能力上构建的开发库,除了支持Redis... 查看详情

分布式系统中的限流与熔断(代码片段)

在应对秒杀、大促、双11、618等高性能压力的场景时,限流已经成为了标配技术解决方案,为保证系统的平稳运行起到了关键性的作用。不管应用场景是哪种,限流无非就是针对超过预期的流量,通过预先设定的限流规则选择性... 查看详情

springboot-优雅的实现流控

...息队列思路+应用拆分思路+应用限流思路深入理解分布式技术-限流计数器 查看详情

图解+代码|常见限流算法以及限流在单机分布式场景下的思考(代码片段)

...限流的相关内容,包括常见的限流算法、单机限流场景、分布式限流场景以及一些常见限流组件。当然在介绍限流算法和具体场景之前我们先得明确什么是限流,为什么要限流?。任何技术都要搞清它的来源,技术的产生来自痛... 查看详情

详解redisson分布式限流的实现原理(代码片段)

...还有一些注意事项。本文分享自华为云社区《详解Redisson分布式限流的实现原理》,作者:xindoo。我们目前在工作中遇到一个性能问题,我们有个定时任务需要处理大量的数据,为了提升吞吐量,所以部署了很... 查看详情

java基于redis实现分布式应用限流(代码片段)

查看详情

使用redis+lua脚本实现分布式限流(代码片段)

...(enhance-boot-limiting)主要是基于Redis+lua实现了分布式限流功能项目中提供两种分布式限流算法(一种是滑动时间窗口算法、一种是令牌桶算法)项目中提供了方便使用的注解形式来直接对接口进行限流,详... 查看详情

使用redis+lua脚本实现分布式限流(代码片段)

...(enhance-boot-limiting)主要是基于Redis+lua实现了分布式限流功能项目中提供两种分布式限流算法(一种是滑动时间窗口算法、一种是令牌 查看详情

一个轻量级的基于ratelimiter的分布式限流实现(代码片段)

...。RateLimiter通过线程锁控制同步,只适用于单机应用,在分布式环境下,虽然有像阿里Sentinel的限流开源框架,但对于一些小型应用来说未免过重,但限流的需求在小型项目中也是存在的,比如获取手机验证码的控制,对资源消... 查看详情

微服务架构分布式限流策略(代码片段)

...程内限流单机限流依赖配置中心go-zero基于redis设计的两款分布式限流器kubernetes怎么实现分布式限流istioingressratelimitingk8s获取pod副本数做限流限流方式集中式(全局)限流网关层限流服务网关,作为整个分布式链路中... 查看详情

分布式接口幂等性分布式限流:guavanginx和lua限流(代码片段)

点击关注公众号,实用技术文章及时了解一、接口幂等性接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用... 查看详情

分布式架构(10)---基于redis组件的特性,实现一个分布式限流(代码片段)

分布式---基于Redis进行接口IP限流场景为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有... 查看详情

分布式场景下接口的限流幂等防止重复提交(代码片段)

简单实现定义注解importjava.lang.annotation.*;/***@author向振华*@date2022/11/2118:16*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceLimiter/***限制 查看详情

常见的限流方式(代码片段)

...的可用性。根据限流作用范围,可以分为单机限流和分布式限流;根据限流方式,又分为计数器、滑动窗口、漏桶和令牌桶限流,下面我们对这块详细进行讲解。1.常用限 查看详情

常见的限流方式(代码片段)

...的可用性。根据限流作用范围,可以分为单机限流和分布式限流;根据限流方式,又分为计数器、滑动窗口、漏桶和令牌桶限流,下面我们对这块详细进行讲解。1.常用限 查看详情

常见的限流方式(代码片段)

...的可用性。根据限流作用范围,可以分为单机限流和分布式限流;根据限流方式,又分为计数器、滑动窗口、漏桶和令牌桶限流,下面我们对这块详细进行讲解。1.常用限 查看详情

我司用了6年的redis分布式限流器,可以说是非常厉害了!(代码片段)

...的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。相比Redis事务,Lua脚本的优点:减少网络开销:使用Lua脚本,无需向Redis发送多次请求,执行一次即可,减少网络传输原子操作:Re... 查看详情

springboot如何进行限流?老鸟们还可以这样玩!(代码片段)

...实现了接口限流。但是这种方式有个问题,无法实现分布式限流。那今天我们来利用Redis+Lua来实现分布式限流。Lua脚本和MySQL数据库的存储过程比较相似,他们执行一组命令,所有命令 查看详情