详解 Spring Boot 分布式限流实践
简介
随着互联网的快速发展,面对海量的请求,如何保证系统的稳定性和可用性就成为了分布式系统中必须解决的问题之一。限流是一种应对高并发场景的有效手段,只有控制请求流量,才能避免系统的崩溃或服务的瘫痪。本篇文章将详细讲解如何在 Spring Boot 中实现分布式限流。
限流方式
常见的限流方式包括漏桶算法、令牌桶算法和计数器算法等。其中,相对来说比较好理解的是令牌桶算法。所谓令牌桶算法,就是系统会按照一定的速率存入一定数量的令牌到桶中,每当一个请求到达时,就从桶中拿掉一个令牌,如果没有令牌,则拒绝请求,否则就接受请求并消耗一个令牌。在实际应用中,我们可以借助 Redis 来实现分布式限流,Redis 作为分布式缓存,可以很好的解决每个节点的访问限制问题。
实现流程
下面基于 Spring Boot 2.x 版本,结合 Redis,演示如何实现分布式限流。
1. 添加 Redis 依赖
在 pom.xml 文件中添加 Redis 相关依赖,如下所示:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置 Redis
在 application.yml 文件中添加 Redis 相关配置,如下所示:
spring:
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
max-active: 10
3. 自定义注解
定义一个自定义注解,用于标识需要进行访问限制的接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimit {
/**
* 允许访问的次数
*/
int times();
/**
* 时间段,单位为秒
*/
int second();
}
4. AOP 切面实现限流
基于 AOP 切面实现访问限制,代码如下所示:
@Aspect
@Component
@Slf4j
public class AccessLimitAspect {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Pointcut("@annotation(com.example.demo.annotation.AccessLimit)")
public void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
int times = accessLimit.times();
int second = accessLimit.second();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = IpUtils.getIpAddr(request);
String key = "access-limit:" + ip + ":" + request.getRequestURI();
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, second, TimeUnit.SECONDS);
}
if (count > times) {
throw new Exception("访问次数受限!");
}
return joinPoint.proceed();
}
}
5. 测试
@RestController
@RequestMapping("/demo")
@Slf4j
public class DemoController {
@GetMapping("/accessLimit")
@AccessLimit(times = 3, second = 10)
public String accessLimit() {
log.info("访问成功!");
return "访问成功!";
}
}
在上面的代码中,我们定义了一个 /accessLimit
接口,为该接口添加了 @AccessLimit(times = 3, second = 10)
注解,表示在 10 秒内最多允许访问 3 次。如果超过访问次数则会抛出 Exception
异常,接口无法访问。
示例
假设有两个服务,service-provider
和 service-consumer
,都是基于 Spring Boot 实现,现在需要在 service-provider
中实现限流,限制每个客户端在 60 秒内最多允许请求 100 次 /hello
接口,代码如下所示:
service-provider
端实现
Maven 依赖
在 service-provider
的 pom.xml 文件中添加以下依赖:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Redis 配置
在 service-provider
的 application.yml 文件中添加 Redis 配置:
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
访问限制
添加访问限制注解:
@RestController
@Slf4j
public class HelloController {
@GetMapping("/hello")
@AccessLimit(times = 100, second = 60)
public String hello() {
log.info("Hello!");
return "Hello!";
}
}
AOP 切面实现
添加 AOP 切面,实现访问限制:
@Aspect
@Component
@Slf4j
public class AccessLimitAspect {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Pointcut("@annotation(com.example.demo.annotation.AccessLimit)")
public void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
int times = accessLimit.times();
int second = accessLimit.second();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = IpUtils.getIpAddr(request);
String key = "access-limit:" + ip + ":" + request.getRequestURI();
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, second, TimeUnit.SECONDS);
}
if (count > times) {
throw new Exception("访问次数受限!");
}
return joinPoint.proceed();
}
}
service-consumer
端调用
在 service-consumer
的代码中,调用 service-provider
的 /hello
接口,代码如下:
@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
public class HelloServiceTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void hello() {
for (int i = 0; i < 500; i++) {
ResponseEntity<String> responseEntity = restTemplate.getForEntity("/hello", String.class);
log.info("调用次数:" + (i + 1) + ", 返回结果:" + responseEntity.getBody());
}
}
}
在上面代码中,我们在 for
循环中,模拟了 500 次调用 /hello
接口,运行结果如下:
2022-01-01 20:30:30.030 INFO hello-service-test:23 - 调用次数:1, 返回结果:Hello!
...
2022-01-01 20:30:33.331 INFO hello-service-test:23 - 调用次数:100, 返回结果:Hello!
2022-01-01 20:31:10.098 ERROR hello-service-test:26 - org.springframework.web.client.HttpClientErrorException$Forbidden: 403 null
2022-01-01 20:31:10.099 ERROR hello-service-test:26 - org.springframework.web.client.HttpClientErrorException$Forbidden: 403 null
...
可以看到,前 100 次访问都是正常的,当访问次数超过 100 次后,就会抛出 403 错误,访问被拒绝。
总结
通过本文的讲解,我们了解到了分布式限流的实现方法和流程,可以使用类似的方式来实现接口的访问限制。明确接口的业务场景和流量特点,采取对应的限流算法和策略,可以提升服务的可用性,避免服务的瘫痪,从而保证整个系统的稳定性。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:详解Springboot分布式限流实践 - Python技术站