【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{@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脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/513972.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32CubeMX学习笔记12 ---低功耗模式

在实际使用中很多产品都需要考虑低功耗的问题&#xff0c;STM32F10X提供了三种低功耗模式&#xff1a;睡眠模式&#xff08;Sleep mode&#xff09;、停机模式&#xff08;Stop mode&#xff09;和待机模式&#xff08;Standby mode&#xff09;。这些低功耗模式可以有效减少系…

【笔记】ArkTS 语言(OpenHarmony系统)

一、官方简介和文档 介绍&#xff1a;aArkTS 语言 | 华为开发者联盟 (huawei.com) 学习指南&#xff08;文档&#xff09;&#xff1a;初识ArkTS语言-学习ArkTS语言-入门 | 华为开发者联盟 (huawei.com) 二、ArkTS语言知识 &#xff08;一&#xff09;编程语言介绍 Mozilla创…

jmeter 弹性压测数据库

当前版本&#xff1a; jmeter 5.6.3mysql 5.7.39 简介 JMeter 通过 Open Model Thread Group 来实现弹性模拟负载测试&#xff0c;它是5.4.1 版本中引入的一个实验性线程组。例如设置多个线程模式&#xff0c;再根据这些线程模式调整不同的并发数、暂停时间。由于Open Model T…

【Linux】Linux C编程

gcc编译器 gcc [options] [filenames] 其中&#xff0c;options是编译器所需要的选项参数&#xff0c;filenames是文件名。 gcc编译过程 C语言编译过程一般可以分为预处理、编译、汇编、链接四个步骤。 1.预处理阶段 预处理阶段主要处理宏定义和include&#xff0c;并进行语…

Luajit 2023移动版本编译 v2.1.ROLLING

文章顶部有编好的 2.1.ROLLING 2023/08/21版本源码 Android 64 和 iOS 64 luajit 目前最新的源码tag版本为 v2.1.ROLLING on Aug 21, 2023应该是修正了很多bug, 我是出现下面问题才编的. cocos2dx-lua 游戏 黑屏 并报错: [LUA ERROR] bad light userdata pointer 编…

flutter旋转动画,算法题+JVM+自定义View

在很多的博客或者书上&#xff0c;说有三种&#xff0c;除了上述的两种以外&#xff0c;还有一种是实现Callable接口。但是这种并不是&#xff0c;因为&#xff0c;我们检查JDK中Thread的源码&#xff0c;看它的注释&#xff1a; There are two ways to create a new thread o…

#QT(TCP网络编程-服务端)

1.IDE&#xff1a;QTCreator 2.实验&#xff1a;编写一个tcp服务端 QTcpsever QTcpsocket 3.记录&#xff1a; (1)先搭建界面 &#xff08;2&#xff09;服务端代码 a. pro QT core gui networkgreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c1…

Docker的安装跟基础使用一篇文章包会

目录 国内源安装新版本 1、清理环境 2、配置docker yum源 3、安装启动 4、启动Docker服务 5、修改docker数据存放位置 6、配置加速器 现在我们已经完成了docker的安装和初始配置。以下为基本测试使用 自带源安装的版本太低 docker官方源安装的话速度太慢了 所以本篇文…

CSS浮动实战,经典好文

零基础学web前端开发要怎么去学? 首先要学习的就是基础知识&#xff1a;html、css和JavaScript。HTML是内容&#xff0c;CSS是表现&#xff0c;JavaScript是行为。前端开发的门槛其实非常低&#xff0c;与服务器端语言先慢后快的学习曲线相比&#xff0c;前端开发的学习曲线是…

1909_Arm Cortex-M3编程模型

1909_Arm Cortex-M3编程模型 全部学习汇总&#xff1a; g_arm_cores: ARM内核的学习笔记 (gitee.com) 编程模型的部分除了单独的核心寄存器描述之外&#xff0c;它还包含有关处理器模式和软件执行和堆栈的特权级别的信息。 处理器有两种模式&#xff0c;分别是线程模式和Handle…

运维知识点-hibernate引擎-HQL

HQL有两个主要含义&#xff0c;分别是&#xff1a; HQL&#xff08;Hibernate Query Language&#xff09;是Hibernate查询语言的缩写&#xff0c;它是一种面向对象的查询语言&#xff0c;类似于SQL&#xff0c;但不是去对表和列进行操作&#xff0c;而是面向对象和它们的属性…

动态规划(算法竞赛、蓝桥杯)--状态压缩DP蒙德里安的梦想

1、B站视频链接&#xff1a;E31 状态压缩DP 蒙德里安的梦想_哔哩哔哩_bilibili #include <bits/stdc.h> using namespace std; const int N12,M1<<N; bool st[N];//st[i]存储合并列的状态i是否合法 long long f[N][M];//f[i][j]表示摆放第i列&#xff0c;状态为…