Heim > Datenbank > Redis > Hauptteil

So implementieren Sie mit SpringBoot + Redis eine Schnittstellenstrombegrenzung

PHPz
Freigeben: 2023-05-27 15:01:19
nach vorne
1618 Leute haben es durchsucht

Konfiguration

Zuerst erstellen wir ein Spring Boot-Projekt, führen Web- und Redis-Abhängigkeiten ein und berücksichtigen, dass die Strombegrenzung der Schnittstelle im Allgemeinen durch Anmerkungen gekennzeichnet ist und Anmerkungen über AOP analysiert werden Wir brauchen auch So fügen Sie AOP-Abhängigkeiten hinzu:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</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-aop</artifactId>
</dependency>
Nach dem Login kopieren

Bereiten Sie dann im Voraus eine Redis-Instanz vor. Nachdem unser Projekt konfiguriert ist, können wir die grundlegenden Informationen von Redis wie folgt konfigurieren 🎜#

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123
Nach dem Login kopieren

Anmerkung zur Strombegrenzung

Als nächstes erstellen wir eine Anmerkung zur Strombegrenzung #Globales aktuelles Limit für die aktuelle Schnittstelle, zum Beispiel kann auf die Schnittstelle 100 Mal in 1 Minute zugegriffen werden.

  • Aktuelles Limit für eine bestimmte IP-Adresse, zum Beispiel kann auf eine IP-Adresse 100 Mal in 1 Minute zugegriffen werden.

  • Für diese beiden Situationen erstellen wir eine Aufzählungsklasse:

    public enum LimitType {
        /**
         * 默认策略全局限流
         */
        DEFAULT,
        /**
         * 根据请求者IP进行限流
         */
        IP
    }
    Nach dem Login kopieren

    Als nächstes erstellen wir eine Strombegrenzungsannotation: #🎜🎜 #
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RateLimiter {
        /**
         * 限流key
         */
        String key() default "rate_limit:";
    
        /**
         * 限流时间,单位秒
         */
        int time() default 60;
    
        /**
         * 限流次数
         */
        int count() default 100;
    
        /**
         * 限流类型
         */
        LimitType limitType() default LimitType.DEFAULT;
    }
    Nach dem Login kopieren
    # 🎜🎜#Der erste Parameter ist der aktuelle Begrenzungsschlüssel. Der vollständige Schlüssel wird in Zukunft aus diesem Präfix und dem vollständigen Pfad der Schnittstellenmethode bestehen, die zusammen den aktuellen Begrenzungsschlüssel bilden in Redis.
Die anderen drei Parameter sind leicht zu verstehen, daher werde ich nicht mehr sagen.

Okay, welche Schnittstelle auch immer den Fluss in Zukunft begrenzen muss, fügen Sie einfach die Annotation @RateLimiter zu dieser Schnittstelle hinzu und konfigurieren Sie dann die relevanten Parameter.

Angepasstes RedisTemplate

In Spring Boot sind wir eigentlich eher daran gewöhnt, Spring Data Redis zum Betreiben von Redis zu verwenden, aber das Standard-RedisTemplate hat eine kleine Gefahr, das heißt, es ist so Wird für die Serialisierung verwendet. Ich weiß nicht, ob Ihnen aufgefallen ist, dass die Schlüssel und Werte aus unerklärlichen Gründen stärker vorangestellt werden, wenn Sie dieses Serialisierungstool in Zukunft direkt zum Speichern von Schlüsseln und Werten verwenden. Dies kann zu Fehlern führen, wenn Sie sie mit Befehlen lesen.

Beim Speichern lautet der Schlüssel beispielsweise „Name“ und der Wert „Test“, aber wenn Sie in der Befehlszeile arbeiten, kann get name die gewünschten Daten nicht abrufen Der Grund dafür ist, dass nach dem Speichern in Redis einige weitere Zeichen vor dem Namen stehen. Derzeit können wir RedisTemplate nur zum Auslesen verwenden.

@RateLimiter 注解,然后配置相关参数即可。

定制 RedisTemplate

在 Spring Boot 中,我们其实更习惯使用 Spring Data Redis 来操作 Redis,不过默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,不知道小伙伴们有没有注意过,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错。

例如存储的时候,key 是 name,value 是 test,但是当你在命令行操作的时候,get name 却获取不到你想要的数据,原因就是存到 redis 之后 name 前面多了一些字符,此时只能继续使用 RedisTemplate 将之读取出来。

我们用 Redis 做限流会用到 Lua 脚本,使用 Lua 脚本的时候,就会出现上面说的这种情况,所以我们需要修改 RedisTemplate 的序列化方案。

可能有小伙伴会说为什么不用 StringRedisTemplate 呢?StringRedisTemplate 确实不存在上面所说的问题,但是它能够存储的数据类型不够丰富,所以这里不考虑。

修改 RedisTemplate 序列化方案,代码如下:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}
Nach dem Login kopieren

这个其实也没啥好说的,key 和 value 我们都使用 Spring Boot 中默认的 jackson 序列化方式来解决。

Lua 脚本

这个其实我在之前 vhr 那一套视频中讲过,Redis 中的一些原子操作我们可以借助 Lua 脚本来实现,想要调用 Lua 脚本,我们有两种不同的思路:

  • 在 Redis 服务端定义好 Lua 脚本,然后计算出来一个散列值,在 Java 代码中,通过这个散列值锁定要执行哪个 Lua 脚本。

  • 直接在 Java 代码中将 Lua 脚本定义好,然后发送到 Redis 服务端去执行。

Spring Data Redis 中也提供了操作 Lua 脚本的接口,还是比较方便的,所以我们这里就采用第二种方案。

我们在 resources 目录下新建 lua 文件夹专门用来存放 lua 脚本,脚本内容如下:

local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call(&#39;get&#39;, key)
if current and tonumber(current) > count then
    return tonumber(current)
end
current = redis.call(&#39;incr&#39;, key)
if tonumber(current) == 1 then
    redis.call(&#39;expire&#39;, key, time)
end
return tonumber(current)
Nach dem Login kopieren

这个脚本其实不难,大概瞅一眼就知道干啥用的。KEYS 和 ARGV 都是一会调用时候传进来的参数,tonumber 就是把字符串转为数字,redis.call 就是执行具体的 redis 指令,具体流程是这样:

  • 首先获取到传进来的 key 以及 限流的 count 和时间 time。

  • 通过 get 获取到这个 key 对应的值,这个值就是当前时间窗内这个接口可以访问多少次。

  • 如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那就说明已经超过流量限制了,那么直接返回查询的结果即可。

  • 如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。

  • 最后把自增 1 后的值返回就可以了。

其实这段 Lua 脚本很好理解。

接下来我们在一个 Bean 中来加载这段 Lua 脚本,如下:

@Bean
public DefaultRedisScript<Long> limitScript() {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
    redisScript.setResultType(Long.class);
    return redisScript;
}
Nach dem Login kopieren

可以啦,我们的 Lua 脚本现在就准备好了。

注解解析

接下来我们就需要自定义切面,来解析这个注解了,我们来看看切面的定义:

@Aspect
@Component
public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number==null || number.intValue() > count) {
                throw new ServiceException("访问过于频繁,请稍候再试");
            }
            log.info("限制请求&#39;{}&#39;,当前请求&#39;{}&#39;,缓存key&#39;{}&#39;", count, number.intValue(), key);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}
Nach dem Login kopieren

这个切面就是拦截所有加了 @RateLimiter Wenn wir Redis zur Strombegrenzung verwenden, verwenden wir Lua-Skripte. Bei der Verwendung von Lua-Skripten tritt die oben erwähnte Situation auf, daher müssen wir das Serialisierungsschema von RedisTemplate ändern.

#🎜🎜#Einige Freunde fragen sich vielleicht, warum nicht StringRedisTemplate verwendet werden? StringRedisTemplate weist die oben genannten Probleme nicht auf, aber die Datentypen, die es speichern kann, sind nicht umfangreich genug, sodass es hier nicht berücksichtigt wird. #🎜🎜#
#🎜🎜#Ändern Sie das RedisTemplate-Serialisierungsschema. Der Code lautet wie folgt: #🎜🎜#
@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}
Nach dem Login kopieren
Nach dem Login kopieren
#🎜🎜#Dazu gibt es eigentlich nichts zu sagen, wir alle verwenden im Frühjahr den Standard-Jackson Booten für Schlüssel und Wert. Durch Serialisierung gelöst. #🎜🎜##🎜🎜#Lua-Skript#🎜🎜##🎜🎜#Tatsächlich habe ich dies im vorherigen VHR-Video erwähnt. Wenn Sie möchten, können wir einige atomare Operationen in Redis implementieren Lua-Skripte, wir haben zwei verschiedene Ideen: #🎜🎜##🎜🎜##🎜🎜##🎜🎜# Definieren Sie das Lua-Skript auf dem Redis-Server und berechnen Sie dann einen Hash-Wert im Java-Code Der Wert sperrt, welches Lua-Skript ausgeführt werden soll. #🎜🎜##🎜🎜##🎜🎜##🎜🎜# Definieren Sie das Lua-Skript direkt im Java-Code und senden Sie es dann zur Ausführung an den Redis-Server. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Spring Data Redis bietet auch eine Schnittstelle zum Betrieb von Lua-Skripten, was sehr praktisch ist, daher werden wir hier die zweite Option übernehmen. #🎜🎜##🎜🎜#Wir erstellen einen neuen Lua-Ordner speziell zum Speichern von Lua-Skripten: #🎜🎜#
@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}
Nach dem Login kopieren
Nach dem Login kopieren
#🎜🎜#Dieses Skript ist eigentlich nicht schwierig kann wahrscheinlich auf den ersten Blick erkennen, was es bewirkt. KEYS und ARGV sind beide Parameter, die beim Aufruf von Tonumber übergeben werden, um eine Zeichenfolge in eine Zahl umzuwandeln. Der spezifische Vorgang ist wie folgt: #🎜🎜##🎜🎜##🎜🎜# #🎜🎜#Erhalten Sie zunächst den eingehenden Schlüssel sowie die aktuelle Limitanzahl und -zeit. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Der diesem Schlüssel entsprechende Wert wird über get abgerufen. Dieser Wert gibt an, wie oft innerhalb des aktuellen Zeitfensters auf diese Schnittstelle zugegriffen werden kann. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Wenn es sich um den ersten Besuch handelt, ist das zu diesem Zeitpunkt erhaltene Ergebnis Null, andernfalls sollte das erhaltene Ergebnis eine Zahl sein, daher wird als nächstes beurteilt, ob Das Ergebnis Erhalten ist eine Zahl, und diese Zahl ist größer als count, was bedeutet, dass das Verkehrslimit überschritten wurde, dann kann das Abfrageergebnis direkt zurückgegeben werden. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Wenn das erhaltene Ergebnis Null ist, bedeutet dies, dass es sich um den ersten Besuch handelt. Erhöhen Sie zu diesem Zeitpunkt den aktuellen Schlüssel um 1 und legen Sie eine Ablaufzeit fest. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Zum Schluss geben Sie einfach den um 1 erhöhten Wert zurück. #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Tatsächlich ist dieses Lua-Skript leicht zu verstehen. #🎜🎜##🎜🎜#Als nächstes laden wir dieses Lua-Skript wie folgt in ein Bean: #🎜🎜#rrreee#🎜🎜#Okay, unser Lua-Skript ist jetzt fertig. #🎜🎜##🎜🎜#Annotationsanalyse#🎜🎜##🎜🎜#Als nächstes müssen wir den Aspekt anpassen, um diese Annotation zu analysieren: #🎜🎜#rreee#🎜🎜# Dieser Aspekt besteht darin, alle mit @RateLimiter annotierten Methoden abzufangen und die Annotationen in der Vorabbenachrichtigung zu verarbeiten. #🎜🎜#
  • 首先获取到注解中的 key、time 以及 count 三个参数。

  • 获取一个组合的 key,所谓的组合的 key,就是在注解的 key 属性基础上,再加上方法的完整路径,如果是 IP 模式的话,就再加上 IP 地址。以 IP 模式为例,最终生成的 key 类似这样:rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello(如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)。

  • 将生成的 key 放到集合中。

  • 通过 redisTemplate.execute 方法取执行一个 Lua 脚本,第一个参数是脚本所封装的对象,第二个参数是 key,对应了脚本中的 KEYS,后面是可变长度的参数,对应了脚本中的 ARGV。

  • 判断 Lua 脚本执行后的结果是否超过 count,若超过则视为过载,抛出异常处理即可。

接口测试

接下来我们就进行接口的一个简单测试,如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

每一个 IP 地址,在 5 秒内只能访问 3 次。

这个自己手动刷新浏览器都能测试出来。

全局异常处理

由于过载的时候是抛异常出来,所以我们还需要一个全局异常处理器,如下:

@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

我将这句话重写成如下: 这个 demo 很小,所以我没有定义实体类,而是直接使用 Map 来返回 JSON。 最后我们看看过载时的测试效果:

So implementieren Sie mit SpringBoot + Redis eine Schnittstellenstrombegrenzung

Das obige ist der detaillierte Inhalt vonSo implementieren Sie mit SpringBoot + Redis eine Schnittstellenstrombegrenzung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:yisu.com
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!