《RabbitMQ》《Spring》《SpringMVC》《项目实战》
前言
限流是秒杀业务最常用的手段。限流是从用户访问压力的角度来考虑如何应对系统故障。这里我是用限制访问接口次数(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: A***essLimit
* @Description: 通用接口限流,限制xx秒内最多请求次数
* @Date: 2024/3/3 17:09
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface A***essLimit {
//时间,单位秒
int second();
//限制最大请求次数
int maxCount();
//是否需要登录
boolean needLogin() default true;
}
Redis+Lua脚本+拦截器
主要关心业务逻辑:
@***ponent
public class A***essLimitInterceptor implements HandlerInterceptor{
@Autowired
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
//加载lua脚本
private static final DefaultRedisScript<Boolean> 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;
//获取自定义注解内的属性值
A***essLimit a***essLimit = hm.getMethodAnnotation(A***essLimit.class);
if (a***essLimit == null) {
return true;
}
int second = a***essLimit.second();
int maxCount = a***essLimit.maxCount();
boolean needLogin = a***essLimit.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.A***ESS_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: ***.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
二、验证码
引入验证码依赖(这是个开源的图形验证码,直接拿过来用):
<!--验证码依赖-->
<dependency>
<groupId>***.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);
//生成验证码
Arithmeti***aptcha captcha = new Arithmeti***aptcha(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写的简单前端:
<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脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。