Author: Brian Sam-Bodden
One way to improve our implementation is by moving the responsibility of
EXPIRE operations from the
method, to a Lua script.
Redis has the ability to execute Lua scripts on the server side. Lua scripts
are executed atomically, that is, no other script or command will run
while a script is running, which gives us the same transactional semantics as
Below is a simple Lua script to encapsulate the rate limiting logic. The script returns
if the request is to be rejected or
Place the script under
src/main/resources/scripts. Now, Let's break it down:
- Lua scripts in Redis work with keys (
KEYS) and arguments (
ARGV) in our case we are expecting one
KEYS(Lua arrays are 1-based)
- We retrieve the quota for the key in
requestsby making a
-1if the key does not exist, and converting the value to a number.
- The quota is passed as the first argument (
ARGV) and stored in
max_requests, the expiry in seconds is the second argument and stored in
ifstatement checks whether the request is the first request in the time window or if the number of requests have not exceeded the quota, in which case we run the
EXPIREcommands and retunr
false(meaning we are not rate limiting and allowing the request through).
- If they've exceeded the quote, then we rate limit by returning
If you want to learn more about Lua, see Programming in Lua.
Spring Data Redis supports Lua scripting via the class
RedisScript. It handles serialization and intelligently
uses the Redis script cache. The cache is populated using the
SCRIPT LOAD command. The default
EVALSHA using the SHA1 of the script and falling back to
EVAL if the script has not yet been loaded into the cache.
Let's add the bean annotated method
script() to load our script from the classpath:
Next, we'll modify the filter to include the script as well as the quota; the value that we need to pass to the script:
Now we can modify the
filter method to use the script. Scripts are run using the execute methods of
execute methods use a configurable
that inherits the key and value serialization setting of the template to run the scripts:
Let's break down the method additions:
filtermethod uses the template
executemethod passing the script, keys and arguments.
- We expect a
singlemethod takes a default value to be returned in case we get an empty result.
- Finally, we use the
flatMapmethod to grab the value:
- If it is
truewe reject the request with an HTTP 429.
- If it is
falsewe handle the request
- If it is
Let's add a configurable
Long value to the
hold the request quota.
application.properties we'll set it to a max of 20 request per minute:
To invoke the filter we use the newly modified constructor, passing the template, the script, and the
Using our trusty curl loop:
You should see the 21st request being rejected:
If we run Redis in monitor mode, we should see the Lua calls to
EVALSHA, followed by the call to
GET for a rejected
request, and the same plus calls to
EXPIRE for an allowed request:
The complete code for this implementation is under the branch