基于redis实现IP访问频次控制

一、背景描述

思路:以类名+调用方法名+ip作为key

  1. 当用户调用接口的时候,先查询redis中是否有存在该key,获取该key所对应的value,比较value和frequency,如果小于frequency,则在原来的基础上value++;如果大于则返回访问频率过于频繁。
  2. 如果不存在,则将该key存入redis,value为1,设置过期时间。

二、代码演示

1. pom文件

<!--引入web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring boot 测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--分布式锁-->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>2.3.0</version>
</dependency>
<!--连接池-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.0</version>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!-- swagger -->
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<!--aop-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

2、redis的配置

    1.  配置文件
      #redis
      redis.host=192.168.1.6
      redis.password=
      redis.port=6379
      redis.taskScheduler.poolSize=100
      redis.taskScheduler.defaultLockMaxDurationMinutes=10
      redis.default.timeout=10
      redisCache.expireTimeInMilliseconds=1200000
       配置类
package com.example.redis_demo_limit.redis;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;
import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;

@Configuration
public class RedisConfig {
    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Value("${redis.password}")
    private String password;

    @Value("${redis.taskScheduler.poolSize}")
    private int tasksPoolSize;
    @Value("${redis.taskScheduler.defaultLockMaxDurationMinutes}")
    private int lockMaxDuration;

    @Bean(destroyMethod = "shutdown")
    ClientResources clientResources() {
        return DefaultClientResources.create();
    }

    @Bean
    public RedisStandaloneConfiguration redisStandaloneConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration(redisHost, redisPort);
        if (password != null && !password.trim().equals("")) {
            RedisPassword redisPassword = RedisPassword.of(password);
            redisStandaloneConfiguration.setPassword(redisPassword);
        }
        return redisStandaloneConfiguration;
    }

    @Bean
    public ClientOptions clientOptions() {
        return ClientOptions.builder()
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
                .autoReconnect(true).build();
    }

    @Bean
    LettucePoolingClientConfiguration lettucePoolConfig(ClientOptions options, ClientResources dcr) {
        return LettucePoolingClientConfiguration.builder().poolConfig(new GenericObjectPoolConfig())
                .clientOptions(options).clientResources(dcr).build();
    }

    @Bean
    public RedisConnectionFactory connectionFactory(
            RedisStandaloneConfiguration redisStandaloneConfiguration,
            LettucePoolingClientConfiguration lettucePoolConfig) {
        return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolConfig);
    }

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
        return new RedisLockProvider(connectionFactory);
    }

    @Bean
    public ScheduledLockConfiguration taskSchedulerLocker(LockProvider lockProvider) {
        return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider)
                .withPoolSize(tasksPoolSize).withDefaultLockAtMostFor(Duration.ofMinutes(lockMaxDuration))
                .build();
    }
}

3. redis操作工具类

    1. 接口类
      package com.example.redis_demo_limit.redis;
      
      
      public interface DataCacheRepository<T> {
      
        boolean add(String collection, String hkey, T object, Long timeout);
      
        boolean delete(String collection, String hkey);
      
        T find(String collection, String hkey, Class<T> tClass);
      
        Boolean isAvailable();
      
        /**
         * redis 加锁
         * 
         * @param key
         * @param second
         * @return
         */
        Boolean lock(String key, String value, Long second);
      
        Object getValue(String key);
      
        /**
         * redis 解锁
         * 
         * @param key
         * @return
         */
        void unLock(String key);
      
        void setIfAbsent(String key, long value, long ttl);
      
        void increment(String key);
      
        Long get(String key);
      
        void set(String key, long value, long ttl);
      
        void set(Object key, Object value, long ttl);
      
        Object getByKey(String key);
      
      
        void getLock(String key, String clientID) throws Exception;
      
        void releaseLock(String key, String clientID);
      }

       

    2. 实现类
      package com.example.redis_demo_limit.redis;
      
      import com.fasterxml.jackson.databind.ObjectMapper;
      import lombok.extern.slf4j.Slf4j;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.core.ValueOperations;
      import org.springframework.data.redis.support.atomic.RedisAtomicLong;
      import org.springframework.stereotype.Repository;
      
      import java.time.Duration;
      import java.util.TimeZone;
      import java.util.concurrent.TimeUnit;
      
      @Slf4j
      @Repository
      public class CacheRepository<T> implements com.example.redis_demo_limit.redis.DataCacheRepository<T> {
      
        private static final ObjectMapper OBJECT_MAPPER;
        private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC");
      
        static {
          OBJECT_MAPPER = new ObjectMapper();
          OBJECT_MAPPER.setTimeZone(DEFAULT_TIMEZONE);
        }
      
        Logger logger = LoggerFactory.getLogger(CacheRepository.class);
        @Autowired
        RedisTemplate template; // and we're in business
        @Value("${redis.default.timeout}00")
        Long defaultTimeOut;
      
        public boolean addPermentValue(String collection, String hkey, T object) {
          try {
            String jsonObject = OBJECT_MAPPER.writeValueAsString(object);
            template.opsForHash().put(collection, hkey, jsonObject);
            return true;
          } catch (Exception e) {
            logger.error("Unable to add object of key {} to cache collection '{}': {}", hkey, collection,
                e.getMessage());
            return false;
          }
        }
      
        @Override
        public boolean add(String collection, String hkey, T object, Long timeout) {
      
          Long localTimeout;
          if (timeout == null) {
            localTimeout = defaultTimeOut;
          } else {
            localTimeout = timeout;
          }
          try {
            String jsonObject = OBJECT_MAPPER.writeValueAsString(object);
            template.opsForHash().put(collection, hkey, jsonObject);
            template.expire(collection, localTimeout, TimeUnit.SECONDS);
            return true;
          } catch (Exception e) {
            logger.error("Unable to add object of key {} to cache collection '{}': {}", hkey, collection,
                e.getMessage());
            return false;
          }
        }
      
        @Override
        public boolean delete(String collection, String hkey) {
          try {
            template.opsForHash().delete(collection, hkey);
            return true;
          } catch (Exception e) {
            logger.error("Unable to delete entry {} from cache collection '{}': {}", hkey, collection,
                e.getMessage());
            return false;
          }
        }
      
        @Override
        public T find(String collection, String hkey, Class<T> tClass) {
          try {
            String jsonObj = String.valueOf(template.opsForHash().get(collection, hkey));
            return OBJECT_MAPPER.readValue(jsonObj, tClass);
          } catch (Exception e) {
            if (e.getMessage() == null) {
              logger.error("Entry '{}' does not exist in cache", hkey);
            } else {
              logger.error("Unable to find entry '{}' in cache collection '{}': {}", hkey, collection,
                  e.getMessage());
            }
            return null;
          }
        }
      
        @Override
        public Boolean isAvailable() {
          try {
            return template.getConnectionFactory().getConnection().ping() != null;
          } catch (Exception e) {
            logger.warn("Redis server is not available at the moment.");
          }
          return false;
        }
      
        @Override
        public Boolean lock(String key, String value, Long second) {
          Boolean absent = template.opsForValue().setIfAbsent(key, value, second, TimeUnit.SECONDS);
          return absent;
        }
      
        @Override
        public Object getValue(String key) {
          return template.opsForValue().get(key);
        }
      
        @Override
        public void unLock(String key) {
          template.delete(key);
        }
      
        @Override
        public void increment(String key) {
          RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
          counter.incrementAndGet();
        }
      
        @Override
        public void setIfAbsent(String key, long value, long ttl) {
          ValueOperations<String, Object> ops = template.opsForValue();
          ops.setIfAbsent(key, value, Duration.ofSeconds(ttl));
        }
      
        @Override
        public Long get(String key) {
          RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
          return counter.get();
        }
      
        @Override
        public void set(String key, long value, long ttl) {
          RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
          counter.set(value);
          counter.expire(ttl, TimeUnit.SECONDS);
        }
      
        @Override
        public void set(Object key, Object value, long ttl) {
          template.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
        }
      
        @Override
        public Object getByKey(String key) {
          return template.opsForValue().get(key);
        }
      
        @Override
        public void getLock(String key, String clientID) throws Exception {
          Boolean lock = false;
      
          // 重试3次,每间隔1秒重试1次
          for (int j = 0; j <= 3; j++) {
            lock = lock(key, clientID, 10L);
            if (lock) {
              log.info("获得锁》》》" + key);
              break;
            }
            try {
              Thread.sleep(5000);
            } catch (InterruptedException e) {
              log.error("线程休眠异常", e);
              break;
            }
          }
          // 重试3次依然没有获取到锁,那么返回服务器繁忙,请稍后重试
          if (!lock) {
            throw new Exception("服务繁忙");
          }
        }
      
        @Override
        public void releaseLock(String key, String clientID) {
          if (clientID.equals(getByKey(key))) {
            unLock(key);
          }
        }
      }

      4. 访问频次实现核心逻辑

        1. 注解
          package com.example.redis_demo_limit.annotation;
          
          import java.lang.annotation.*;
          
          @Documented
          @Target(ElementType.METHOD)
          @Retention(RetentionPolicy.RUNTIME)
          public @interface LimitedAccess {
            /**
             * 从第一次访问接口的时间到周期时间内,最大访问频率次,默认60次
             * @return
             */
            long frequency() default 60;
          
            /**
             * 周期时间,默认30分钟内
             * @return
             */
            long second() default 30*60;
          }

           

        2. 切面类
          package com.example.redis_demo_limit.annotation;
          
          import com.example.redis_demo_limit.redis.DataCacheRepository;
          import lombok.extern.log4j.Log4j2;
          import org.aspectj.lang.ProceedingJoinPoint;
          import org.aspectj.lang.annotation.Around;
          import org.aspectj.lang.annotation.Aspect;
          import org.aspectj.lang.annotation.Pointcut;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.stereotype.Component;
          import org.springframework.web.context.request.RequestContextHolder;
          import org.springframework.web.context.request.ServletRequestAttributes;
          
          import javax.servlet.http.HttpServletRequest;
          
          @Aspect
          @Component
          @Log4j2
          //@Order
          public class LimitedAccessAspect {
          
              public static String LIMITED_ACCESS_ASPECT_COLLECTION = "LIMITED_ACCESS_ASPECT_COLLECTION";
          
              @Autowired
              private DataCacheRepository redisCacheService;
          
              @Pointcut("@annotation(limitedAccess)")
              public void limitAccessPointCut(LimitedAccess  limitedAccess) {
                  // 限制接口调用切面类
              }
          
              @Around(value = "limitAccessPointCut(limitedAccess)", argNames = "point,limitedAccess")
              public Object doAround(ProceedingJoinPoint point, LimitedAccess limitedAccess) throws Throwable {
                  ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                  if (null != attributes) {
                      String className = point.getTarget().getClass().getName();
                      String methodName = point.getSignature().getName();
                      HttpServletRequest request = attributes.getRequest();
                      String remoteAddr = request.getRemoteAddr();
                      log.info("remoteAddr地址:" + remoteAddr);
                      //String realRequestIps = request.getHeader("X-Forwarded-For");
          
          
                      String key = LIMITED_ACCESS_ASPECT_COLLECTION + className + "." + methodName + "#" + remoteAddr;
                      try {
                          long limit = redisCacheService.get(key);
                          if (limit > 0) {
                              // 时间段内超过访问频次上限 - 阻断
                              if (limit >= limitedAccess.frequency()) {
                                  log.info("接口调用过于频繁 {}", key);
          //
                                  return "接口调用过于频繁!!!";
                              }
                              redisCacheService.increment(key);
                          } else {
                              redisCacheService.set(key, 1, limitedAccess.second());
                          }
                      } catch (Exception e) {
                          log.debug(e.getStackTrace());
                      }
                  }
                  return point.proceed();
              }
          }

          三、调用方法

          package com.example.redis_demo_limit.controller;
          
          import com.example.redis_demo_limit.annotation.LimitedAccess;
          import com.example.redis_demo_limit.redis.DataCacheRepository;
          import org.springframework.web.bind.annotation.PostMapping;
          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;
          
          import javax.annotation.Resource;
          
          @RestController
          @RequestMapping("/redis")
          public class RedisController {
          
              @Resource
              private DataCacheRepository dataCacheRepository;
          
              //这个设置为1秒1次,方便测试
              @LimitedAccess(frequency = 1,second = 1)
              @PostMapping("/add")
              public String add(String str){
                  dataCacheRepository.set("str","add success",200L);
                  return "success";
              }
          }

          原文链接:https://zhuanlan.zhihu.com/p/257455401

       

       
posted @ 2021-01-21 16:36  陈扬天  阅读(466)  评论(0编辑  收藏  举报