上期回顾: Java Seckill Module:Digital formula verification code
接口限流防刷,思路:对接口做限流,利用拦截器减少对业务侵入。
利用计时器比较复杂,但是利用缓存并设置有效期这样的话就简单多了。
就是说用户访问接口的时候,把访问的次数写入到缓存中去,同时给数据加个有效期(60s),如果一分钟之内继续访问就继续+1,如果一分钟超过了我们限制的数量,就返回失败,如果没有超过,到了下一个一分钟,数据就重新从0开始计算。
我们点了秒杀之后,第一步就是请求秒杀路径,拿到路径的时候,随后就是进行秒杀。
假设这个接口我们限制一分钟只能点击60次,在获取秒杀地址之前,需要先查询访问次数, 从redis中获取的key是url拼上用户id,也就是这个用户访问这个url,所以需要先获取访问路径,再拼上用户id,用这个来当做key,value就是integer类型的。此时就获取了访问次数了,下面进行判断,如果访问次数是空,则给redis中的这个key的value设置为1,如果访问次数小于5,则做个++操作,如果是超过5的,则直接返回失败,并且后面就不会继续执行了。
在方法上加个拦截器,拦截请求次数:
@AccessLimit(seconds=5, maxCount=5, needLogin=true)5秒限制请求5次,并且是需要在登录的前提下,对于业务方法,我们只需要加上这个注解即可,不需要在方法里面都去实现限流的这么一个逻辑:
@Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); boolean needLogin() default true; }接着就是定义一个拦截器:在执行方法之前进行拦截,所以需要实现这个方法
@Service public class AccessInterceptor extends HandlerInterceptorAdapter{ @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return true;} ***** @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(handler instanceof HandlerMethod) { MiaoshaUser user = getUser(request, response); UserContext.setUser(user); HandlerMethod hm = (HandlerMethod)handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null) { return true; } int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean needLogin = accessLimit.needLogin(); String key = request.getRequestURI(); if(needLogin) { if(user == null) { render(response, CodeMsg.SESSION_ERROR); return false; } key += "_" + user.getId(); }else {} AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); if(count == null) { redisService.set(ak, key, 1); }else if(count < maxCount) { redisService.incr(ak, key); }else { render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } } return true; } 解读:首先判断handler是否属于HandlerMethod,如果是则先获取用户,而后进行下强转,接着可以拿到方法上的注解,如果没有获取这个注解则直接返回true,就是表示不做任何限制,如果有这个注解,接着我们就是获取这个注解上的数据,访问多少秒内能够访问多少次,以及需要登录,如何获取用户呢?需要先保存用户: public class UserContext { private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>(); public static void setUser(MiaoshaUser user) {userHolder.set(user); } public static MiaoshaUser getUser() {return userHolder.get(); } } 解读:创建线程容器,将用户放入容器,再从容器中获取,ThreadLocal多线程下会保护线程的安全,会与线程进行绑定,加入东西,也只能加入绑定的这个线程,也就是线程安全的,每个线程单独存一份。 接着在UserArgumentResolver中可以直接去取: public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return UserContext.getUser(); } 拦截器会首先执行,而参数UserArgumentResolver是后执行的,前面设置了之后,后面就会取到了,前面没设置上,后面也取不到。因为是同一个线程执行,收到请求之后,同请求都到响应完成这个过程都是一个线程在执行的,所以是肯定能取到的。 流程:可以传参(ThreadLocal<MiaoshaUser>)将AccessInterceptor参数传到UserArgumentResolver。 获取用户还有访问的注解之后,下面就开始进行逻辑实现: if(needLogin) { if(user == null) { render(response, CodeMsg.SESSION_ERROR); return false; } key += "_" + user.getId(); }else {} AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); if(count == null) { redisService.set(ak, key, 1); }else if(count < maxCount) { redisService.incr(ak, key); }else { render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } 解读:首先判断是否需要登录,如果没有登录则不做任何操作,登录了之后就判断是否取得该用户,并且拼上key,如果没有取得,就不进入页面,而是给用户一个提示(客户端错误提示): private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } 解读:把对象转化成json数据给写出去。 AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); if(count == null) { redisService.set(ak, key, 1); }else if(count < maxCount) { redisService.incr(ak, key); }else { render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } 解读:key封装好了之后,开始获取访问路径,再拼上用户id,用这个来当做key,value就是integer类型的。此时就获取了访问次数了,下面进行判断,如果访问次数是空,则给redis中的这个key的value设置为1,如果访问次数小于最大值,则做个++操作,如果是超过最大值的,则直接返回失败,并且后面就不会继续执行了。 public class AccessKey extends BasePrefix{ private AccessKey( int expireSeconds, String prefix) { super(expireSeconds, prefix); } public static AccessKey withExpire(int expireSeconds) { return new AccessKey(expireSeconds, "access"); } }webconfig:注册拦截器
@Autowired AccessInterceptor accessInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor); }