SoFunction
Updated on 2025-04-21

Example of SpringBoot integrating redis to implement counter current limit

Use redis's self-increase to limit the interface

1. Introduce dependencies

<!-- springbootIntegrated,No need to introduce another version -->
<dependency>
  <groupId></groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. Code examples

2.1 Basic Code

I use the redis key that uses a mobile phone number and some other strings here, and you can customize your own key.

private void validRateBasic (String phone) {
        String key = "LIMIT:RATE:" + phone;
        (new StringRedisSerializer());
        (new StringRedisSerializer());
        try {
            String num = (String) ().get(key);
            if ((num)) {
                ().set(key, "1", 60, );
            } else if ((num) >= 20) {
                Long expire = (key);
                throw new CheckedException("Frequent operation, please" + expire + "Try again after s");
            } else {
                ().increment(key);
            }
        } catch (Exception e) {
            if (e instanceof CheckedException) {
                throw new CheckedException(());
            } else {
                ("Failed to verify upload rate,error:{}",e);
                throw new CheckedException("The operation failed, please try again later");
            }
        }

    }

This code implements that in the same interface, the same mobile phone number can only be accessed 20 times within 60s. Although redis is single-threaded, in the case of high concurrency, this code still has concurrency problems. Between getting accesses and increasing accesses, the number of accesses may have been modified by other threads. If you do not have high requirements for one or two extra requests, then this limit basically meets the requirements.
In redis, we can use lua scripts and redis transactions to ensure the atomicity of the operation.

2.2 Using redis transactions

2.2.1 SessionCallback (not recommended)

Some people use (true) to support transactions using redisTemplate, but this may have the following problems:

  • If you use Redis in a distributed environment, transaction support may be problematic because Redis's transaction model is optimistic and if operations in a transaction are modified by other instances, the transaction will fail. In high concurrency scenarios, this can lead to a large number of transaction failures.
  • Making RedisTemplate support transactions causes all Redis operations to be executed in the transaction, which may degrade performance, especially if a large number of Redis operations are required.
  • This setting will affect all code using this RedisTemplate instance, so you need to make sure that all relevant code can handle Redis operations in the transaction correctly.

Here is the SessionCallback interface provided by Spring Data Redis. It allows us to perform multiple operations in a single Redis connection and maintain atomicity.

private void validRate(String phone) {
        String key = "LIMIT:RATE:" + phone;SpringBootIntegrationredisImplement counter current limit
        int retryTimes = 0;
        // Failed to try five times        (new StringRedisSerializer());
        (new StringRedisSerializer());
        while(retryTimes < 6) {
            retryTimes++;
            try {
                // Get the value of this key outside the transaction                String num = (String) ().get(key);

                // Use SessionCallback for atomic operations                SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
                    @Override
                    public Object execute(RedisOperations operations) throws DataAccessException {
                        (key);
                        ();
                        // Check the value of this key again inside the transaction                        String currentNum = (String) ().get(key);
                        if (num == null ? currentNum != null : !(currentNum)) {
                            // The value of this key has been modified, so the transaction is cancelled                            ();
                            return null;
                        }
                        if ((num)) {
                            ().set(key, "1", 60, );
                        } else if ((num) >= 5) {
                            Long expire = (key);
                            throw new CheckedException("Frequent operation, please" + expire + "Try again after s");
                        } else {
                            ().increment(key);
                        }
                        // Submit transaction and return result                        return ();
                    }
                };

                // Execute SessionCallback                List<Object> results = (List<Object>) (sessionCallback);
                if ((results)) {
                    // If the transaction fails, try the transaction again                    ("Try again");
                    continue;
                }
                return;
            } catch (Exception e) {
                //Catch any exceptions in case of retry                if (retryTimes >= 5) {
                    throw new CheckedException("Frequent operation, please try again later");
                }
            }
        }
    }

This piece of code looks nothing wrong. Once you run it, you will find that String num = (String) ().get(key); has always been null. This is because in a redis transaction, all commands in the transaction will be placed in the queue and will not be executed at one time until the exec command is called. Redis transactions are in some ways inferior to relational databases:

  • No isolation: Redis's transaction has no isolation. After the transaction starts (multi command execution), other clients can still read and write keys in the transaction, which may affect the transaction's results.
  • Atomless reading: Unable to read data that has not been submitted by one's own transaction, nor can it read data written by other transactions. As above code, the get command after the transaction starts returns null, not the latest data.
  • No rollback: Once a transaction is committed (exec command execution), all operations in the transaction will be executed, and even if some of the operations fail, other operations will not be rolled back.
  • Lock-free: The redis transaction does not provide locks, or redis does not have the concept of locks, which is the same as the result of non-isolation.

2.2.2 Distributed lock (recommended)

Distributed locks already have many mature frameworks and many excellent blogs. I won’t go into details here. I will add one article if I have time.

2.3 Using Lua scripts (recommended)

Lua scripts are atomic when executed: when the script is running, no other scripts or Redis commands are executed.

private void validRateLua(String phone) {
        String key = "LIMIT:RATE:" + phone;
        int retryTimes = 0;
        // Create Lua script and return a new count value        String luaScript =
                "local num = ('GET', KEYS[1]);" +
                        "if num == false then " +
                        "   ('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]);" +
                        "   return ARGV[1];" +
                        "elseif tonumber(num) <= tonumber(ARGV[3]) then " +
                        "   local newNum = ('INCR', KEYS[1]);" +
                        "   return newNum;" +
                        "else " +
                        "   return num;" +
                        "end;";
        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, );
        (new StringRedisSerializer());
        (new StringRedisSerializer());
        while(retryTimes < 5) {
            retryTimes++;
            try {
                // Execute Lua scripts                String num = (String) (redisScript, (key), "1", "60","5");
                if (num != null && (num) > 5) {
                    Long expire = (key);
                    throw new CheckedException("Frequent operation, please" + expire + "Try again after s");
                }

                return;
            } catch (Exception e) {
                if (e instanceof CheckedException) {
                    throw new CheckedException(());
                } else {
                    //Catch any exceptions in case of retry                    // If necessary, you can add index backoff, maximum retry time, etc.                    if (retryTimes >= 5) {
                        ("Upload failed,error:{}",e);
                        throw new CheckedException("Frequent operation, please try again later");
                    }
                }
            }
        }
    }

There are several points to note when executing Lua scripts:

  • Lua scripts will block all operations of Redis. You need to ensure that the execution time of Lua scripts is short to avoid affecting the performance of redis.
  • Once the lua script is executed, it will be loaded into memory, and will be continuously saved in memory even if it is not executed. The purpose of this design is to facilitate quick execution and avoid reloading of the script every time it is executed.
  • Lua scripts are generally very small, but if you have a large number of Lua scripts stored in memory for a long time and are loaded and executed frequently, it will occupy a lot of memory. This problem can be solved by script command and LUA-EVAL-NOLOAD configuration options.

This is the end of this article about SpringBoot integrating redis to implement counter current limiting. For more related SpringBoot redis counter current limiting content, please search for my previous articles or continue browsing the following related articles. I hope everyone will support me in the future!