From 162df3e7173309ad9d4fec5a59a65ded110e262e Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Wed, 5 Feb 2025 01:30:35 -0700 Subject: [PATCH] add more discussion to INCR docu case studies The discussion about implementing a rate limiter pattern contained in the docu about INCR command may lead one to think that the rate limiter pattern is made somewhat more complicated due to INCR not having NX features similar to SET. This is because a very simple but flawed example is demonstrated, and the working solutions all either use timestamps within the buckets (which adds complexity, and raises questions about clock synchronization across servers), or use lua scripting, or use redis lists. Meanwhile an NX feature on INCR would seem to solve the problem pointed out in the flawed example. This idea re-appears in github issues where users request additional features to be added to INCR, and point to rate limiter as a motivation: https://github.com/redis/redis/issues/4423 https://github.com/redis/redis/issues/7631 In all the comments, more experienced users explain how you can already basically do this using a MULTI pipeline, and you can use NX in that pipeline as well if you want, which would accomplish what these feature requests seem to be asking for. However, this idea doesn't actually appear in the case studies in the documentation. Expanding the case studies to include this idea, and working through explicitly how it avoids race conditions and pitfalls described in the other approaches, may be helpful to users. (It was helpful to me.) --- commands/incr.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/commands/incr.md b/commands/incr.md index e8aae005d0..7f2875dc99 100644 --- a/commands/incr.md +++ b/commands/incr.md @@ -114,7 +114,7 @@ value greater than 10, otherwise it will expire and start again from 0. If for some reason the client performs the `INCR` command but does not perform the `EXPIRE` the key will be leaked until we'll see the same IP address again. -This can be fixed easily turning the `INCR` with optional `EXPIRE` into a Lua +One way to fix this is by turning the `INCR` with optional `EXPIRE` into a Lua script that is send using the `EVAL` command (only available since Redis version 2.6). @@ -126,7 +126,31 @@ if current == 1 then end ``` -There is a different way to fix this issue without using scripting, by using +Another way to fix this, which doesn't require Lua, is to use the `NX` option of `EXPIRE`. + +``` +FUNCTION LIMIT_API_CALL(ip) +MULTI + INCR(ip) + EXPIRE(ip,10,NX) +EXEC +current = RESPONSE_OF_INCR_WITHIN_MULTI +IF current > 10 THEN + ERROR "too many requests per second" +ELSE + PERFORM_API_CALL() +END +``` + +Here, EXPIRE with NX is a no-op if there is already an expiry value set. So it only +has an effect when this key was created just now by the call to INCR, and it isn't necessary +to have a test for `current == 1` to get the desired behavior. The use of MULTI +ensures that EXPIRE will always be called whenever INCR is. So keys cannot be created +this way without having an expiry set, and we don't have to worry about leaking keys. +Compared to Rate Limiter 1 pattern, it is a little simpler, and doesn't rely on clock synchronization +across your web servers in order to work well. + +There is another way to fix this issue without using scripting, by using Redis lists instead of counters. The implementation is more complex and uses more advanced features but has the advantage of remembering the IP addresses of the clients currently performing an