前提场景

根据业务要求,需要实现一个针对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);
  		}
	}
}

总结

逻辑不严谨!