基於redis實現IP訪問頻次控制
阿新 • • 發佈:2021-01-21
一、背景描述
思路:以類名+呼叫方法名+ip作為key
- 當用戶呼叫介面的時候,先查詢redis中是否有存在該key,獲取該key所對應的value,比較value和frequency,如果小於frequency,則在原來的基礎上value++;如果大於則返回訪問頻率過於頻繁。
- 如果不存在,則將該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的配置
- 配置檔案
#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操作工具類
- 介面類
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); }
- 實現類
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. 訪問頻次實現核心邏輯
- 註解
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; }
- 切面類
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"; } }
- 註解