ホームページ >データベース >Redis >SpringBoot + Redis を使用してインターフェイス電流制限を実装する方法

SpringBoot + Redis を使用してインターフェイス電流制限を実装する方法

PHPz
PHPz転載
2023-05-27 15:01:191634ブラウズ

構成

まず、Spring Boot プロジェクトを作成し、Web と Redis の依存関係を導入します。また、インターフェイスの電流制限は一般にアノテーションによってマークされ、アノテーションは AOP を通じて解析されることを考慮します。そのため、AOP の依存関係も含める必要があります。 , 最終的な依存関係は次のとおりです:

<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>

次に、事前に Redis インスタンスを準備します。プロジェクトが構成されたら、次のように Redis の基本情報を直接構成できます:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

Current制限アノテーション

次に、電流制限アノテーションを作成します。電流制限を 2 つの状況に分けます:

  • 現在のインターフェイス (たとえば、インターフェイス) に対するグローバル電流制限1分間に100回アクセス可能。

  • 特定の IP アドレスのレート制限。たとえば、ある IP アドレスには 1 分間に 100 回アクセスできます。

これら 2 つの状況では、列挙型クラスを作成します:

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

次に、電流制限アノテーションを作成します:

@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;
}

最初のパラメータ current-制限キー。これは単なるプレフィックスです。将来的には、完全なキーはこのプレフィックスとインターフェイス メソッドの完全なパスになり、これらが合わせて電流制限キーを形成します。このキーは Redis に保存されます。

他の 3 つのパラメーターは理解するのが簡単なので、これ以上は説明しません。

わかりました。今後インターフェースでフローを制限する必要がある場合は、そのインターフェースに @RateLimiter アノテーションを追加して、関連するパラメーターを構成するだけです。

カスタマイズされた RedisTemplate

Spring Boot では、実際には Spring Data Redis を使用して Redis を操作することに慣れていますが、デフォルトの RedisTemplate には小さな落とし穴があります。それは、シリアル化に JdkSerializationRedisSerializer が使用されるということです。わかりませんが、皆さん、このシリアル化ツールを使用して Redis に保存されたキーと値にはどういうわけかプレフィックスが多くなり、コマンドで読み取るときにエラーが発生する可能性があることに気づいたことがありますか。

たとえば、保存する場合はキーがname、値がtestですが、コマンドラインで操作する場合、get nameでは目的のデータを取得できません。データが保存されます Redis に到着すると、名前の前にさらにいくつかの文字が追加されますが、現時点では 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;
    }
}

これについては実際には何も言うことはありません。両方のキーに対して Spring Boot のデフォルトの jackson シリアル化メソッドを使用します。と値です。

Lua スクリプト

実際、これについては前の vhr ビデオで説明しました。Lua スクリプトを使用して、Redis でいくつかのアトミック操作を実装できます。Lua スクリプトを呼び出したい場合は、2 つあります。さまざまなアイデア:

  • Redis サーバー上で Lua スクリプトを定義し、ハッシュ値を計算します。Java コードでは、このハッシュ値を使用してどの Lua を実行するかをロックします。

  • Lua スクリプトを Java コード内で直接定義し、実行のために Redis サーバーに送信します。

Spring Data Redis では Lua スクリプトを操作するためのインターフェースも提供されており、非常に便利なので、ここでは 2 番目のソリューションを採用します。

特に 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)

このスクリプトは実際には難しくありません。一目見るために使用されます。 KEYS と ARGV はどちらも呼び出し時に渡されるパラメータです。Tonumber は文字列を数値に変換します。redis.call は特定の Redis 命令を実行します。具体的なプロセスは次のとおりです:

  • まず、受信キーと現在の制限回数と時間を取得します。

  • get を通じてこのキーに対応する値を取得します。この値は、現在の時間枠内でこのインターフェイスにアクセスできる回数です。

  • 最初の訪問の場合、この時点で得られる結果は nil です。それ以外の場合、得られる結果は数値になるはずです。そのため、次のステップでは、得られた結果が A であるかどうかを判断します。この数値が count より大きい場合は、トラフィック制限を超えていることを意味し、クエリ結果を直接返すことができます。

  • 得られた結果が nil の場合は初回アクセスなので、現在のキーを 1 増やして有効期限を設定します。

  • 最後に、1 ずつ増分した値を返すだけです。

実際、この Lua スクリプトは理解しやすいです。

次に、次のように、この Lua スクリプトを Bean にロードします。

@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;
}

さて、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();
    }
}

このアスペクトは、すべてをインターセプトします。追加 @RateLimiter アノテーション付きメソッドが追加され、アノテーションは事前通知で処理されます。

  • 首先获取到注解中的 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();
    }
}

每一个 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;
    }
}

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

SpringBoot + Redis を使用してインターフェイス電流制限を実装する方法

以上がSpringBoot + Redis を使用してインターフェイス電流制限を実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。