浩泽学编程:个人主页

  推荐专栏:《深入浅出SpringBoot》《java对AI的调用开发》              《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{

@Autowired

private IUserService userService;

@Autowired

private RedisTemplate redisTemplate;

//加载lua脚本

private static final DefaultRedisScript SCRIPT;

static {

SCRIPT = new DefaultRedisScript<>();

SCRIPT.setLocation(new ClassPathResource("script.lua"));

SCRIPT.setResultType(Boolean.class);

}

@Override

public 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();

//获取当前请求地址作为key

String 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 then

count = tonumber(count)

if count < maxCount then

count = count + 1

redis.call('SET', key, count)

redis.call('EXPIRE', key, second)

else

return false

end

else

redis.call('SET', key, 1)

redis.call('EXPIRE', key, second)

end

return true

二、验证码

引入验证码依赖(这是个开源的图形验证码,直接拿过来用):

com.github.whvcse

easy-captcha

1.6.2

org.openjdk.nashorn

nashorn-core

15.3

/**

* @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);

//奖验证码结果存入redis

redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);

try {

captcha.out(response.getOutputStream());

} catch (IOException e) {

log.error("验证码生成失败", e.getMessage());

}

}

这里用的是bootstrap写的简单前端:

onclick="refreshCaptcha()"/>