【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
🏡���泽学编程:个人主页
🔥 推荐专栏:《深入浅出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
二、验证码
引入验证码依赖(这是个开源的图形验证码,直接拿过来用):
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
这里用的是bootstrap写的简单前端:
立即秒杀
校验验证码逻辑也很简单 (从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
总结
以上就是用redis+自定义注解+Lua脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。
The End