基于redis实现IP访问频次控制(超简单)

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 基于redis实现IP访问频次控制(超简单)

一、背景描述



最近做了一个项目,通过调用短信网关实现手机号和验证码登录。但是如果有人恶意调用,会发出很多无谓的短信,而且会增加短信服务的费用。于是对接口做了请求频率限制 Rate limiting。例如限制一个用户1分钟内最多可以范围100次。这里简要的复现一下实现思路。


思路:以类名+调用方法名+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


  1. 配置类
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);
}


  1. 实现类
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;
}


  1. 切面类
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";
    }
}


四、测试结果



测试


2020-09-22 00:24:21.786  INFO 30552 --- [nio-8080-exec-3] c.e.r.annotation.LimitedAccessAspect     : remoteAddr地址:0:0:0:0:0:0:0:1
2020-09-22 00:24:22.417  INFO 30552 --- [nio-8080-exec-9] c.e.r.annotation.LimitedAccessAspect     : remoteAddr地址:0:0:0:0:0:0:0:1
2020-09-22 00:24:22.419  INFO 30552 --- [nio-8080-exec-9] c.e.r.annotation.LimitedAccessAspect     : 接口调用过于频繁 LIMITED_ACCESS_ASPECT_COLLECTIONcom.example.redis_demo_limit.controller.RedisController.add#0:0:0:0:0:0:0:1


从测试结果来看,它第二次访问的时候,直接返回了接口访问过于频繁。所以实现了接口访问频次的控制。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
NoSQL 关系型数据库 Redis
Docker的通俗理解和通过宿主机端口访问Redis容器的实例
本文目标:引导初学者入门Docker,理解镜像、容器和宿主机概念,学习常用Docker命令,特别是如何创建并从Redis容器通过宿主机端口访问。 关键点: - Docker核心:镜像(类)、容器(实例)、宿主机(运行环境)。 - `docker pull` 拉取镜像,如 `redis:3.0`。 - `docker run -d --name` 后台运行容器,如 `my-redis`。 - `-p` 参数做端口映射,如 `6379:6379`。 - `docker exec -it` 交互式进入容器,如 `bash` 或执行命令。
208 4
|
8天前
|
NoSQL 关系型数据库 MySQL
无法访问Docker 里的 mysql, redis
无法访问Docker 里的 mysql, redis
9 0
|
1月前
|
数据库 NoSQL Redis
Redis访问模式
【7月更文挑战第17天】
32 2
|
22天前
|
NoSQL Redis
Redis 使用 hyperLogLog 实现请求ip去重的浏览量
Redis 使用 hyperLogLog 实现请求ip去重的浏览量
27 0
|
1月前
|
前端开发 NoSQL 数据库
部署常用的流程,可以用后端,连接宝塔,将IP地址修改好,本地只要连接好了,在本地上前后端跑起来,前端能够跑起来,改好了config.js资料,后端修改好数据库和连接redis,本地上跑成功了,再改
部署常用的流程,可以用后端,连接宝塔,将IP地址修改好,本地只要连接好了,在本地上前后端跑起来,前端能够跑起来,改好了config.js资料,后端修改好数据库和连接redis,本地上跑成功了,再改
|
2月前
|
存储 NoSQL Redis
多次访问redis造成redis连接断开的解决方案
多次访问redis造成redis连接断开的解决方案
29 2
|
2月前
|
存储 监控 NoSQL
Redis系列学习文章分享---第十二篇(搭建哨兵集群+RedisTemplate连接哨兵+搭建分片集群+-散列插槽+集群伸缩 +故障转移+RedisTemplate访问分片集群)
Redis系列学习文章分享---第十二篇(搭建哨兵集群+RedisTemplate连接哨兵+搭建分片集群+-散列插槽+集群伸缩 +故障转移+RedisTemplate访问分片集群)
114 0
|
2月前
|
NoSQL 网络安全 Redis
redis未授权访问利用汇总
redis未授权访问利用汇总
|
2月前
|
NoSQL 关系型数据库 Serverless
Serverless 应用引擎产品使用合集之连接RDS、Redis等数据库时,是否需要通过安全组来控制访问权限
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
|
3月前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
63 0