【分布式架構】--- 基於Redis組件的特性,實現一個分布式限流


分布式---基於Redis進行接口IP限流

場景 為了防止我們的接口被人惡意訪問,比如有人通過JMeter工具頻繁訪問我們的接口,導致接口響應變慢甚至崩潰,所以我們需要對一些特定的接口進行IP限流,即一定時間內同一IP訪問的次數是有限的。

實現原理 用Redis作為限流組件的核心的原理,將用戶的IP地址當Key,一段時間內訪問次數為value,同時設置該Key過期時間。

比如某接口設置相同IP10秒內請求5次,超過5次不讓訪問該接口。

1. 第一次該IP地址存入redis的時候,key值為IP地址,value值為1,設置key值過期時間為10秒。
2. 第二次該IP地址存入redis時,如果key沒有過期,那么更新value為2。
3. 以此類推當value已經為5時,如果下次該IP地址在存入redis同時key還沒有過期,那么該Ip就不能訪問了。
4. 當10秒后,該key值過期,那么該IP地址再進來,value又從1開始,過期時間還是10秒,這樣反反復復。

說明從上面的邏輯可以看出,是一時間段內訪問次數受限,不是完全不讓該IP訪問接口。

技術框架 SpringBoot + RedisTemplate (采用自定義注解完成)

這個可以用於真實項目開發場景。

一、代碼

1、自定義注解

這邊采用自定義注解的目的就是,在接口上使用自定義注解,讓代碼看去非常整潔。

IpLimiter

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IpLimiter {
    /**
     * 限流ip
     */
    String ipAdress() ;
    /**
     * 單位時間限制通過請求數
     */
    long limit() default 10;
    /**
     * 單位時間,單位秒
     */
    long time() default 1;
    /**
     * 達到限流提示語
     */
    String message();
}

2、測試接口

在接口上使用了自定義注解@IpLimiter

@Controller
public class IpController {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class);
    private static final String MESSAGE = "請求失敗,你的IP訪問太頻繁";

    //這里就不獲取請求的ip,而是寫死一個IP
    @ResponseBody
    @RequestMapping("iplimiter")
    @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE)
    public String sendPayment(HttpServletRequest request) throws Exception {
        return "請求成功";
    }
    @ResponseBody
    @RequestMapping("iplimiter1")
    @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE)
    public String sendPayment1(HttpServletRequest request) throws Exception {
        return "請求成功";
    }
}

3、處理IpLimter注解的AOP

這邊采用切面的方式處理自定義注解。同時為了保證原子性,這邊寫了redis腳本ipLimiter.lua來執行redis命令,來保證操作原子性。

@Aspect
@Component
public class IpLimterHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class);

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * getRedisScript 讀取腳本工具類
     * 這里設置為Long,是因為ipLimiter.lua 腳本返回的是數字類型
     */
    private DefaultRedisScript<Long> getRedisScript;

    @PostConstruct
    public void init() {
        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(Long.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
        LOGGER.info("IpLimterHandler[分布式限流處理器]腳本加載完成");
    }

    /**
     * 這個切點可以不要,因為下面的本身就是個注解
     */
//    @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)")
//    public void rateLimiter() {}

    /**
     * 如果保留上面這個切點,那么這里可以寫成
     * @Around("rateLimiter()&&@annotation(ipLimiter)")
     */
    @Around("@annotation(ipLimiter)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[分布式限流處理器]開始執行限流操作");
        }
        Signature signature = proceedingJoinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
        }
        /**
         * 獲取注解參數
         */
        // 限流模塊IP
        String limitIp = ipLimiter.ipAdress();
        Preconditions.checkNotNull(limitIp);
        // 限流閾值
        long limitTimes = ipLimiter.limit();
        // 限流超時時間
        long expireTime = ipLimiter.time();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[分布式限流處理器]參數值為-limitTimes={},limitTimeout={}", limitTimes, expireTime);
        }
        // 限流提示語
        String message = ipLimiter.message();
        /**
         * 執行Lua腳本
         */
        List<String> ipList = new ArrayList();
        // 設置key值為注解中的值
        ipList.add(limitIp);
        /**
         * 調用腳本並執行
         */
        Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes);
        if (result == 0) {
            String msg = "由於超過單位時間=" + expireTime + "-允許的請求次數=" + limitTimes + "[觸發限流]";
            LOGGER.debug(msg);
            // 達到限流返回給前端信息
            return message;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("IpLimterHandler[分布式限流處理器]限流執行結果-result={},請求[正常]響應", result);
        }
        return proceedingJoinPoint.proceed();
    }
}

4、RedisCacheConfig(配置類)

@Configuration
public class RedisCacheConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class);

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //使用StringRedisSerializer來序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        LOGGER.info("Springboot RedisTemplate 加載完成");
        return template;
    }
}

5、ipLimiter.lua 腳本

優點
減少網絡的開銷: 腳本只執行一次,不需要發送多次請求, 減少網絡傳輸;
保證原子操作: 整個腳本作為一個原子執行, 就不用擔心並發問題;

--獲取KEY
local key1 = KEYS[1]

local val = redis.call('incr', key1)
local ttl = redis.call('ttl', key1)

--獲取ARGV內的參數並打印
local expire = ARGV[1]
local times = ARGV[2]

redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))

redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
if val == 1 then
    redis.call('expire', key1, tonumber(expire))
else
    if ttl == -1 then
        redis.call('expire', key1, tonumber(expire))
    end
end

if val > tonumber(times) then
    return 0
end
return 1

6、application.properties

#redis
spring.redis.hostName=
spring.redis.host=
spring.redis.port=6379
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=10
spring.redis.timeout=100ms
spring.redis.password=

logging.path= /Users/xub/log
logging.level.com.jincou.iplimiter=DEBUG
server.port=8888

7、SpringBoot啟動類

@SpringBootApplication
public class Application {

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

8、測試

完美上面這個測試非常符合我們的預期,前五次訪問接口是成功的,后面就失敗了,直到10秒后才可以重新訪問,這樣反反復復。

其它的這邊就不一一展示了,附上該項目源碼。

Github地址 https://github.com/yudiandemingzi/spring-boot-redis-ip-limiter


參考

這個設計是我在刷github的時候看到確實很好,我這邊只是在它的基礎上做了一些改動,非常感謝該作者的分享。
github地址:https://github.com/TaXueWWL/shleld-ratelimter

有關AOP有篇文章講的不錯:spring aop 中@annotation()的使用



只要自己變優秀了,其他的事情才會跟着好起來(中將1)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM