ทำไมต้อง Rate Limiting?
API สาธารณะที่ไม่จำกัดอัตราคำขอ (unlimited rate) อาจนำไปสู่:
- การหมดทรัพยากร (resource exhaustion)
- คุณภาพบริการลดลง
- การโจมตี DoS/DDoS
- ค่าใช้จ่ายไม่คาดคิด
Rate limiting ช่วย: กัน DoS, ประมาณการ traffic, รองรับ tier-based pricing (จ่ายมากกว่า = rate สูงกว่า)
Token Bucket Algorithm
- สร้าง bucket พร้อมความจุ (tokens)
- คำขอเข้ามา → ตรวจสอบ bucket: พอ tokens? อนุญาต / ปฏิเสธ
- อนุญาต → ลด tokens
- หลังช่วงเวลา → เติม 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