Java Concurrency: Distributed application current limit Redis + Lua Practice

Source: Internet
Author: User
Tags cas lua semaphore redis server stringbuffer


Any limit flow is not aimless, nor a switch can solve the problem, commonly used current limit algorithm: Token bucket, leaky bucket. In the previous article, it was also mentioned, but it was written based on a single-machine scenario.





Previous article: interface current limiting algorithm: Leaky bucket algorithm & token bucket algorithm



However, the machine, the optimization of the design, for special scenes we also have to deal with special. Take seconds to kill, there may be millions other users to buy, and the number of goods is much smaller than the number of users. If these requests go into the queue or query cache, there is no point in the end result, and the background is gorgeous data. For this, in order to reduce the waste of resources, reduce the back-end pressure, we also need to limit the second kill, just to ensure that some user services are normal.



In the case of the second-kill interface, when the frequency of the visit or the concurrent request exceeds its scope, we should consider the current limit to ensure the availability of the interface, in order to prevent unexpected requests to the system caused by excessive pressure system paralysis. The usual strategy is to deny redundant access, or to queue up redundant access for services.



Distributed current limiting



Single-machine current limit, can be usedAtomicInteger,RateLimiterandSemaphorethese. However, in the distributed, it cannot be used. Commonly used distributed current limitNginx, but it belongs to the gateway level, can not solve all problems, such as internal services, SMS interface, you can not ensure that consumers will do a good limit control, so that their own application layer to achieve current limit is still very necessary.



This paper does not dealnginx + luawith theredis + luaimplementation of the distributed current limit in brief. If it is necessary to limit the flow in the access layer, we should directly adopt the connection number current limit module and the request current limit module with Nginx.


Redis + Lua Current Limit Example


This project usesSpringBoot 2.0.4, uses to theRediscluster, the currentLualimit script


Introducing Dependencies
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>
Redis Configuration


Application.properties


spring.application.name = spring-boot-limit

# Redis database index
spring.redis.database = 0
# Redis server address
spring.redis.host = 10.4.89.161
# Redis server connection port
spring.redis.port = 6379
# Redis server connection password (default is empty)
spring.redis.password =
# Maximum number of connections in the connection pool (use a negative value to indicate no limit)
spring.redis.jedis.pool.max-active = 8
# Connection pool maximum blocking wait time (use a negative value to indicate no limit)
spring.redis.jedis.pool.max-wait = -1
# Maximum idle connection in the connection pool
spring.redis.jedis.pool.max-idle = 8
# The smallest idle connection in the connection pool
spring.redis.jedis.pool.min-idle = 0
# Connection timeout (ms)
spring.redis.timeout = 10000
Lua Script


Reference: Talk about current limit stunts for high concurrency systems
http://jinnianshilongnian.iteye.com/blog/2305117


local key = "rate.limit:" .. KEYS [1]-current-limit KEY
local limit = tonumber (ARGV [1])-current limit size
local current = tonumber (redis.call (‘get‘, key) or "0")
if current + 1> limit then-if the current limit is exceeded
   return 0
else-number of requests +1, and set to expire in 2 seconds
   redis.call ("INCRBY", key, "1")
    redis.call ("expire", key, "2")
    return current + 1
end


1. We get the passed key parameter via Keys[1]
2. Get the incoming limit parameter by argv[1]
3. Redis.call method, get and key related value from cache, if nil then return 0
4, then determine whether the value recorded in the cache will be greater than the limit size, if the current limit is exceeded, return 0
5, if not exceeded, then the key cache value of +1, and set the expiration time is 1 seconds later, and return the cache value +1


Current limit annotations


The purpose of the annotation is to use the method on which the current limit is required.


package com.souyunku.example.annotation;
/ **
  * Description: current limit annotation
  *
  * @author yanpenglei
  * @create 2018-08-16 15:24
  ** /
@Target ({ElementType.TYPE, ElementType.METHOD})
@Retention (RetentionPolicy.RUNTIME)
public @interface RateLimit {

     / **
      * Unique sign of current limit
      *
      * @return
      * /
     String key () default "";

     / **
      * Current limit time
      *
      * @return
      * /
     int time ();

     / **
      * Current limiting times
      *
      * @return
      * /
     int count ();
}
Public configuration
package com.souyunku.example.config;

@Component
public class Commons {

     / **
      * Read current limit script
      *
      * @return
      * /
     @Bean
     public DefaultRedisScript <Number> redisluaScript () {
         DefaultRedisScript <Number> redisScript = new DefaultRedisScript <> ();
         redisScript.setScriptSource (new ResourceScriptSource (new ClassPathResource ("rateLimit.lua")));
         redisScript.setResultType (Number.class);
         return redisScript;
     }

     / **
      * RedisTemplate
      *
      * @return
      * /
     @Bean
     public RedisTemplate <String, Serializable> limitRedisTemplate (LettuceConnectionFactory redisConnectionFactory) {
         RedisTemplate <String, Serializable> template = new RedisTemplate <String, Serializable> ();
         template.setKeySerializer (new StringRedisSerializer ());
         template.setValueSerializer (new GenericJackson2JsonRedisSerializer ());
         template.setConnectionFactory (redisConnectionFactory);
         return template;
     }

}
Interception device


Using the method of intercepting annotations by interceptors@RateLimit,Redsi executewe use the method to execute our current limit script to determine if the current limit number is exceeded.



The following is the core code


package com.souyunku.example.config;

/ **
 * Description: Interceptor
 *
 * @author yanpenglei
 * @create 2018-08-16 15:33
 ** /
@Aspect
@Configuration
public class LimitAspect {

    private static final Logger logger = LoggerFactory.getLogger (LimitAspect.class);

    @Autowired
    private RedisTemplate <String, Serializable> limitRedisTemplate;

    @Autowired
    private DefaultRedisScript <Number> redisluaScript;

    @Around ("execution (* com.souyunku.example.controller .. * (..))")
    public Object interceptor (ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature ();
        Method method = signature.getMethod ();
        Class <?> TargetClass = method.getDeclaringClass ();
        RateLimit rateLimit = method.getAnnotation (RateLimit.class);

        if (rateLimit! = null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes ()). GetRequest ();
            String ipAddress = getIpAddr (request);

            StringBuffer stringBuffer = new StringBuffer ();
            stringBuffer.append (ipAddress) .append ("-")
                    .append (targetClass.getName ()). append ("-")
                    .append (method.getName ()). append ("-")
                    .append (rateLimit.key ());

            List <String> keys = Collections.singletonList (stringBuffer.toString ());

            Number number = limitRedisTemplate.execute (redisluaScript, keys, rateLimit.count (), rateLimit.time ());

            if (number! = null && number.intValue ()! = 0 && number.intValue () <= rateLimit.count ()) {
                logger.info ("Visited: {} times during the time limit", number.toString ());
                return joinPoint.proceed ();
            }

        } else {
            return joinPoint.proceed ();
        }

        throw new RuntimeException ("The number of current limit has been reached");
    }

    public static String getIpAddr (HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader ("x-forwarded-for");
            if (ipAddress == null || ipAddress.length () == 0 || "unknown" .equalsIgnoreCase (ipAddress)) {
                ipAddress = request.getHeader ("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length () == 0 || "unknown" .equalsIgnoreCase (ipAddress)) {
                ipAddress = request.getHeader ("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length () == 0 || "unknown" .equalsIgnoreCase (ipAddress)) {
                ipAddress = request.getRemoteAddr ();
            }
            // For the case of passing multiple agents, the first IP is the client's real IP, and multiple IPs are divided according to
            if (ipAddress! = null && ipAddress.length ()> 15) {// "***. ***. ***. ***". length ()
                // = 15
                if (ipAddress.indexOf (",")> 0) {
                    ipAddress = ipAddress.substring (0, ipAddress.indexOf (","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}
Control layer


Add@RateLimit()annotations that are generated in REDSI for 10 seconds and can be accessed 5 times by key



RedisAtomicLongis for the test example, record the cumulative number of visits, with no relationship to the current limit.


package com.souyunku.example.controller;

/ **
  * Description: Test page
  *
  * @author yanpenglei
  * @create 2018-08-16 15:42
  ** /
@RestController
public class LimiterController {

     @Autowired
     private RedisTemplate redisTemplate;

     // Access 10 times in 10 seconds
     @RateLimit (key = "test", time = 10, count = 10)
     @GetMapping ("/ test")
     public String luaLimiter () {
         RedisAtomicInteger entityIdCounter = new RedisAtomicInteger ("entityIdCounter", redisTemplate.getConnectionFactory ());

         String date = DateFormatUtils.format (new Date (), "yyyy-MM-dd HH: mm: ss.SSS");

         return date + "Accumulated visits:" + entityIdCounter.getAndIncrement ();
     }
}
Start the service
package com.souyunku.example;

@SpringBootApplication
public class SpringBootLimitApplication {

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


Start Project page access : http://127.0.0.1:8080/test



10 seconds, you can access 10 times, more than 10 times, the page error, such as enough 10 seconds, recalculate.



Background log


2018-08-16 18: 41: 08.205 INFO 18076 --- [nio-8080-exec-1] com.souyunku.example.config.LimitAspect: Access within the time limit: 1 time
2018-08-16 18: 41: 08.426 INFO 18076 --- [nio-8080-exec-3] com.souyunku.example.config.LimitAspect: Access within the current-limit time period: 2 times
2018-08-16 18: 41: 08.611 INFO 18076 --- [nio-8080-exec-5] com.souyunku.example.config.LimitAspect: Access within the current-limit period: 3 times
2018-08-16 18: 41: 08.819 INFO 18076 --- [nio-8080-exec-7] com.souyunku.example.config.LimitAspect: Access within the time limit of the current time: 4 times
2018-08-16 18: 41: 09.021 INFO 18076 --- [nio-8080-exec-9] com.souyunku.example.config.LimitAspect: Visits within the time limit: 5 times
2018-08-16 18: 41: 09.203 INFO 18076 --- [nio-8080-exec-1] com.souyunku.example.config.LimitAspect: Access within the time limit: 6 times
2018-08-16 18: 41: 09.406 INFO 18076 --- [nio-8080-exec-3] com.souyunku.example.config.LimitAspect: Access within the time limit: 7 times
2018-08-16 18: 41: 09.629 INFO 18076 --- [nio-8080-exec-5] com.souyunku.example.config.LimitAspect: Access within the time limit: 8 times
2018-08-16 18: 41: 09.874 INFO 18076 --- [nio-8080-exec-7] com.souyunku.example.config.LimitAspect: Access within the time limit: 9 times
2018-08-16 18: 41: 10.178 INFO 18076 --- [nio-8080-exec-9] com.souyunku.example.config.LimitAspect: Access within the time limit: 10 times
2018-08-16 18: 41: 10.702 ERROR 18076 --- [nio-8080-exec-1] oaccC [. [. [/]. [DispatcherServlet]: Servlet.service () for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: The number of times the current limit has been set] with root cause

java.lang.RuntimeException: has reached the limit
    at com.souyunku.example.config.LimitAspect.interceptor (LimitAspect.java:73) ~ [classes /: na]
    at sun.reflect.GeneratedMethodAccessor35.invoke (Unknown Source) ~ [na: na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) ~ [na: 1.8.0_112]
    at java.lang.reflect.Method.invoke (Method.java:498) ~ [na: 1.8.0_112] 
Recommended Reading
    • Dubbo integrated Pinpoint to do distributed service request tracking
    • Interface Current limit: Leaky bucket algorithm & token bucket algorithm
    • Java Concurrency: Application-limited practice for distributed applications
    • Java Concurrency: Semaphore semaphore Source Analysis
    • Java Concurrency: Aqs shared lock mode source code Analysis
    • Java Concurrency: The exclusive lock mode source analysis of Aqs in layman's view
    • Java Concurrency: Understanding unlocked CAs from source code analysis
    • Java Concurrency: CAS principle analysis
Contact
    • Peng Lei
    • Source: http://www.ymq.io/2018/08/16/redis-lua/
    • Copyright belongs to the author, please specify the source of the reprint
    • WeChat: Pay attention to the public number, search cloud library, focus on the development of technology research and knowledge sharing





Java Concurrency: Distributed application current limit Redis + Lua Practice


Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.