如果是简单做一个限流器的话,利用zset也可以实现,以score作为时间戳,然后的话,value为任意(保证不重复即可),因为重点还是在于score,然后在访问请求的时候,通过zset的的zscore指令,判断出某个时间段内的成员数,就可以模拟,某个时间段,被请求了多少次,然后是否进行控制。
模拟一下隔壁老王敲门,假设score为时间戳,value的话,那么可以看出 3秒内,老王敲了3次门。
127.0.0.1:6379> ZCOUNT laowang:qiaomen 0 1
(integer) 1
127.0.0.1:6379> ZCOUNT laowang:qiaomen 1 3
(integer) 3
127.0.0.1:6379> del key laowang:qiaomen
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 1 "first"
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 2 "second"
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 3 "third"
(integer) 1
127.0.0.1:6379> ZCARD laowang:qiaomen
(integer) 3
127.0.0.1:6379> ZCOUNT laowang:qiaomen 1 3
(integer) 3
127.0.0.1:6379>
这样子就简单模拟了这个过程,但是存在许多问题,如浪费内存,如果不进行内存的浪费的话,就要频繁去修改这个zset,因为对我们来说,只有某个时间段的数据是有意义的。
那么剩下的一步呢?如何进行控制,当然是根据时间段统计出来的次数,多于我们限定的值,就返回异常信息,或者是拒绝请求。
如果我们用ZSET来实现的话,这里面就涉及到了,增改查,逻辑略复杂,操作涉及到的资源也比较多。
我们可以通过redis提供的CL.THROTTLE来实现。
这里首先是需要下载redis-cell的插件
先来简单使用一下这个指令
CL.THROTTLE key 容量 释放数 周期
//这个指令的意思是 容量为1 然后每次释放一个容量 周期为10秒
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 0 //0为成功 1为失败
2) (integer) 1 //最大容量
3) (integer) 0 //剩余容量
4) (integer) -1 //多久之后有空间 第一次加进去为-1 就是整个周期
5) (integer) 10 //多久之后容器完全为空
//几秒钟之后再试
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1 //失败
2) (integer) 1 //最大容量还是1
3) (integer) 0 //剩余容量
4) (integer) 6 //6秒之后有空间
5) (integer) 6 //6秒之后容器完全为空,因为容量为1所以都是6秒
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1
2) (integer) 1
3) (integer) 0
4) (integer) 3
5) (integer) 3
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1
2) (integer) 1
3) (integer) 0
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 0
2) (integer) 1
3) (integer) 0
4) (integer) -1
5) (integer) 10
下面贴代码
首先是限流器的内部实现
@Component
public class RequestFunnel {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean ifCanRequest(String url, String userName, String containerNum, String releaseNum, String period) {
String key = url + ":" + userName;
String script = "return redis.call('CL.THROTTLE', KEYS[1], ARGV[1], ARGV[2], ARGV[3])";
List<Long> rs = redisTemplate
.execute((RedisCallback<List<Long>>) con -> con.eval(script.getBytes(), ReturnType.MULTI, Constants.ONE,
key.getBytes(), containerNum.getBytes(), releaseNum.getBytes(), period.getBytes()));
if (Constants.ZERO == rs.get(Constants.ZERO).intValue()) {
return true;
}
return false;
}
}
然后结合切面,用于控制请求
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestFunnel {
String url();
int containerMaxNum();
int releaseNum();
int releasePeriod();
}
@Aspect
@Component
public class RequestFunnelAspect {
@Autowired
private RequestFunnelUtils requestFunnelUtils;
@Pointcut("@annotation(com.huangzhaoji.redis.annotation.RequestFunnel)")
public void pointCut() {
};
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object obj = joinPoint.getTarget();
Method[] methods = obj.getClass().getMethods();
Method method = null;
for(Method methodItem : methods) {
if(methodItem.getName().equals(methodName)) {
method = methodItem;
break;
}
}
RequestFunnel requestFunnel = method.getAnnotation(RequestFunnel.class);
String url = requestFunnel.url();
Integer containerMaxNum = requestFunnel.containerMaxNum();
Integer releaseNum = requestFunnel.releaseNum();
Integer releasePeriod = requestFunnel.releasePeriod();
//这个username其实跟id一样,可以通过不写死的方式获取的,可以通过threadLocal建立线程与请求的关系,从请求中获取到对应的userId或者名字,这样子就不用写死了
boolean canVisit = requestFunnelUtils.ifCanRequest(url, "老王", containerMaxNum.toString(), releaseNum.toString(), releasePeriod.toString());
if(!canVisit) {
//throw new MyException("访问次数过多,请稍后重试");
//如果是在实际业务场景中,这里肯定是抛一个异常,然后通过全局异常处理器捕获,最后返回自定义的异常响应信息
System.out.println("访问次数过多,请稍后重试");
}
}
}
编写一个业务类
public interface UserService {
public void visit();
}
@Component
public class UserServiceImpl implements UserService{
@Override
@RequestFunnel(url = "visit", containerMaxNum = 1, releaseNum = 1, releasePeriod = 10)
public void visit() {
System.out.println("visit");
}
}
测试方法
public class RequestFunnelUtilsTest extends TestApplication{
@Autowired
private UserService userService;
@Test
public void testFunnel() {
for (int i = 0; i < 10; i++) {
userService.visit();
}
}
}
最后输出
visit
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
网友评论