Spring Boot application uses Redis to achieve frequency limit

In this article, we demonstrate how to build a product-level frequency limit feature and use Redis and Spring data Redis modules in Spring Boot applications. Frequency limits are usually used to throttle API requests.

Frequency limit

Frequency limitation is to limit the number of requests for a specific service within a given time. For production-level APIs, there is usually a limit to the number of calls per hour by no one. The following example illustrates:

  • A specific mobile phone allows 5 OTP (One-Time Password) within an hour
  • The website allows 5 times per hour to forget the password
  • Use the given API KEY to allow 20 requests
  • The blog site allows users (or IP addresses) to post up to 1 comment per minute

Redis implementation

In this article, we build a basic frequency limit feature that allows each logged-in user to request service 10 times per hour. Redis provides two commands incrand expirecan easily achieve our demands.

We use each user name to create a Redis build every hour, and ensure that it expires automatically after 1 hour, so that our database will not be filled with expired data.

For the user name carvia, the following table shows the changes of the Redis key over time and whether it has passed.

Time11:0012:0013:0014:00
Redis Key (string)carvia:11carvia:12carvia:13carvia:14
Value3510 (max limit)null
Expires At13:00 (2 hours later)14:0015:0016:00

The Redis key is a combination of user name and time number through a colon. And set to expire after 2 hours, so don't worry about Redis storage space.

Pseudo-code implementation:

  1. GET [username]:[current hour]
  2. If the result exists and is less than 10, go to step 4, otherwise go to step 4
  3. Display the maximum limit reached error message and end
  4. Redis starts the transaction and performs the following steps
  • Use incrincreased [username]: Counter [current hour] bond
  • For the key to set the expiration time to 2 hours from now, use expire[username]:[当前小时]3600
  1. Allow request to continue service

Spring Boot application implementation

Spring Data Redis provides simple configuration and access methods, as well as low-level and high-level encapsulation abstractions for Redis storage interaction. Next, we create Spring Boot to implement the frequency limit feature.

Start Redis in docker

docker run -itd --name redis -p 6379:6379 --rm redis

# 用客户端端来连接redis
redis-cli

You can use the docker plug-in in idea to access redis and connect to the client for testing.

Insert picture description here

Reference dependency

Spring Boot version and main dependencies. The Java Redis client uses Lettuce by default, of course you can also use Jedis.

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.1</version>
        </dependency>
	</dependencies> 

Configure Redis connection

The application.propertiescorresponding increase in the configuration:

# redis 服务器地址(安装在虚拟机中的docker)
spring.redis.host=192.168.31.93 
spring.redis.database=0
spring.redis.password=

So we started the automatic configuration of Redis. Spring Boot will automatically inject StringRedisTemplatebeans and use it to interact with Redis.

Implementation code

To simplify, we directly write a class for testing:

package com.dataz.ratelimit.service;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @Author Tommy
 * @create 2021/6/5 16:18
 */
@RestController
public class RateLimit {
    private static final Logger logger = LoggerFactory.getLogger(RateLimit.class);
    private static final int REQUESTS_PER_HOUR = 10;
    private static final int TEST_PER_HOUR = 20;
    private static final String USER_NAME = "carvia";


    private final StringRedisTemplate stringTemplate;
    public RateLimit(StringRedisTemplate stringTemplate) {
        this.stringTemplate = stringTemplate;
    }

    private boolean isAllowed(String username) {
        final int hour = LocalDateTime.now().getHour();
        String key = username + ":" + hour;
        ValueOperations<String, String> operations = stringTemplate.opsForValue();
        String requests = operations.get(key);
        if (StringUtils.isNotBlank(requests) && Integer.parseInt(requests) >= REQUESTS_PER_HOUR) {
            return false;
        }

        List<Object> txResults = stringTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
                final StringRedisTemplate redisTemplate = (StringRedisTemplate) operations;
                final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
                operations.multi();
                valueOperations.increment(key);
                redisTemplate.expire(key, 2, TimeUnit.HOURS);
                // This will contain the results of all operations in the transaction
                return operations.exec();
            }
        });
        logger.info("Current request count:{} ", Objects.requireNonNull(txResults.get(0),"null"));
        return true;
    }

    @GetMapping("/api/service01")
    public ResponseEntity<String> service() {
        for (int i=0; i< TEST_PER_HOUR; i++) {
            boolean allowed = isAllowed(USER_NAME);
            if(!allowed) {
                return ResponseEntity.ok("超过限制");
            }
        }
        return ResponseEntity.ok("正常访问");
    }
}

Start the application and send a request to test:

GET http://localhost:8080/api/service01?userName=jack

The execution result is returned 超过限制.

Check whether there is a corresponding key in Redis and the value reaches the maximum value.

127.0.0.1:6379> keys ja*
1) "jack:17"
127.0.0.1:6379> get jack:17
"10"

to sum up

This article uses Redis to implement frequency limitation in Spring Boot applications. For uncomplicated frequency limit is achieved by relatively easy to herein, a more complex scenario requires specialized tools to achieve, such as: Bucket4j.