Redis In addition to caching , There are many things to do : Distributed lock , Current limiting , Idempotency of processing request interface ... Too much too much ~

I want to talk with my friends today Redis Processing interface current limiting , This is also recent This knowledge point is involved in the project , I'll bring it out and talk about this topic with you .



1. preparation

First we create a Spring Boot engineering , introduce Web and Redis rely on , At the same time, considering that the interface current limit is generally marked by annotations , And annotation is through AOP
To resolve , So we need to add AOP Dependency of , The final dependencies are as follows :
<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>
    <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Then prepare one in advance Redis example , Here, after our project is configured , Configure it directly Redis Basic information of , as follows : spring.redis.port=6379 spring.redis.password=123
All right , The preparatory work is in place .

2. Current limiting notes

Next, we create a current limiting annotation , We divide current limiting into two cases :

Global current limiting for the current interface , For example, this interface can be used in 1 Access in minutes 100 second .

For one IP Current limiting of address , For example, a IP The address can be 1 Access in minutes 100 second .

For these two cases , We create an enumeration class :
public enum LimitType {     /**      *  Default policy global flow limit      */     DEFAULT,     /**
     *  According to requestor IP Conduct current limiting      */     IP }
Next, let's create a current limiting annotation :
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented
public @interface RateLimiter {     /**      *  Current limiting key      */
    String key() default "rate_limit:";     /**      *  Current limiting time , Unit: Second      */
    int time() default 60;     /**      *  Current limiting times      */
    int count() default 100;     /**      *  Current limiting type      */
    LimitType limitType() default LimitType.DEFAULT; }
The first parameter is current limiting key, This is just a prefix , Complete in the future key Is this prefix plus the full path of the interface method , Together form current limiting key, this key Will be saved to
Redis in .

The other three parameters are easy to understand , I won't say more .

okay , Which interface needs current limitation in the future , Add on which interface  @RateLimiter  annotation , Then configure relevant parameters .

3. customized RedisTemplate

My friends know , stay Spring Boot in , We are actually more used to it Spring Data Redis To operate Redis, But by default
RedisTemplate There is a small pit , Serialization uses
JdkSerializationRedisSerializer, I don't know if my friends have noticed , Directly use this serialization tool to save to Redis On key and
value Will be inexplicably more prefixes , This may cause errors when you read with the command .

For example, when storing ,key yes name,value yes javaboy, But when you operate on the command line ,get name  But you can't get the data you want , The reason is to save to
redis after name There are some more characters in front , It can only be used at this time RedisTemplate Read it out .

We use Redis It will be used for current limiting Lua script , use Lua Script time , The situation mentioned above will occur , So we need to modify RedisTemplate
Serialization scheme for .
Maybe some friends will say ” Why not StringRedisTemplate And ?”StringRedisTemplate
There really is no problem mentioned above , But the data types it can store are not rich enough , So it's not considered here .

modify RedisTemplate Serialization scheme , The code is as follows :
@Configuration public class RedisConfig {     @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        //  use Jackson2JsonRedisSerialize  Replace default serialization ( The default is JDK serialize )
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        return redisTemplate;     } }
There's nothing to say about this ,key and value We all use Spring Boot Default in jackson Serialization method .

4. development Lua script

Redis We can use some atomic operations in Lua Script to implement , Want to call Lua script , We have two different ideas :

stay Redis The server is well defined Lua script , Then calculate a hash value , stay Java In code , Lock which to execute with this hash value Lua script .

Directly in Java In the code Lua Well defined script , Then send to Redis The server executes it .

Spring Data Redis Operations are also provided in Lua Interface of script , It's quite convenient , So we will adopt the second scheme here .

We are resources New under directory lua The folder is specially used for storing lua script , The script is as follows :
local key = KEYS[1] local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2]) local current ='get', key)
if current and tonumber(current) > count then     return tonumber(current) end
current ='incr', key) if tonumber(current) == 1 then'expire', key, time) end return tonumber(current)
This script is not difficult , You probably know what to do at a glance .KEYS and ARGV Are parameters passed in when calling later ,tonumber
Is to convert a string into a number , Is to implement specific redis instructions , The specific process is as follows :

First get the incoming key as well as Current limiting count And time time.

adopt get Get this key Corresponding value , This value is how many times this interface can be accessed in the current time window .

If this is the first visit , The result obtained at this time is nil, Otherwise, the result should be a number , So let's judge next , If the result is a number , And this number is greater than
count, That means the flow limit has been exceeded , Then directly return the query result .

If the result is nil, Description is the first visit , Now give the current key Self accretion 1, Then set an expiration time .

Finally, self increase 1 The returned value is OK .

Actually, this paragraph Lua The script is easy to understand .

Next, we are in a Bean To load this paragraph Lua script , as follows :
@Bean public DefaultRedisScript<Long> limitScript() {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
    redisScript.setResultType(Long.class);     return redisScript; }
ours Lua The script is ready now .

5. Annotation parsing

Next, we need to customize the section , To parse this annotation , Let's look at the definition of section :
@Aspect @Component public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    @Autowired     private RedisTemplate<Object, Object> redisTemplate;
    @Autowired     private RedisScript<Long> limitScript;
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();         int time = rateLimiter.time();
        int count = rateLimiter.count();
        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number==null || number.intValue() > count) {
                throw new ServiceException(" Visit too often , Please try again later ");             }
  " Limit request '{}', Current request '{}', cache key'{}'", count, number.intValue(), key);
        } catch (ServiceException e) {             throw e;
        } catch (Exception e) {
            throw new RuntimeException(" Server current limit exception , Please try again later ");         }     }
    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        return stringBuffer.toString();     } }
This section is to intercept all additions  @RateLimiter  Annotation method , Processing annotations in pre notification .

First, get the key,time as well as count Three parameters .

Get a combined key, The so-called combined key, It's in annotation key Attribute based , Plus the full path of the method , If it is IP Mode words , Just add IP address . with IP
Mode as an example , Finally generated key Like this :
( If not IP pattern , Then generated key Not included in IP address ).

To be generated key Put in the set .

adopt redisTemplate.execute Method to execute one Lua script , The first parameter is the object encapsulated by the script , The second parameter is key, Corresponds to the
KEYS, Followed by variable length parameters , Corresponds to the ARGV.

take Lua The results of script execution and count Compare , If greater than count, It means overload , Just throw exceptions .

okay , It's done .

6. Interface test

Next, we will conduct a simple test of the interface , as follows :
@RestController public class HelloController {     @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {         return "hello>>>"+new Date();     } }
every last IP address , stay 5 Only accessible within seconds 3 second .

This can be tested by manually refreshing the browser .

7. Global exception handling

Because when overloaded, it is thrown abnormally , So we also need a global exception handler , as follows :
@RestControllerAdvice public class GlobalException {
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);         map.put("message", e.getMessage());
        return map;     } }
This is a small demo, I won't define entity classes , Direct use Map To return JSON Yes .

All right , be accomplished .

Finally, let's look at the test effect under overload :

All right , This is what we use Redis Way of current limiting .

©2019-2020 Toolsou All rights reserved,
Solve in servlet The Chinese output in is a question mark C String function and character function in language MySQL management 35 A small coup optimization Java performance —— Concise article Seven sorting algorithms (java code ) use Ansible Batch deployment SSH Password free login to remote host according to excel generate create Build table SQL sentence Spring Source code series ( sixteen )Spring merge BeanDefinition Principle of Virtual machine installation Linux course What are the common exception classes ?