iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0

前言

昨天我們已經在 SpringBoot 專案內完成了 Redis Client 的基本配置,今天實際用固定窗口計數器來整合 Redis 分散式儲存限流計數的功能,並且建立一個測試用的 Dockerfile 跟新的 docker-compose.yaml 來在容器內運行起兩個實例,測試看看分散式的環境下,是否可以共享 Redis 的狀態、做到統一限流。

實作 Lab

首先我們先新增一個用 Redis 實作的計數器,由於之前已經有一個用 ConcurrentHashMap 實作的計數器了,它們都會 implements RateLimiterStorage 在 Spring 容器的要求下需要在 @Component 為他們做顯式命名,不然到時限流 class 要注入時只會注入介面,不知道會使用到哪個計數器:

@Component("redisRateLimiterStorage")
@RequiredArgsConstructor
public class RedisRateLimiterStorage implements RateLimiterStorage {

    private final RedisHelper redisHelper;

    @Override
    public long incrementAndSetExpire(String key, long expireSeconds) {
        return redisHelper.incrementWithCustomTTL(RedisKey.RATE_LIMITER.getValue(), expireSeconds, key);
    }

    // 略...

}

在這之前我已經先做 RedisHelper 工具類新增一個方法以供使用:

@Component
@RequiredArgsConstructor
public class RedisHelper {

    private final RedisTemplate<String, Object> redisTemplate;

    public long incrementWithCustomTTL(String cacheName, long ttl, Object... keys) {
        var key = getCacheKey(cacheName, keys);
        var count = redisTemplate.opsForValue().increment(key);

        if (count != null && count == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(ttl));
        }
        return count != null ? count : 0L;
    }

    private String getCacheKey(String cacheName, Object... args) {
        if (args == null || args.length == 0) {
            return cacheName;
        }
        return cacheName.concat("::").concat(StringUtils.join(args, ":"));
    }
}

FixedWindowLimiter

這邊要注意,如果要使用 @Qualifier("redisRateLimiterStorage") 指定要注入的實作類,就只能用建構子注入:

@Component
public class FixedWindowLimiter implements RateLimiterStrategy {

    private final RateLimiterStorage storage;

    public FixedWindowLimiter(@Qualifier("redisRateLimiterStorage") RateLimiterStorage storage) {
        this.storage = storage;
    }

    @Override
    public boolean isAllow(String key, RateLimiter rateLimiter) {
        var currentCount = storage.incrementAndSetExpire(key, rateLimiter.window());
        return currentCount <= rateLimiter.limit();
    }

    @Override
    public Algorithm getAlgorithmType() {
        return Algorithm.FIXED_WINDOW;
    }
}

測試:Dockerfile & docker-compose.yaml for Test

因為我們要使用 docker-compose 來啟動 docker-image,所以必須先把我們自己的服務 build 成 docker-image,為此需要寫一個 Dockerfile:

FROM openjdk:17-jdk-slim
WORKDIR /app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN chmod +x ./mvnw
RUN ./mvnw dependency:go-offline -B
COPY src ./src
RUN ./mvnw clean package -DskipTests
EXPOSE 8080
CMD ["java", "-jar", "target/playground-module-0.0.1-SNAPSHOT.jar"]

接著才能用 docker-compose 拉 image 來使用:

services:
  app1:
    build: ..
    container_name: test-app1
    environment:
      - SPRING_PROFILES_ACTIVE=test
      - REDIS_HOST=host.docker.internal
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD}
      - SERVER_PORT=8080
    ports:
      - "8081:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

  app2:
    build: ..
    container_name: test-app2
    environment:
      - SPRING_PROFILES_ACTIVE=test
      - REDIS_HOST=host.docker.internal
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD}
      - SERVER_PORT=8080
    ports:
      - "8082:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

如此一來就有兩個實例被運行起來了,分別是 8081 和 8082:

http://localhost:8081/rate/window/fixed
http://localhost:8082/rate/window/fixed

輪流 call 這兩隻 api 會發現它們是對同一個 Redis 裡面的資料結構做操作:

81 先 call → 計數 +1 = 1

82 再 call → 計數 +1 = 2

.

.

.

兩個合計訪問到第六次就會達到上限了,這樣就完成最基本的分散式限流架構。

總結

  • 只挑一個限流器實作就好,不在細節上花太多時間,之後再把它們用現有的 Library 抽象起來。
  • 架構面的東西才是應該要 focus 的。

上一篇
Day 8 | 限流器實作:在 SpringBoot 系統中配置 Redis Client
下一篇
Day 10 | 推送系統實作前導&十日回顧
系列文
系統設計一招一式:最基本的功練到爛熟就是殺手鐧,從單體架構到分布式系統的 Lab 實作筆記11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言