《RabbitMQ》《Spring》《SpringMVC》《项目实战》
文章目录
- 前言
- 一、接口限流
- 自定义注解
- Redis+Lua脚本+拦截器
- 二、验证码
- 总结
前言
限流是秒杀业务最常用的手段。限流是从用户访问压力的角度来考虑如何应对系统故障。这里我是用限制访问接口次数(Redis+拦截器+自定义注解)和验证码的方式实现简单限流。
一、接口限流
- 接口限流是为了对服务端的接口接收请求的频率进行限制,防止服务挂掉。
- 栗子:假设我们的秒杀接口一秒只能处理12w个请求,结果秒杀活动刚开始就一下来了20w个请求。这肯定是不行的,我们可以通过接口限流将这8w个请求给拦截住,不然系统直接就整挂掉。
- 实现方案:
- Sentiel等开源流量控制组件(Sentiel主要以流量为切入点,提供流量控制、熔断降级、系统自适应保护等功能的稳定性和可用性)
- 秒杀请求之前进行验证码输入或答题等
- 限制同一用户、ip单位时间内请求次数
- 提前预约
- 等等
这里我使用的是Redis+Lua脚本+拦截器实现同一用户单位时间内请求次数限制。
自定义注解
含义:限制xx秒内最多请求xx次
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Version: 1.0.0* @Author: Dragon_王* @ClassName: AccessLimit* @Description: 通用接口限流,限制xx秒内最多请求次数* @Date: 2024/3/3 17:09*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {//时间,单位秒int second();//限制最大请求次数int maxCount();//是否需要登录boolean needLogin() default true;
}
Redis+Lua脚本+拦截器
主要关心业务逻辑:
@Component
public class AccessLimitInterceptor implements HandlerInterceptor{@Autowiredprivate IUserService userService;@Autowiredprivate RedisTemplate redisTemplate;//加载lua脚本private static final DefaultRedisScript<Boolean> SCRIPT;static {SCRIPT = new DefaultRedisScript<>();SCRIPT.setLocation(new ClassPathResource("script.lua"));SCRIPT.setResultType(Boolean.class);}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) {//获取登录用户User user = getUser(request, response);HandlerMethod hm = (HandlerMethod) handler;//获取自定义注解内的属性值AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);if (accessLimit == null) {return true;}int second = accessLimit.second();int maxCount = accessLimit.maxCount();boolean needLogin = accessLimit.needLogin();//获取当前请求地址作为keyString key = request.getRequestURI();//如果needLogin=true,是必须登录,进行用户状态验证if (needLogin) {if (user == null) {render(response, RespBeanEnum.SESSION_ERROR);return false;}key += ":" + user.getId();}//使用lua脚本Object result = redisTemplate.execute(SCRIPT, Collections.singletonList(key),new String[]{String.valueOf(maxCount), String.valueOf(second)});if (result.equals(false)){//render函数就是一个让我返回报错的函数,这里的RespBeanEnum是我封装好的报错的枚举类型,无需关注,render函数你也无需管,只要关心return false拦截render(response,RespBeanEnum.ACCESS_LIMIT_REACHED);//拦截return false;}}return true;}private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {response.setCharacterEncoding("UTF-8");response.setContentType("application/json");PrintWriter printWriter = response.getWriter();RespBean bean = RespBean.error(respBeanEnum);printWriter.write(new ObjectMapper().writeValueAsString(bean));printWriter.flush();printWriter.close();}/*** @Description: 获取当前登录用户* @param request* @param response* @methodName: getUser* @return: com.example.seckill.pojo.User* @Author: dragon_王* @Date: 2024-03-03 17:20:51*/private User getUser(HttpServletRequest request, HttpServletResponse response) {String userTicket = CookieUtil.getCookieValue(request, "userTicker");if (StringUtils.isEmpty(userTicket)) {return null;}return userService.getUserByCookie(userTicket, request, response);}
}
lua脚本,如果第一次访问就存入计数器,每次访问+1,如果计数器大于5返回false
local key = KEYS[1]
local maxCount = tonumber(ARGV[1])
local second = tonumber(ARGV[2])local count = redis.call('GET', key)
if count thencount = tonumber(count)if count < maxCount thencount = count + 1redis.call('SET', key, count)redis.call('EXPIRE', key, second)elsereturn falseend
elseredis.call('SET', key, 1)redis.call('EXPIRE', key, second)
endreturn true
二、验证码
引入验证码依赖(这是个开源的图形验证码,直接拿过来用):
<!--验证码依赖--><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency><dependency><groupId>org.openjdk.nashorn</groupId><artifactId>nashorn-core</artifactId><version>15.3</version></dependency>
/*** @Description: 获取验证码* @param user* @param goodsId* @param response* @methodName: verifyCode* @return: void* @Author: dragon_王* @Date: 2024-03-03 12:38:14*/@ApiOperation("获取验证码")@GetMapping(value = "/captcha")public void verifyCode(User user, Long goodsId, HttpServletResponse response) {if (user == null || goodsId < 0) {throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);}//设置请求头为输出图片的类型response.setContentType("image/jpg");response.setHeader("Pargam", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);//生成验证码ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);//奖验证码结果存入redisredisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);try {captcha.out(response.getOutputStream());} catch (IOException e) {log.error("验证码生成失败", e.getMessage());}}
这里用的是bootstrap写的简单前端:
<div class="row"><div class="form-inline"><img id="captchaImg" width="130" height="32" style="display: none"
onclick="refreshCaptcha()"/><input id="captcha" class="form-control" style="display: none"/><button class="btn btn-primary" type="button" id="buyButton"onclick="getSeckillPath()">立即秒杀</button></div></div><script>
校验验证码逻辑也很简单 (从redis中取出存入的图形结果和输入框中比对):
/*** @Description: 校验验证码* @param user* @param goodsId* @param captcha* @methodName: checkCaptcha* @return: boolean* @Author: dragon_王* @Date: 2024-03-03 15:48:13*/public boolean checkCaptcha(User user, Long goodsId, String captcha) {if (user == null || goodsId < 0 || StringUtils.isEmpty(captcha)) {return false;}String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);return captcha.equals(redisCaptcha);}
总结
以上就是用redis+自定义注解+Lua脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。