Rate limiting is a common technique used to control the amount of traffic that a service or application receives. By limiting the rate of incoming requests, you can prevent overloading of your servers and ensure that your application runs smoothly.
Redisson is a Java client library for Redis, a popular in-memory data structure store. Redisson provides several features for rate limiting, including support for both global and per-client rate limiting.
In this blog post, we'll walk through how to implement rate limiting in Spring Boot using Redisson.
Setting up Redisson
Before we can start implementing rate limiting, we need to set up Redisson in our Spring Boot application. To do this, we'll add the Redisson dependency to our pom.xml
file:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
Next, we'll create a Redisson client bean in our Spring Boot configuration. This bean will be used to connect to our Redis server and perform Redis operations:
@Configuration
public class RedisConfig {
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
In this example, we're creating a Redisson client that connects to a Redis server running on localhost
on the default Redis port of 6379
. We're using the useSingleServer
method to configure the client to connect to a single Redis server.
Implementing rate limiting
Now that we have Redisson set up, we can start implementing rate limiting. Redisson provides two types of rate limiting: global and per-client.
Global rate limiting
Global rate limiting limits the rate of incoming requests for all clients. To implement global rate limiting in Redisson, we'll use a Redis RRateLimiter
object. Here's an example:
@RestController
public class ExampleController {
private final RRateLimiter rateLimiter;
public ExampleController(RedissonClient redissonClient) {
this.rateLimiter = redissonClient.getRateLimiter("my-rate-limiter");
// set rate limit to 3 requests per 1 Minute
rateLimiter.trySetRate(RateType.OVERALL, 3, 1, RateIntervalUnit.MINUTES);
}
@GetMapping("/example")
public String example() {
// acquire a permit before processing the request
boolean permitAcquired = rateLimiter.tryAcquire(1);
if (!permitAcquired) {
throw new TooManyRequestsException();
}
// process the request
return "Example response";
}
}
In this example, we're creating a RRateLimiter
object with the name "my-rate-limiter". We're setting the rate limit to 10 requests per second using the trySetRate
method. This sets the rate limit for all clients, since we're using the OVERALL
rate type.
In the example
method, we're using the tryAcquire
method to acquire a permit before processing the request. If the rate limit has been exceeded, this method will return false
, and we'll throw a TooManyRequestsException
.
Let's check what is happening in the redis
127.0.0.1:6379> keys *
1) "my-rate-limiter"
127.0.0.1:6379> type my-rate-limiter
hash
127.0.0.1:6379> HGETALL my-rate-limiter
1) "rate"
2) "3"
3) "interval"
4) "60000"
5) "type"
6) "0"
127.0.0.1:6379>
As soon as you hit the first endpoint, two new keys are created value and permits, You can check their types and values with these commands.
keys *
1) "{my-rate-limiter}:value"
2) "{my-rate-limiter}:permits"
3) "my-rate-limiter"
127.0.0.1:6379> type {my-rate-limiter}:value
string
127.0.0.1:6379> type {my-rate-limiter}:permits
zset
127.0.0.1:6379> ZRANGE {my-rate-limiter}:permits 0 -1 WITHSCORES
1) "mP'^\x01\x00\x00\x00"
2) "1677392274348"
3) "\xa7\xeeS\\\x01\x00\x00\x00"
4) "1677392494709"
5) "\x85G\x04\xde\x01\x00\x00\x00"
6) "1677392495237"
get {my-rate-limiter}:value
"0"
1677392274348 is the timestamp when the call was made, 26 February 2023 11:47:54.348 GMT+05:30
In a global rate limiting scenario, where you are limiting the number of requests across all clients or users, you may want to update the rate limiting properties dynamically, without having to restart the application. Redisson provides a way to achieve this by allowing you to update the configuration of the RRateLimiter
object at runtime.
@GetMapping("/api/update-rate-limiter")
public ResponseEntity<?> updateRateLimiter(@RequestParam int rate, @RequestParam int rateInterval) {
rateLimiter.setRate(RateType.OVERALL, rate, rateInterval, RateIntervalUnit.SECONDS);
return ResponseEntity.ok("Rate limiter updated" );
}
trySetRate - Initializes RateLimiter's state and stores config to Redis server.
setRate - Updates RateLimiter's state and stores config to Redis server.
trySetRate uses HSETNX redis command
This command sets field
in the hash stored at key
to value
, only if field
does not yet exist.
If field
already exists, this operation has no effect.
setRate uses HSET
redis command
This command overwrites the values of specified fields that exist in the hash. If key
doesn't exist, a new key holding a hash is created.
http://localhost:8080/api/update-rate-limiter?rate=10&rateInterval=1
127.0.0.1:6379> HGETALL my-rate-limiter
1) "rate"
2) "10"
3) "interval"
4) "1000"
5) "type"
6) "0"
You can check all the redis commands that redission is executing by enabling redis logs, For steps check this: https://til.hashnode.dev/enable-redis-logs
Per-client rate limiting
Per-client rate limiting limits the rate of incoming requests for each client individually. To implement per-client rate limiting in Redisson, we'll use a Redis RPerClientRateLimiter
object. Here's an example:
@RestController
public class ExampleController {
private final RRateLimiter rateLimiter;
public ExampleController(RedissonClient redissonClient) {
this.rateLimiter = redissonClient.getRateLimiter("my-rate-limiter");
// set rate limit to 10 requests per second per client
rateLimiter.trySetRate(RateType.PER_CLIENT, 10, 10, RateIntervalUnit.MINUTES);
}
@GetMapping("/example")
public String example() throws TooManyRequestsException {
// acquire a permit before processing the request
boolean permitAcquired = rateLimiter.tryAcquire(1);
if (!permitAcquired) {
throw new TooManyRequestsException();
}
// process the request
return "Example response left " + rateLimiter.availablePermits();
}
@GetMapping("/api/update-rate-limiter")
public ResponseEntity<?> updateRateLimiter(@RequestParam int rate, @RequestParam int rateInterval) {
rateLimiter.setRate(RateType.PER_CLIENT, rate, rateInterval, RateIntervalUnit.SECONDS);
return ResponseEntity.ok("Rate limiter update" );
}
}
In this example, we're creating a RPerClientRateLimiter
object using the getPerClientRateLimiter
method. We're setting the rate limit to 10 requests per minute per client using the trySetRate
method and the PER_CLIENT
rate type. In the example
method, we're using the tryAcquire
method to acquire a permit before processing the request. We're passing the client's IP address as the key to the tryAcquire
method, so the rate limit will be applied per client.
127.0.0.1:6379> HGETALL my-rate-limiter
1) "rate"
2) "10"
3) "interval"
4) "1000"
5) "type"
6) "1"
If you run two instances of the same application, and use the common rate limiter config
keys *
1) "{my-rate-limiter}:permits:0b68ca6e-1b46-49cd-9279-6d0b1313a09d"
2) "{my-rate-limiter}:permits:aec1fd29-75af-42b9-b2dc-98f239d86d38"
3) "{my-rate-limiter}:value:aec1fd29-75af-42b9-b2dc-98f239d86d38"
4) "{my-rate-limiter}:value:0b68ca6e-1b46-49cd-9279-6d0b1313a09d"
5) "my-rate-limiter"
Redisson's per-client rate limiting feature can be useful in a scenario where you have multiple instances of the same application running in parallel, and you want to add a smaller rate limit per instance. This can be useful in a distributed system where multiple instances of an application are processing requests concurrently and you want to limit the number of requests that each instance can handle.
Conclusion
In this blog post,we learned how to implement rate limiting in Spring Boot using Redisson. We've covered both global and per-client rate limiting. By using rate limiting, you can prevent overloading of your servers and ensure that your application runs smoothly, even during periods of high traffic. Redisson provides a simple and powerful way to implement rate limiting in your Spring Boot applications.
If you want to implement a basic rate limiter in redis check this
Check the code for this example: https://github.com/nkalra0123/rate-limiting
For part 2 check this: https://til.hashnode.dev/rate-limiting-in-spring-boot-with-redisson-part-2
If you liked this blog, you can follow me on twitter, and learn something new with me.