前提场景
根据业务要求,需要实现一个针对IP级别的请求资源池,在1分钟之内,如果对同一个url请求超过1000次,则认为非法请求.对ip进行一个小时的锁死,很容易就想到用redis来实现.
Java代码实现
1.使用redis的string数据类型,记录请求次数
2.如果超过次数,记录ip
黑名单实现
String countKey = "IP:REQUEST:COUNT:"+url+":"+ip;
String blackKey = "IP:BLACK:"+url+":"+ip;
Object obj = redisTemplate.opsForValue().get(countKey );
if(obj==null||obj.equals("")){
//第一次,新增一个计数
redisTemplate.opsForValue().set(countKey ,1,60, TimeUnit.SECONDS);
}else{
if(Integer.parseInt(obj.toString())>1000){
//超过指定的次数,加入黑名单
redisTemplate.opsForValue().set(blackKey ,1,60*, TimeUnit.SECONDS);
}else{
//每次增1
redisTemplate.opsForValue().increment(countKey , 1)
}
}
黑名单验证
String blackKey = "IP:BLACK:"+url+":"+ip;
Object obj = redisTemplate.opsForValue().get(blackKey);
if(obj==null||obj.equals("")){
//未加入黑名单
//通过,继续执行后边的逻辑
}else{
//在黑名单中
//抛出异常 403
}
问题现象
起初,这段逻辑在运行了一段时间没有发生问题,直到有同学提出,自己的请求一直报错,提示403
问题排查
在排除了是前端使用错了请求方法的前提下,定位到问题是被加入IP黑名单了导致的,但是前端的请求量固定的1分钟60次,结合日志查看,远远没有达到1000次的请求量。重新梳理黑名单的逻辑,想要黑名单校验通过,必须黑名单有值,而黑名单有值,需要计数器达到1000,于是连接Redis,查看计数器的值,确实达到了1000,但同时也注意到计数器的过期时间变成了-1.
问题显然出在了过期时间上。
再次排查黑名单逻辑,很快便发现了问题,每次做增量之前,会先获取到这个值,那么就有可能在获取的时候没有过期,在做增量的时候就已经过期了,因此再做增量的时候,没给指定过期时间,就会导致过期时间变成永久。
问题修复
String countKey = "IP:REQUEST:COUNT:"+url+":"+ip;
String blackKey = "IP:BLACK:"+url+":"+ip;
Object obj = redisTemplate.opsForValue().get(countKey );
if(obj==null||obj.equals("")){
//第一次,新增一个计数
redisTemplate.opsForValue().set(countKey ,1,60, TimeUnit.SECONDS);
}else{
if(Integer.parseInt(obj.toString())>1000){
//超过指定的次数,加入黑名单
redisTemplate.opsForValue().set(blackKey ,1,60*, TimeUnit.SECONDS);
}else{
//每次增1
redisTemplate.opsForValue().increment(countKey , 1)
//在增量完成后,取一遍剩余过期时间,如果是-1,重新指定过期时间
Long expire = redisTemplate.getExpire(countKey);
if(-1 == expire){
redisTemplate.expire(countKey ,60,TimeUnit.SECONDS);
}
}
}
总结
逻辑不严谨!