Chat with LLM: Source Code Analysis of Redisson Rate Limiter
Introduction
My project uses Redis as a cache. For user module endpoints, except for login and registration which require a phone number, all other endpoints operate using an ID. The initial code stored both an ID-to-user mapping and a phone-number-to-user mapping in the cache. Storing the same user object twice is obviously unreasonable, not to mention the potential additional consistency problems that could arise.
Since one of the purposes of a cache is to protect the database, naturally we can achieve this by other means, such as rate limiting. If we only apply rate limiting to the login and registration endpoints, then we can store only the ID-to-user mapping in the cache. This is also reasonable from a business perspective, as these two endpoints are not frequently called.
There are at least three specific implementations of rate limiting: the leaky bucket algorithm, the token bucket algorithm, and message queues. I don't want to introduce a message queue just for rate limiting, especially since that component is difficult to operate. The token bucket algorithm is a superior alternative to the leaky bucket algorithm, as it better handles burst traffic. Therefore, the leaky bucket algorithm is rarely used nowadays.
There are many libraries that implement the token bucket algorithm: Guava, Bucket4j, Resilience4j, Sentinel, Redisson, etc. Clearly, this business scenario requires distributed rate limiting. Since Redisson is already used in the project, and my requirements for rate limiting are simple, I decided to use the rate limiter provided by Redisson.
The token bucket algorithm is conceptually simple: it is a non-blocking producer-consumer model. A bucket with a fixed capacity is filled with tokens at a constant rate. When a request arrives, it attempts to acquire a token, and the request is processed only if it successfully obtains one. However, I wasn't clear on the specifics of how this algorithm is implemented, so in this blog post, I leverage an LLM to read the source code of the RedissonRateLimiter class and analyze its implementation.
Source Code Analysis
RedissonRateLimiter is a distributed rate limiter implementation provided by Redisson, based on the token bucket algorithm. It utilizes Redis data structures such as Sorted Sets, Hashes, and Strings, and ensures the atomicity of multiple operations through Lua scripts, thereby achieving cross-JVM rate limiting capabilities.
Key Fields and Methods
-
Name Generation: The rate limiter stores multiple keys in Redis, generated with fixed suffixes:
getRawName(): The main key of the rate limiter (stores configuration in a Hash).getValueName(): The key storing the current number of available tokens (String type).getClientValueName(): Client-specific token counter (used in PER_CLIENT mode).getPermitsName(): The Sorted Set storing issued but not yet expired tokens.getClientPermitsName(): Client-specific token set.
-
Core Methods:
trySetRateAsync(): Attempts to initialize the rate limiter configuration (atomic operation).tryAcquireAsync(): Attempts to acquire a specified number of tokens, returns the wait time (in milliseconds) ornullfor immediate success.releaseAsync(): Manually releases tokens (typically for compensation).availablePermitsAsync(): Queries the current number of available tokens.getConfigAsync(): Retrieves the current rate limiter configuration.
Initializing the Rate Limiter Configuration
Java Code
1 |
|
Method Explanation:
- Uses
HSETNXcommands to sequentially set therate,interval,keepAliveTime, andtypefields.HSETNXsucceeds only if the field does not already exist, ensuring the configuration is initialized only once. - If this initialization succeeds (
res == 1) andkeepAliveTimeis set, it immediately sets an expiration time on the main key. - The return value
trueindicates successful initialization (i.e., no configuration existed before),falseindicates the configuration already exists.
Lua Script
1 | -- Attempt to set the rate field, succeeds only if the field does not exist |
Attempting to Acquire Tokens
Java Code
tryAcquireAsync has multiple overloads, ultimately calling a private method:
1 | private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) { |
Method Explanation:
- The parameter
valueis the number of requested tokens (permits). - Generates a random byte array
randomused to construct the member in the sorted set (ensuring uniqueness). - Executes the Lua script to perform the token acquisition logic:
- Retrieves the rate limiter configuration (rate, interval, type).
- Selects the corresponding
valueNameandpermitsNamebased on the mode (OVERALL or PER_CLIENT). - Checks if the requested number of tokens exceeds the rate limit (
rate). - Attempts to remove expired token records from
permitsNameand releases the corresponding tokens. - Determines if enough tokens are currently available; if so, deducts tokens and records the issued tokens in the sorted set; if not, calculates the required wait time.
- Updates the expiration time of the keys (if
keepAliveTimeis configured, refreshes to that time; otherwise, inherits the TTL of the main key).
- Return result:
nilindicates immediate success; a number indicates the wait time in milliseconds (includes a 3ms compensation).
Lua Script
1 | -- Retrieve configuration from the main key's hash (KEYS[1]) |
Summary
- Redisson's rate limiter stores configuration in a Hash, the current available tokens in a String, and issued token timestamps and counts in a Sorted Set.
- Lua scripts ensure the atomicity of operations such as acquiring tokens, releasing expired tokens, and deducting tokens.
- Two modes are supported: OVERALL mode and PER_CLIENT mode, where the latter maintains independent counts for each client.
- The
keepAliveTimeparameter can automatically clean up inactive rate limiter keys to prevent memory leaks. - The wait time returned by
tryAcquireAsyncincludes a 3ms extra offset to compensate for network and clock errors, making the rate limiting more accurate.
The code comments and script analysis above demonstrate how Redisson leverages Redis's atomic features to implement a high-performance distributed token bucket rate limiter.
References
- LLM