Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách triển khai giới hạn tốc độ trong một dịch vụ được mở rộng quy mô.
Chúng tôi sẽ sử dụng thư viện Bucket4J để triển khai nó và chúng tôi sẽ sử dụng Redis làm bộ nhớ đệm phân tán.
Tại sao nên sử dụng giới hạn tỷ lệ?
Hãy bắt đầu với một số điều cơ bản để đảm bảo chúng ta hiểu được nhu cầu giới hạn tỷ lệ và giới thiệu các công cụ mà chúng ta sẽ sử dụng trong hướng dẫn này.
Vấn đề với mức giá không giới hạn
Nếu một API công khai như API Twitter cho phép người dùng thực hiện số lượng yêu cầu không giới hạn mỗi giờ thì điều đó có thể dẫn đến:
- cạn kiệt tài nguyên
- chất lượng dịch vụ ngày càng giảm
- tấn công từ chối dịch vụ
Điều này có thể dẫn đến tình huống dịch vụ không khả dụng hoặc chậm . Nó cũng có thể dẫn đến nhiều chi phí không mong muốn do dịch vụ phát sinh.
Việc giới hạn tỷ lệ giúp ích như thế nào
Thứ nhất, giới hạn tốc độ có thể ngăn chặn các cuộc tấn công từ chối dịch vụ. Khi kết hợp với cơ chế chống trùng lặp hoặc khóa API, việc giới hạn tốc độ cũng có thể giúp ngăn chặn các cuộc tấn công từ chối dịch vụ phân tán.
Thứ hai, nó giúp ước tính lưu lượng truy cập. Điều này rất quan trọng đối với các API công khai. Điều này cũng có thể được kết hợp với các tập lệnh tự động để giám sát và mở rộng quy mô dịch vụ.
Và thứ ba, bạn có thể sử dụng nó để triển khai định giá theo cấp độ. Loại mô hình định giá này có nghĩa là người dùng có thể trả tiền cho tỷ lệ yêu cầu cao hơn. API Twitter là một ví dụ về điều này.
Thuật toán nhóm mã thông báo
Nhóm mã thông báo là một thuật toán mà bạn có thể sử dụng để triển khai giới hạn tỷ lệ. Nói tóm lại, nó hoạt động như sau:
- Một nhóm được tạo với dung lượng nhất định (số lượng mã thông báo).
- Khi có yêu cầu đến, nhóm sẽ được chọn. Nếu có đủ dung lượng, yêu cầu được phép tiếp tục. Nếu không, yêu cầu sẽ bị từ chối.
- Khi một yêu cầu được cho phép, dung lượng sẽ giảm.
- Sau một khoảng thời gian nhất định, dung lượng sẽ được bổ sung.
Cách triển khai nhóm mã thông báo trong hệ thống phân tán
Để triển khai thuật toán nhóm mã thông báo trong hệ thống phân tán, chúng tôi cần sử dụng bộ đệm được phân phối .
Bộ nhớ đệm là kho lưu trữ khóa-giá trị để lưu trữ thông tin xô. Chúng tôi sẽ sử dụng bộ đệm Redis để triển khai việc này.
Trong nội bộ, Bucket4j cho phép chúng tôi triển khai bất kỳ triển khai nào của API Java JCache. Ứng dụng khách Redisson của Redis là triển khai mà chúng tôi sẽ sử dụng.
Triển khai dự án
Chúng tôi sẽ sử dụng khung Spring Boot để xây dựng dịch vụ của mình.
Dịch vụ của chúng tôi sẽ chứa các thành phần dưới đây:
- Một API REST đơn giản.
- Bộ đệm Redis được kết nối với dịch vụ – sử dụng ứng dụng khách Redisson.
- Thư viện Bucket4J bao quanh API REST.
- Chúng tôi sẽ kết nối Bucket4J với giao diện JCache sẽ sử dụng ứng dụng khách Redisson làm phương thức triển khai ở chế độ nền.
Đầu tiên, chúng ta sẽ tìm hiểu cách xếp hạng giới hạn API cho tất cả các yêu cầu. Sau đó, chúng ta sẽ tìm hiểu cách triển khai cơ chế giới hạn tỷ lệ phức tạp hơn cho mỗi người dùng hoặc theo từng mức giá.
Hãy bắt đầu với việc thiết lập dự án.
Cài đặt phụ thuộc
Hãy thêm các phần phụ thuộc bên dưới vào pom.xml của chúng tôi (hoặc build.gradle ) tập tin.
<dependencies>
<!-- To build the Rest API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redisson Starter = Spring Data Redis starter(excluding other clients) and Redisson client -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
<!-- Bucket4J starter = Bucket4J + JCache -->
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
<version>0.5.2</version>
</dependency>
</dependencies>
Cấu hình bộ đệm
Đầu tiên, chúng ta cần khởi động máy chủ Redis của mình. Giả sử chúng ta có máy chủ Redis chạy trên cổng 6379 trên máy cục bộ.
Chúng ta cần thực hiện hai bước:
- Tạo kết nối tới máy chủ này từ ứng dụng của chúng tôi.
- Thiết lập JCache để sử dụng ứng dụng khách Redisson làm phương pháp triển khai.
Tài liệu của Redisson cung cấp các bước ngắn gọn để triển khai điều này trong một ứng dụng Java thông thường. Chúng ta sẽ thực hiện các bước tương tự nhưng trong Spring Boot.
Trước tiên hãy nhìn vào mã. Chúng ta cần tạo một lớp Cấu hình để tạo các đậu cần thiết.
@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();
cacheManager.createCache("cache", RedissonConfiguration.fromConfig(config));
return cacheManager;
}
@Bean
ProxyManager<String> proxyManager(CacheManager cacheManager) {
return new JCacheProxyManager<>(cacheManager.getCache("cache"));
}
}
Cái này có tác dụng gì?
- Tạo đối tượng cấu hình mà chúng ta có thể sử dụng để tạo kết nối.
- Tạo trình quản lý bộ đệm bằng đối tượng cấu hình. Điều này sẽ tạo một kết nối nội bộ đến phiên bản Redis và tạo một hàm băm có tên là "cache" trên đó.
- Tạo trình quản lý proxy sẽ được sử dụng để truy cập bộ đệm. Bất kể ứng dụng của chúng tôi cố gắng lưu vào bộ đệm bằng API JCache, nó sẽ được lưu vào bộ nhớ đệm trên phiên bản Redis bên trong hàm băm có tên "cache".
Xây dựng API
Hãy tạo một API REST đơn giản.
@RestController
public class RateLimitController {
@GetMapping("/user/{id}")
public String getInfo(@PathVariable("id") String id) {
return "Hello " + id;
}
}
Nếu tôi nhấn API bằng URL http://localhost:8080/user/1 , tôi sẽ nhận được phản hồi Hello 1 .
Cấu hình Bucket4J
Để thực hiện giới hạn tốc độ, chúng ta cần định cấu hình Bucket4J. Rất may, chúng tôi không cần phải viết bất kỳ mã soạn sẵn nào do có thư viện khởi động.
Nó cũng tự động phát hiện hạt ProxyManager chúng tôi đã tạo ở bước trước và sử dụng nó để lưu vào bộ nhớ đệm.
Những gì chúng ta cần làm là định cấu hình thư viện này xung quanh API mà chúng ta đã tạo.
Một lần nữa, có nhiều cách để làm điều này.
Chúng ta có thể sử dụng cấu hình dựa trên thuộc tính được xác định trong thư viện khởi động.
Đây là cách thuận tiện nhất cho các trường hợp đơn giản như giới hạn tỷ lệ cho tất cả người dùng hoặc tất cả người dùng khách.
Tuy nhiên, nếu chúng ta muốn triển khai điều gì đó phức tạp hơn như giới hạn tốc độ cho mỗi người dùng, thì tốt hơn hết bạn nên viết mã tùy chỉnh cho nó.
Chúng tôi sẽ triển khai giới hạn tỷ lệ cho mỗi người dùng. Giả sử chúng ta có giới hạn tốc độ cho mỗi người dùng được lưu trữ trong cơ sở dữ liệu và chúng ta có thể truy vấn nó bằng id người dùng.
Hãy viết mã cho nó từng bước một.
Tạo nhóm
Trước khi bắt đầu, hãy xem cách tạo nhóm.
Refill refill = Refill.intervally(10, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, refill);
Bucket bucket = Bucket4j.builder()
.addLimit(limit)
.build();
- Nạp tiền – Sau bao lâu thì thùng sẽ đầy lại.
- Băng thông – Băng thông của nhóm có bao nhiêu. Về cơ bản, yêu cầu cho mỗi khoảng thời gian nạp lại.
- Nhóm – Một đối tượng được cấu hình bằng hai tham số này. Ngoài ra, nó còn duy trì một bộ đếm mã thông báo để theo dõi số lượng mã thông báo có sẵn trong nhóm.
Sử dụng phần này làm nền tảng, hãy thay đổi một số thứ để phù hợp với trường hợp sử dụng của chúng ta.
Tạo và lưu trữ bộ đệm bằng ProxyManager
Chúng tôi đã tạo trình quản lý proxy nhằm mục đích lưu trữ các nhóm trên Redis. Sau khi tạo một bộ chứa, bộ chứa đó cần được lưu vào bộ nhớ đệm trên Redis và không cần tạo lại.
Để thực hiện điều này, chúng tôi sẽ thay thế Bucket4j.builder() với proxyManager.builder() . ProxyManager sẽ đảm nhiệm việc lưu các nhóm vào bộ nhớ đệm và không tạo lại chúng.
Trình tạo của ProxyManager có hai tham số – một khóa dựa vào đó nhóm sẽ được lưu vào bộ nhớ đệm và đối tượng cấu hình mà nó sẽ sử dụng để tạo nhóm.
Hãy xem chúng tôi có thể triển khai nó như thế nào:
@Service
public class RateLimiter {
//autowiring dependencies
public Bucket resolveBucket(String key) {
Supplier<BucketConfiguration> configSupplier = getConfigSupplierForUser(key);
// Does not always create a new bucket, but instead returns the existing one if it exists.
return buckets.builder().build(key, configSupplier);
}
private Supplier<BucketConfiguration> getConfigSupplierForUser(String key) {
User user = userRepository.findById(userId);
Refill refill = Refill.intervally(user.getLimit(), Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(user.getLimit(), refill);
return () -> (BucketConfiguration.builder()
.addLimit(limit)
.build());
}
}
Chúng tôi đã tạo một phương thức trả về một nhóm cho khóa được cung cấp. Trong bước tiếp theo, chúng ta sẽ xem cách sử dụng tính năng này.
Cách sử dụng mã thông báo và thiết lập giới hạn tỷ lệ
Khi có yêu cầu, chúng tôi sẽ cố gắng sử dụng mã thông báo từ nhóm có liên quan.
Chúng ta sẽ sử dụng tryConsume() phương pháp của nhóm để thực hiện việc này.
@GetMapping("/user/{id}")
public String getInfo(@PathVariable("id") String id) {
// gets the bucket for the user
Bucket bucket = rateLimiter.resolveBucket(id);
// tries to consume a token from the bucket
if (bucket.tryConsume(1)) {
return "Hello " + id;
} else {
return "Rate limit exceeded";
}
}
tryConsume() phương thức trả về true nếu mã thông báo được sử dụng thành công hoặc false nếu mã thông báo không được tiêu thụ.
Cách kiểm tra dịch vụ của chúng tôi
Chúng tôi có thể kiểm tra điều này bằng bất kỳ kỹ thuật kiểm tra tự động nào. Ví dụ:chúng ta có thể sử dụng JUnit. Hãy viết một trường hợp thử nghiệm gọi getInfo() nhiều lần và xác minh rằng phản hồi là chính xác.
Giả sử chúng ta có một người dùng có id 1 và giới hạn là 10 yêu cầu mỗi phút. Giả sử chúng ta cũng có một người dùng có id 2 và giới hạn là 20 yêu cầu mỗi phút.
Chúng tôi sẽ đạt 11 yêu cầu cho cả hai người dùng và xác minh rằng yêu cầu đó không thành công đối với người dùng có id 1 nhưng thành công đối với người dùng có id 2 .
@Test
public void testGetInfo() {
// calls the method 10 times for user 1
for (int i = 0; i < 10; i++) {
rateLimiter.getInfo(1));
rateLimiter.getInfo(2));
}
// verifies that the response is rate limited for user 1
assertEquals("Rate limit exceeded", rateLimiter.getInfo(1));
// verifies that the response is successful for user 2
assertEquals("Hello 2", rateLimiter.getInfo(2));
}
Khi chạy thử nghiệm, chúng tôi sẽ thấy rằng thử nghiệm đã đạt.
Kết luận
Trong hướng dẫn này, chúng tôi đã đề cập đến cách tạo bộ giới hạn tốc độ bằng Bucket4j và Redis trong ứng dụng Spring Boot. Chúng tôi cũng đã xem xét cách thiết lập ứng dụng khách Redisson với JCache và cách sử dụng nó để lưu trữ các bộ đệm.
Cuối cùng, chúng tôi đã triển khai một công cụ giới hạn tốc độ đơn giản có thể dùng để xếp hạng các yêu cầu giới hạn cho những người dùng cụ thể.
Hy vọng bạn thích hướng dẫn này. Cảm ơn đã đọc!
Học cách viết mã miễn phí. Chương trình giảng dạy mã nguồn mở của freeCodeCamp đã giúp hơn 40.000 người có được việc làm với tư cách là nhà phát triển. Bắt đầu