The Spring Cloud project is part of the Spring ecosystem. It's a way for developers to easily implement services from patterns that were already implemented in a cloud-native mindset that's scalable, resilient and fault-tolerant.
In this article, we're going to go over the implementation of the AbstractRateLimiter; RedisRateLimiter. We'll create an API that greets the user while rate limiting the greetings!
Let's start by creating a new project with three dependencies:
The code for this is hosted on GitHub: JadCham/spring-redis-ratelimit-demo
If you're interested to learn more about rate limiting take a look at stripe's article on rate limiters.
In this article, we're going to go over the implementation of the AbstractRateLimiter; RedisRateLimiter. We'll create an API that greets the user while rate limiting the greetings!
Let's start by creating a new project with three dependencies:
- spring-cloud-gateway
- spring-data-redis
- spring-security
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
curl https://start.spring.io/starter.zip -d dependencies=cloud-gateway,data-redis,security -d bootVersion=2.0.5.RELEASE -o request-limiter.zip |
Create a Default user:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public MapReactiveUserDetailsService reactiveUserDetailsService() { | |
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); | |
UserDetails user = User.builder() | |
.username("test") | |
.password(encoder.encode("pass")) | |
.roles("USER") | |
.build(); | |
return new MapReactiveUserDetailsService(user); | |
} |
This bean creates a User with default credentials that will be used for rate limiting.
Create a New Route:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public RouteLocator routeLocator(RouteLocatorBuilder builder){ | |
return builder.routes() | |
.route("ratelimited_hello", route -> route | |
.path("hi") | |
.uri("http://localhost:8080/hello") | |
).build(); | |
} |
This route will match whatever URI that has a path "/hi" and will forward the request to "localhost:8080/hello"
Now that we have the RouteLocator handling the request we can try it out.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
curl --user test:pass localhost:8080/hi -vv | |
* Trying ::1... | |
* TCP_NODELAY set | |
* Connected to localhost (::1) port 8080 (#0) | |
* Server auth using Basic with user 'test' | |
> GET /hi HTTP/1.1 | |
> Host: localhost:8080 | |
> Authorization: Basic dGVzdDpwYXNz | |
> User-Agent: curl/7.54.0 | |
> Accept: */* | |
> | |
< HTTP/1.1 404 Not Found | |
< Content-Type: application/json;charset=UTF-8 | |
< Content-Length: 108 | |
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate | |
< Pragma: no-cache | |
< Expires: 0 | |
< X-Content-Type-Options: nosniff | |
< X-Frame-Options: DENY | |
< X-XSS-Protection: 1 ; mode=block | |
< | |
* Connection #0 to host localhost left intact | |
{"timestamp":"2018-10-24T02:32:22.758+0000","path":"/hello","status":404,"error":"Not Found","message":null}% |
The 404 is because of the request being routed to /hello that doesn't exist.
Create the Rest Controller:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
@RestController | |
public class GreetingApiEndpoint { | |
@RequestMapping("/hello") | |
public String sayHello(){ | |
return "Hey!"; | |
} | |
} |
If we run the curl command again we get a successful response now that the request can be routed to the destination.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
curl --user test:pass localhost:8080/hi -vv | |
* Trying ::1... | |
* TCP_NODELAY set | |
* Connected to localhost (::1) port 8080 (#0) | |
* Server auth using Basic with user 'test' | |
> GET /hi HTTP/1.1 | |
> Host: localhost:8080 | |
> Authorization: Basic dGVzdDpwYXNz | |
> User-Agent: curl/7.54.0 | |
> Accept: */* | |
> | |
< HTTP/1.1 200 OK | |
< Content-Type: text/plain;charset=UTF-8 | |
< Content-Length: 4 | |
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate | |
< Pragma: no-cache | |
< Expires: 0 | |
< X-Content-Type-Options: nosniff | |
< X-Frame-Options: DENY | |
< X-XSS-Protection: 1 ; mode=block | |
< | |
* Connection #0 to host localhost left intact | |
Hey!% |
Add the Rate Limiter:
The rate limiter takes two values:
- replenishRate: The number of requests allowed by a user every second.
- burstCapacity: The maximum number of requests allowed by a user every second.
To better understand these values check the docs and the algorithm behind the rate limiting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public RedisRateLimiter rateLimiter(){ | |
return new RedisRateLimiter(1,1); | |
} |
Update The Route To Include The Rate Limiter:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public RouteLocator routeLocator(RouteLocatorBuilder builder){ | |
return builder.routes() | |
.route("ratelimited_hello", route -> route | |
.path("/hi") | |
.filters(i -> i.requestRateLimiter(j -> j.setRateLimiter(rateLimiter()))) | |
.uri("http://localhost:8080/hello") | |
).build(); | |
} |
Now when we try the same curl command again we notice three new headers:
- X-RateLimit-Remaining (Used by clients to throttle their requests)
- X-RateLimit-Burst-Capacity (Defined in the rate limiter above)
- X-RateLimit-Replenish-Rate (Defined in the rate limiter above)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
curl --user test:pass localhost:8080/hi -vv | |
* Trying ::1... | |
* TCP_NODELAY set | |
* Connected to localhost (::1) port 8080 (#0) | |
* Server auth using Basic with user 'test' | |
> GET /hi HTTP/1.1 | |
> Host: localhost:8080 | |
> Authorization: Basic dGVzdDpwYXNz | |
> User-Agent: curl/7.54.0 | |
> Accept: */* | |
> | |
< HTTP/1.1 200 OK | |
< X-RateLimit-Remaining: 0 | |
< X-RateLimit-Burst-Capacity: 1 | |
< X-RateLimit-Replenish-Rate: 1 | |
< Content-Type: text/plain;charset=UTF-8 | |
< Content-Length: 4 | |
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate | |
< Pragma: no-cache | |
< Expires: 0 | |
< X-Content-Type-Options: nosniff | |
< X-Frame-Options: DENY | |
< X-XSS-Protection: 1 ; mode=block | |
< | |
* Connection #0 to host localhost left intact | |
Hey!% |
Test The Rate Limiter:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# With Rate Limiting | |
while;do curl -I --user test:pass localhost:8080/hi;done | |
# Without Rate Limiting | |
while;do curl -I --user test:pass localhost:8080/hello;done |
The code for this is hosted on GitHub: JadCham/spring-redis-ratelimit-demo
Comments
Post a Comment