[Distributed Architecture] (10) -- A Distributed Current Limiting System Based on the Characteristics of Redis Components

Distributed-Redis-based Interface IP Current Limitation

In order to prevent our interfaces from being accessed maliciously, for example, when someone accesses our interfaces frequently through JMeter tools, the interface response becomes slower or even crashes, so we need to limit the IP current of some specific interfaces, that is, the number of accesses to the same IP at a certain time is limited.

The implementation principle uses Redis as the core principle of current limiting component. The user's IP address is regarded as Key, and the number of visits is value in a period of time. At the same time, the key expiration time is set.

For example, an interface with the same IP10 seconds requests five times, more than five times do not allow access to the interface.

1. The first time the IP address is stored in redis, the key value is IP address, the value is 1, and the expiration time of the key value is set to 10 seconds.
2. The second time the IP address is stored in redis, if the key does not expire, the update value is 2.
3. By analogy, when value is already 5, if the next IP address is stored in redis and the key has not expired, then the IP can not be accessed.
4. When 10 seconds later, the key value expires, then the IP address comes in again, value starts from 1, and the expiration time is 10 seconds, which repeats and repeats.

It can be seen from the above logic that the number of visits is limited in a period of time, not that the IP access interface is totally excluded.

Technical framework SpringBoot + RedisTemplate (done with custom annotations)

This can be used for real project development scenarios.

I. Code

1. Custom Annotations

The purpose of using custom annotations here is to use custom annotations on the interface to make the code look very neat.

IpLimiter

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IpLimiter {
    /**
     * Current limiting ip
     */
    String ipAdress() ;
    /**
     * Number of requests per unit time limit
     */
    long limit() default 10;
    /**
     * Unit time, unit second
     */
    long time() default 1;
    /**
     * Tips for reaching current limit
     */
    String message();
}

2. Test Interface

Used a custom annotation @IpLimiter on the interface

@Controller
public class IpController {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class);
    private static final String MESSAGE = "request was aborted,Your IP Too frequent visits";

    //Instead of getting the requested ip, write to death an IP
    @ResponseBody
    @RequestMapping("iplimiter")
    @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE)
    public String sendPayment(HttpServletRequest request) throws Exception {
        return "Successful request";
    }
    @ResponseBody
    @RequestMapping("iplimiter1")
    @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE)
    public String sendPayment1(HttpServletRequest request) throws Exception {
        return "Successful request";
    }
}

3. AOP Processing IpLimter Annotations

This side deals with custom annotations in a slice-wise manner. In order to ensure atomicity, the redis script ipLimiter.lua is written here to execute the redis command to ensure operation atomicity.

@Aspect
@Component
public class IpLimterHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class);

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * getRedisScript Read script tool class
     * This is set to Long because the ipLimiter.lua script returns a number type
     */
    private DefaultRedisScript<Long> getRedisScript;

    @PostConstruct
    public void init() {
        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(Long.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
        LOGGER.info("IpLimterHandler[Distributed Current Limiting Processor]Script loading completed");
    }

    /**
     * This cut-off point can be avoided, because the following itself is a commentary
     */
//    @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)")
//    public void rateLimiter() {}

    /**
     * If the above cut point is retained, then here it can be written as
     * @Around("rateLimiter()&&@annotation(ipLimiter)")
     */
    @Around("@annotation(ipLimiter)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]Start current limiting operation");
        }
        Signature signature = proceedingJoinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
        }
        /**
         * Getting annotation parameters
         */
        // Current Limiting Module IP
        String limitIp = ipLimiter.ipAdress();
        Preconditions.checkNotNull(limitIp);
        // Current Limiting Threshold
        long limitTimes = ipLimiter.limit();
        // Current Limiting Overtime
        long expireTime = ipLimiter.time();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]The parameter value is-limitTimes={},limitTimeout={}", limitTimes, expireTime);
        }
        // Current Limiting Tips
        String message = ipLimiter.message();
        /**
         * Executing Lua scripts
         */
        List<String> ipList = new ArrayList();
        // Set the key value to the value in the comment
        ipList.add(limitIp);
        /**
         * Call the script and execute it
         */
        Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes);
        if (result == 0) {
            String msg = "Because of exceeding unit time=" + expireTime + "-Permissible number of requests=" + limitTimes + "[Triggered current limiting]";
            LOGGER.debug(msg);
            // Reaching the current limit and returning to the front-end information
            return message;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]Current Limiting Execution Results-result={},request[normal]response", result);
        }
        return proceedingJoinPoint.proceed();
    }
}

4. RedisCacheConfig (configuration class)

@Configuration
public class RedisCacheConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class);

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        //Use Jackson 2Json RedisSerializer to serialize and deserialize the value of redis (default JDK serialization)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //Use String RedisSerializer to serialize and deserialize the key value of redis
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        LOGGER.info("Springboot RedisTemplate Loading completed");
        return template;
    }
}

5. ipLimiter.lua script

Advantage
Reduce network overhead: scripts are executed only once, and multiple requests are not needed to reduce network transmission;
Assuring atomic operations: The entire script is executed as an atom without worrying about concurrency.

--Obtain KEY
local key1 = KEYS[1]

local val = redis.call('incr', key1)
local ttl = redis.call('ttl', key1)

--Obtain ARGV Parameters inside and print
local expire = ARGV[1]
local times = ARGV[2]

redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))

redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
if val == 1 then
    redis.call('expire', key1, tonumber(expire))
else
    if ttl == -1 then
        redis.call('expire', key1, tonumber(expire))
    end
end

if val > tonumber(times) then
    return 0
end
return 1

6,application.properties

#redis
spring.redis.hostName=
spring.redis.host=
spring.redis.port=6379
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=10
spring.redis.timeout=100ms
spring.redis.password=

logging.path= /Users/xub/log
logging.level.com.jincou.iplimiter=DEBUG
server.port=8888

7. SpringBoot Startup Class

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

8. Testing

Perfectly, this test is in line with our expectations. The first five accesses to the interface were successful, and then failed. It was not accessible until 10 seconds later, so it was repeated and repeated.

The other side is not shown one by one. Attached is the source code of the project.

Github address https://github.com/yudiandemingzi/spring-boot-redis-ip-limiter


Reference resources

This design is really good when I brush github. I just made some changes on the basis of it. Thank you very much for the author's sharing.
github address: https://github.com/TaXueWWL/shleld-ratelimter

There are good articles about AOP: The use of @ annotation() in spring aop



As long as you become excellent, everything else will follow (Lieutenant General 1)

Keywords: Redis Spring github Jedis

Added by VertLime on Wed, 21 Aug 2019 15:38:06 +0300