Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

Rate Limiting แบบกระจายด้วย Bucket4J และ Redis: คู่มือ Spring Boot

ทำไมต้อง Rate Limiting?

API สาธารณะที่ไม่จำกัดอัตราคำขอ (unlimited rate) อาจนำไปสู่:

  • การหมดทรัพยากร (resource exhaustion)
  • คุณภาพบริการลดลง
  • การโจมตี DoS/DDoS
  • ค่าใช้จ่ายไม่คาดคิด

Rate limiting ช่วย: กัน DoS, ประมาณการ traffic, รองรับ tier-based pricing (จ่ายมากกว่า = rate สูงกว่า)

Token Bucket Algorithm

  1. สร้าง bucket พร้อมความจุ (tokens)
  2. คำขอเข้ามา → ตรวจสอบ bucket: พอ tokens? อนุญาต / ปฏิเสธ
  3. อนุญาต → ลด tokens
  4. หลังช่วงเวลา → เติม tokens (refill)

Token Bucket ในระบบกระจาย (Distributed)

ต้องใช้ distributed cache (Redis) เก็บข้อมูล bucket ใช้ Redisson client ร่วมกับ JCache API

การ Implement ใน Spring Boot

Dependencies (pom.xml)

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
  </dependency>
  <dependency>
    <groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
    <artifactId>bucket4j-spring-boot-starter</artifactId>
    <version>0.5.2</version>
  </dependency>
</dependencies>

Redis & JCache Configuration

@Configuration
public class RedisConfig {
    @Bean
    public Config config() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        return config;
    }

    @Bean
    public CacheManager cacheManager(Config config) {
        CacheManager manager = Caching.getCachingProvider().getCacheManager();
        manager.createCache("cache", RedissonConfiguration.fromConfig(config));
        return manager;
    }

    @Bean
    public ProxyManager<String> proxyManager(CacheManager cacheManager) {
        return new JCacheProxyManager<>(cacheManager.getCache("cache"));
    }
}

สร้าง REST API

@RestController
public class RateLimitController {
    @GetMapping("/user/{id}")
    public String getInfo(@PathVariable("id") String id) {
        return "Hello " + id;
    }
}

Bucket4J Configuration - Rate Limit ต่อ User

@Service
public class RateLimiter {
    private final ProxyManager<String> buckets;
    private final UserRepository userRepository;

    public RateLimiter(ProxyManager<String> buckets, UserRepository userRepository) {
        this.buckets = buckets;
        this.userRepository = userRepository;
    }

    public Bucket resolveBucket(String key) {
        Supplier<BucketConfiguration> configSupplier = getConfigSupplierForUser(key);
        return buckets.builder().build(key, configSupplier);
    }

    private Supplier<BucketConfiguration> getConfigSupplierForUser(String key) {
        User user = userRepository.findById(key);
        Refill refill = Refill.intervally(user.getLimit(), Duration.ofMinutes(1));
        Bandwidth limit = Bandwidth.classic(user.getLimit(), refill);
        return () -> BucketConfiguration.builder().addLimit(limit).build();
    }
}

Consume Tokens ใน Controller

@GetMapping("/user/{id}")
public String getInfo(@PathVariable("id") String id) {
    Bucket bucket = rateLimiter.resolveBucket(id);
    if (bucket.tryConsume(1)) {
        return "Hello " + id;
    } else {
        return "Rate limit exceeded";
    }
}

ทดสอบ (JUnit)

@Test
public void testGetInfo() {
    // user 1: limit 10/min, user 2: limit 20/min
    for (int i = 0; i < 10; i++) {
        assertEquals("Hello 1", rateLimiter.getInfo("1"));
        assertEquals("Hello 2", rateLimiter.getInfo("2"));
    }
    assertEquals("Rate limit exceeded", rateLimiter.getInfo("1"));
    assertEquals("Hello 2", rateLimiter.getInfo("2"));
}

สรุป

ใช้ Bucket4J + Redisson + Redis สร้าง rate limiter แบบกระจายได้ รองรับ limit ต่อ user, คงทน, ขยายระบบได้ โค้ดทดสอบผ่าน JUnit