SpringBoot 结合 Mybatis 实现创建数据库表

SpringBoot 结合 Mybatis 实现创建数据库表

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

一、滑动时间窗口是什么

滑动时间窗口是一种限流算法,它可以在一段时间内限制请求的数量,以保护后端服务免受大流量冲击。

滑动时间窗口限流算法将时间分成较小的时间片,将每个时间片内的请求进行累加,然后计算在当前时间点向前一个完整窗口内的请求总数。如果请求总数超过预设的阈值,那么后续请求将被拒绝。

而且,滑动时间窗口限流算法与固定时间窗口限流算法相比,滑动时间窗口限流算法可以更好地应对突发流量,降低限流对服务稳定性的影响。

二、滑动时间窗口的应用场景

滑动时间窗口限流算法适用于对请求频率进行限制的场景,例如:

  • 限制用户在一段时间内的请求次数;
  • 防止恶意用户对服务进行洪水攻击;
  • 保护后端服务免受大流量冲击,避免服务崩溃;
  • 适用于对流量限制要求较高的场景,需要更好地应对突发流量;

三、滑动时间窗口实现流程

滑动时间窗口限流算法的实现流程如下:

将时间划分成规定的时间片段,每个片段有固定的时间间隔,如 1ms、1s 或者 1min,然后定义一个时间窗口,比如1s、10s、1min 等,之后设置一个在指定时间窗口内请求的限制次数(阈值)。

时间窗口会随着时间向从左到右移动,过程中需要一个计数器来统计窗口时间内的请求次数。也就是说,当窗口移动过程中,清除掉窗口时间范围之前的请求次数,只统计窗口时间内的请求次数。

并且,每次请求时都要对窗口内的请求次数进行检查,判断当前的请求次数是否已经达到了限制的次数,如果是则拒绝执行请求,否则允许请求。

四、Redis 实现滑动时间窗口流程

这里主要使用 ZSET 有序集合来实现滑动窗口限流,ZSET 集合有下面几个特点:

  • ZSET 集合中的值不能重复;
  • ZSET 集合中的值可以根据分数进行排序;
  • ZSET 集合可以使用 ZCARD 命令获取元素个数;
  • ZSET 集合可以使用 ZREMRANGEBYSCORE 命令移除指定分数范围内的元素;

基于上面的四点特性,可以基于 Redis ZSET 集合实现滑动窗口限流。流程如下:

  1. 定义三个参数,分别为: 时间窗口大小、当前时间、请求次数阈值;
  2. 计算时间窗口的开始时间,即当前时间减去时间窗口大小;
  3. 使用 ZREMRANGEBYSCORE 命令移除时间窗口开始时间之前的所有请求次数记录;
  4. 使用 ZCARD 命令获取当前时间窗口内的请求次数;
  5. 如果当前时间窗口内的请求次数小于阈值,则将当前时间添加到时间窗口内,返回 1 表示超出请求次数阈值,否则返回 0 表示请求通过;

不过这种方式有一个问题,就是在高并发的情况下,可能会出现多个请求同时到达 Redis 服务器,导致多个请求同时通过了限流,这样就会导致超出了限流的次数。为了解决这个问题,可以使用 Redis 的 Lua 脚本来保证原子性。脚本内容如下:

 1-- ====== 滑动时间窗口计算 Lua 脚本 ======
 2-- KEYS[1]: zset集合Key名称
 3-- ARGV[1]: 当前时间
 4-- ARGV[2]: 窗口长度
 5
 6-- 参数设置
 7local key = KEYS[1]
 8local current_time = tonumber(ARGV[1])
 9local window_length = tonumber(ARGV[2])
10local threshold = tonumber(ARGV[3])
11-- 计算窗口开始时间
12local window_start = current_time - window_length
13-- 移除窗口时间之前的全部请求次数记录
14redis.call('zremrangebyscore', key, '-inf', window_start)
15-- 获取当前窗口内请求次数
16local current_requests = redis.call('zcard', key)
17-- 如果当前窗口内请求次数小于阈值,则添加当前时间到窗口内并返回1,否则返回0
18if current_requests < threshold then
19    -- 添加当前请求到窗口内,并且设置score和value都为当前时间
20    redis.call('zadd', key, current_time, current_time)
21    -- 设置过期时间为1小时
22    redis.call('expire', key, 3600)
23    return 1
24else
25    return 0
26end

不过这种方案只适合请求次数较少的情况,如果请求次数较多,比如每秒上万次请求,那么这种方式就不太适用了。因为每次请求都需要将请求记录添加到 ZSET 集合中,然后再进行 ZREMRANGEBYSCORE 和 ZCARD 操作,这样会导致 Redis 请求过于频繁,并且频繁往 ZSET 集合中添加元素,会导致 ZSET 集合中的元素过多,对 Redis 服务器的性能有一定的影响。

五、SpringBoot+Redis 实现滑动时间窗口示例

5.1 引入 Maven 依赖

pom.xml 文件中引入 SpringBoot、Redis、AOP 的依赖,如下所示:

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5
 6	<parent>
 7		<groupId>org.springframework.boot</groupId>
 8		<artifactId>spring-boot-starter-parent</artifactId>
 9		<version>3.2.5</version>
10		<relativePath/>
11	</parent>
12
13	<groupId>club.mydlq</groupId>
14	<artifactId>spring-boot-windows-limit</artifactId>
15	<version>0.0.1-SNAPSHOT</version>
16	<name>spring-boot-windows-limit</name>
17	<description>sliding windows limit flow</description>
18
19	<properties>
20		<java.version>17</java.version>
21	</properties>
22	
23	<dependencies>
24		<dependency>
25			<groupId>org.springframework.boot</groupId>
26			<artifactId>spring-boot-starter-web</artifactId>
27		</dependency>
28		<dependency>
29			<groupId>org.springframework.boot</groupId>
30			<artifactId>spring-boot-starter-aop</artifactId>
31		</dependency>
32		<dependency>
33			<groupId>org.springframework.boot</groupId>
34			<artifactId>spring-boot-starter-data-redis</artifactId>
35		</dependency>
36	</dependencies>
37
38	<build>
39		<plugins>
40			<plugin>
41				<groupId>org.springframework.boot</groupId>
42				<artifactId>spring-boot-maven-plugin</artifactId>
43			</plugin>
44		</plugins>
45	</build>
46
47</project>

5.2 创建 Redis 配置文件

创建 application.yml 配置文件,配置 Redis 集群的连接参数,如下所示:

 1spring:
 2  application:
 3    name: spring-boot-windows-limit
 4  data:
 5    redis:
 6      cluster:
 7        maxRedirects: 6
 8        nodes:
 9          - 192.168.2.1:6379
10          - 192.168.2.2:6379
11          - 192.168.2.3:6379
12          - 192.168.2.4:6379
13          - 192.168.2.5:6379
14          - 192.168.2.6:6379
15      password: 123456
16      lettuce:
17        pool:
18          max-active: 100
19          max-idle: 20
20          min-idle: 0
21          max-wait: 2000

5.3 创建 Redis 配置类

创建 Redis 配置类,设置 RedisTemplate 的序列化方式。

注: 如果这里不设置序列化方式,会导致 RedisTemplate 执行 lua 脚本时出现参数错误。

 1import org.springframework.context.annotation.Bean;
 2import org.springframework.context.annotation.Configuration;
 3import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 4import org.springframework.data.redis.core.RedisTemplate;
 5import org.springframework.data.redis.serializer.StringRedisSerializer;
 6
 7/**
 8 * Redis 配置
 9 *
10 * @author mydlq
11 */
12@Configuration
13public class RedisConfig {
14
15    @Bean(name = "redisLimitTemplate")
16    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
17        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
18        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
19        redisTemplate.setConnectionFactory(factory);
20        redisTemplate.setKeySerializer(stringRedisSerializer);
21        redisTemplate.setValueSerializer(stringRedisSerializer);
22        redisTemplate.setHashKeySerializer(stringRedisSerializer);
23        redisTemplate.setHashValueSerializer(stringRedisSerializer);
24        return redisTemplate;
25    }
26
27}

5.4 创建限流注解

创建限流注解 @RateLimit,这样设置限流只对添加了当前注解的方法生效。

 1import java.lang.annotation.ElementType;
 2import java.lang.annotation.Retention;
 3import java.lang.annotation.RetentionPolicy;
 4import java.lang.annotation.Target;
 5
 6/**
 7 * 限流注解
 8 *
 9 * @author mydlq
10 */
11@Target(ElementType.METHOD)
12@Retention(RetentionPolicy.RUNTIME)
13public @interface RateLimit {
14    /**
15     * 限流的key,如果不指定,则使用类名+方法名
16     */
17    String key() default "";
18
19    /**
20     * 限流时间(单位秒),即在多少时间范围内限制次数,默认60秒
21     */
22    int time() default 60;
23
24    /**
25     * 限制次数
26     */
27    int limit();
28}

5.5 创建限流 AOP 切面

创建限流 AOP 切面,用于拦截限流注解的方法,并执行限流逻辑。

 1import io.micrometer.common.util.StringUtils;
 2import jakarta.annotation.Resource;
 3import org.aspectj.lang.ProceedingJoinPoint;
 4import org.aspectj.lang.annotation.Around;
 5import org.aspectj.lang.annotation.Aspect;
 6import org.aspectj.lang.reflect.MethodSignature;
 7import org.springframework.http.HttpStatus;
 8import org.springframework.http.ResponseEntity;
 9import org.springframework.stereotype.Component;
10
11/**
12 * 限流 Aspect
13 *
14 * @author mydlq
15 */
16@Aspect
17@Component
18public class RateLimitAspect {
19
20    /**
21     * Redis Key 前缀
22     */
23    private static final String OPEN_RATE_LIMIT = "rate_limit:%s:%s";
24
25    @Resource
26    private SlidingWindowRateLimiter slidingWindowRateLimiter;
27
28    @Around("@annotation(rateLimit)")
29    public Object rateLimitAdvice(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
30        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
31        // 获取类名
32        String className = signature.getDeclaringType().getSimpleName();
33        // 获取方法名
34        String methodName = signature.getName();
35
36        // 设置限流KEY,如果设置了KEY,则使用设置的KEY,否则使用【类名+方法名】作为KEY
37        String key = StringUtils.isBlank(rateLimit.key()) ?
38                OPEN_RATE_LIMIT + className + "_" + methodName :
39                rateLimit.key();
40
41        // 判断是否达到限流阈值,是则抛出限流异常
42        boolean allowRequest = slidingWindowRateLimiter.allowRequest(key, rateLimit.time(), rateLimit.limit());
43        if (!allowRequest) {
44            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁,请稍后再试");
45        }
46
47        return joinPoint.proceed();
48    }
49
50}

5.6 创建滑动窗口限流 Lua 脚本

/resorces/redis 目录下创建 slidingWindow.lua 脚本,用于实现滑动窗口限流。

 1-- ====== 滑动时间窗口计算 Lua 脚本 ======
 2-- KEYS[1]: zset集合Key名称
 3-- ARGV[1]: 当前时间
 4-- ARGV[2]: 窗口长度
 5
 6-- 参数设置
 7local key = KEYS[1]
 8local current_time = tonumber(ARGV[1])
 9local window_length = tonumber(ARGV[2])
10local threshold = tonumber(ARGV[3])
11-- 计算窗口开始时间
12local window_start = current_time - window_length
13-- 移除窗口时间之前的全部请求次数记录
14redis.call('zremrangebyscore', key, '-inf', window_start)
15-- 获取当前窗口内请求次数
16local current_requests = redis.call('zcard', key)
17-- 如果当前窗口内请求次数小于阈值,则添加当前时间到窗口内并返回1,否则返回0
18if current_requests < threshold then
19    -- 添加当前请求到窗口内,并且设置score和value都为当前时间
20    redis.call('zadd', key, current_time, current_time)
21    -- 设置过期时间为1小时(如果设置KEY永不过期,则可以将下面的命令删掉)
22    redis.call('expire', key, 3600)
23    return 1
24else
25    return 0
26end

5.7 创建滑动窗口限流类

创建滑动窗口限流类,用于执行 Lua 脚本,实现滑动窗口限流。

 1import jakarta.annotation.Resource;
 2import org.springframework.core.io.ResourceLoader;
 3import org.springframework.data.redis.core.RedisTemplate;
 4import org.springframework.data.redis.core.script.DefaultRedisScript;
 5import org.springframework.stereotype.Component;
 6import java.util.Collections;
 7import java.util.concurrent.TimeUnit;
 8
 9/**
10 * 时间窗口算法实现的限流器
11 *
12 * @author mydlq
13 */
14@Component
15public class SlidingWindowRateLimiter {
16
17    @Resource
18    private ResourceLoader resourceLoader;
19    @Resource(name = "redisLimitTemplate")
20    private RedisTemplate<String, Object> redisTemplate;
21
22    /**
23     * 通过滑动窗口算法实现限流
24     *
25     * @param key   KEY
26     * @param limit 指定时间内允许的请求次数
27     * @param time  时间窗口大小,单位秒
28     * @return true 表示通过,false 表示被限流
29     */
30    public boolean allowRequest(String key, int time, int limit) {
31        // 当前时间戳
32        String currentTime = String.valueOf(System.currentTimeMillis());
33        // 窗口间隔
34        String preTime = String.valueOf(TimeUnit.SECONDS.toMillis(time));
35        // 限制次数
36        String limitNumber = String.valueOf(limit);
37
38        // 创建 DefaultRedisScript 实例
39        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
40        script.setResultType(Long.class);
41        script.setLocation(resourceLoader.getResource("classpath:/redis/slidingWindow.lua"));
42
43        // 执行 Lua 脚本
44        Long result = redisTemplate.execute(script, Collections.singletonList(key), currentTime, preTime, limitNumber);
45
46        // 判断结果,如果结果为null或者1,则表示通过
47        return result == null || result == 1L;
48    }
49
50}

5.8 创建限流测试 Controller 类

创建测试的 Controller 类,用于后续对限流进行测试。

 1import club.mydlq.common.RateLimit;
 2import org.springframework.http.ResponseEntity;
 3import org.springframework.web.bind.annotation.GetMapping;
 4import org.springframework.web.bind.annotation.RequestMapping;
 5import org.springframework.web.bind.annotation.RestController;
 6
 7/**
 8 * 限流测试
 9 *
10 * @author mydlq
11 */
12@RestController
13@RequestMapping("/test")
14public class LimitTestController {
15
16    @RateLimit(key = "test-key", time = 10, limit = 5)
17    @GetMapping("/query")
18    public ResponseEntity<String> testLimit() {
19        return ResponseEntity.ok("请求成功");
20    }
21
22}

5.9 测试限流效果

在上面我们创建了一个用于测试的 Controller 类,提拱了一个限流的接口 testLimit,并且使用 @RateLimit 注解设置了限流的时间为 10 秒,限流的次数为 5 次。

接下来我们启动 Spring Boot 项目,访问 http://localhost:8080/test/query 接口,连续 5 次访问,可以看到返回结果都为 请求成功,然后再次访问,可以看到返回结果为 请求过于频繁,请稍后再试,说明限流生效。


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。