0


秒杀优化+秒杀安全

1.Redis预减库存

1.OrderServiceImpl.java 问题分析

image-20240513102548010

2.具体实现 SeckillController.java
1.实现InitializingBean接口的afterPropertiesSet方法,在bean初始化之后将库存信息加载到Redis
/**
     * 系统初始化,将秒杀商品库存加载到redis中
     *
     * @throws Exception
     */@OverridepublicvoidafterPropertiesSet()throwsException{// 将秒杀商品库存加载到redis中List<GoodsVo> goodsVoList = goodsService.findGoodsVo();// 如果没有秒杀商品,直接返回if(CollectionUtils.isEmpty(goodsVoList)){return;}
        goodsVoList.forEach(goodsVo ->{
            redisTemplate.opsForValue().set("seckillGoods:"+ goodsVo.getId(), goodsVo.getStockCount());});}
2.进行库存预减
// 库存预减Long stock = redisTemplate.opsForValue().decrement("seckillGoods:"+ goodsId);// 判断库存是否充足if(stock <0){// 库存不足,返回秒杀失败页面
            redisTemplate.opsForValue().increment("seckillGoods:"+ goodsId);
            model.addAttribute("errmsg",RespBeanEnum.EMPTY_STOCK.getMessage());return"secKillFail";}
3.优化分析
  • 正常情况下,每次都需要到数据库减少库存,来解决超卖问题
  • 使用Redis进行库存预减,可以减少对数据库的操作,从而提升效率
4.测试
1.清空Redis

image-20240513111015701

2.清空订单表和秒杀商品表,设置一号商品库存为10

image-20240513111216371

3.将项目部署上线
4.UserUtil.java生成100个用户

image-20240513111713541

5.发送5000次请求
1.线程组配置

image-20240513111955965

2.cookie管理器

image-20240513112038442

3.秒杀请求

image-20240513112155873

4.QPS为307,从80提升到了307提升了283%

image-20240513112324226

5.但是,出现了库存遗留问题

image-20240513113053268

5.缓存遗留原因分析

image-20240513120739584

2.内存标记优化高并发

1.问题分析
  • 在未使用内存标记时,每次请求都需要对库存进行预减,来判断是否有库存,即使库存为0
  • 所以采用内存标记的方式,当库存为0的时候,就不用进行库存预减
2.具体实现 SeckillController.java
1.首先定义一个标记是否有库存的map

image-20240513133912046

2.在系统初始化时,初始化map

image-20240513134008613

3.如果库存预减发现没有库存了,就设置内存标记

image-20240513134102161

4.在库存预减前,判断内存标记,减少redis访问

image-20240513134121258

3.测试
1.将项目上线
2.清空订单表和秒杀商品表,设置一号商品库存为10
3.清空Redis
4.发送5000次请求,QPS为330,从307提高到了330

image-20240513135337854

3.消息队列实现异步秒杀

1.问题分析

image-20240513140733314

2.思路分析

image-20240513140723821

image-20240513141159400

3.构建秒杀消息对象 SeckillMessage.java
packagecom.sxs.seckill.pojo;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;/**
 * Description: 秒杀消息
 *
 * @Author sun
 * @Create 2024/5/13 14:15
 * @Version 1.0
 */@Data@NoArgsConstructor@AllArgsConstructorpublicclassSeckillMessage{privateUser user;privateLong goodsId;}
4.秒杀RabbitMQ配置
packagecom.sxs.seckill.config;importorg.springframework.amqp.core.Binding;importorg.springframework.amqp.core.BindingBuilder;importorg.springframework.amqp.core.Queue;importorg.springframework.amqp.core.TopicExchange;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
 * Description: 秒杀RabbitMQ配置
 *
 * @Author sun
 * @Create 2024/5/13 14:23
 * @Version 1.0
 */@ConfigurationpublicclassRabbitMQSeckillConfig{// 定义一个消息队列和一个topic交换机的名字publicstaticfinalStringSECKILL_QUEUE="seckillQueue";publicstaticfinalStringSECKILL_EXCHANGE="seckillExchange";// 创建一个消息队列@BeanpublicQueueseckillQueue(){returnnewQueue(SECKILL_QUEUE,true);}// 创建一个topic交换机@BeanpublicTopicExchangeseckillExchange(){returnnewTopicExchange(SECKILL_EXCHANGE);}// 将消息队列绑定到交换机@BeanpublicBindingbinding(){// 绑定消息队列到交换机,并指定routingKey,表示只接收routingKey为seckill.#的消息returnBindingBuilder.bind(seckillQueue()).to(seckillExchange()).with("seckill.#");}}
5.生产者和消费者
1.生产者 MQSendMessage.java
packagecom.sxs.seckill.rabbitmq;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;/**
 * Description: 消息队列发送消息
 *
 * @Author sun
 * @Create 2024/5/13 15:14
 * @Version 1.0
 */@Service@Slf4jpublicclassMQSendMessage{@ResourceprivateRabbitTemplate rabbitTemplate;// 发送秒杀消息publicvoidsendSeckillMessage(String message){
        log.info("发送消息:"+ message);
        rabbitTemplate.convertAndSend("seckillExchange","seckill.message", message);}}
2.消费者,进行秒杀
1.引入hutool工具类
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.3</version></dependency>
2. MQReceiverMessage.java
packagecom.sxs.seckill.rabbitmq;importcn.hutool.json.JSONUtil;importcom.sxs.seckill.pojo.SeckillMessage;importcom.sxs.seckill.pojo.User;importcom.sxs.seckill.service.GoodsService;importcom.sxs.seckill.service.OrderService;importcom.sxs.seckill.service.SeckillGoodsService;importcom.sxs.seckill.vo.GoodsVo;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.rabbit.annotation.RabbitListener;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;/**
 * Description: 消息队列接收消息
 *
 * @Author sun
 * @Create 2024/5/13 15:17
 * @Version 1.0
 */@Service@Slf4jpublicclassMQReceiverMessage{@ResourceprivateGoodsService goodsService;@ResourceprivateOrderService orderService;// 接收秒杀消息@RabbitListener(queues ="seckillQueue")publicvoidreceiveSeckillMessage(String message){
        log.info("接收消息:"+ message);// 此时的message是秒杀的消息,要将其转换为SeckillMessage对象SeckillMessage seckillMessage =JSONUtil.toBean(message,SeckillMessage.class);// 获取秒杀信息User user = seckillMessage.getUser();Long goodsId = seckillMessage.getGoodsId();// 根据商品id查询商品详情GoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId);// 进行秒杀
        orderService.seckill(user, goodsVoByGoodsId);}}
6.编写控制层
1.SeckillController.java

image-20240513154029078

// MQ实现异步秒杀// 封装秒杀信息SeckillMessage seckillMessage =newSeckillMessage(user, goodsId);// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送
        mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));// 返回排队中页面
        model.addAttribute("errmsg",RespBeanEnum.QUEUE_ERROR.getMessage());return"secKillFail";
2.RespBeanEnum.java 新增响应枚举类

image-20240513154055007

7.测试
1.将项目上线
2.清空订单表和秒杀商品表,设置一号商品库存为10
3.清空Redis
4.发送5000次请求,QPS为363

image-20240513161847459

秒杀安全

1.秒杀接口隐藏

1.需求分析

image-20240514103812853

2.思路分析

image-20240514104439584

3.具体实现
1.RespBeanEnum.java 新增几个响应
2.OrderService.java 新增方法
/**
     * 方法:生成秒杀路径
     * @param user
     * @param goodsId
     * @return
     */StringcreatePath(User user,Long goodsId);/**
     * 方法:校验秒杀路径
     * @param user
     * @param goodsId
     * @param path
     * @return
     */booleancheckPath(User user,Long goodsId,String path);
3.OrderServiceImpl.java
@OverridepublicStringcreatePath(User user,Long goodsId){// 对参数进行校验if(user ==null|| goodsId <=0){returnnull;}// 生成秒杀路径String path =MD5Util.md5(UUIDUtil.uuid()+"123456");// 保存到redis中,设置过期时间为60秒
        redisTemplate.opsForValue().set("seckillPath:"+ user.getId()+":"+ goodsId, path,60,TimeUnit.SECONDS);return path;}@OverridepublicbooleancheckPath(User user,Long goodsId,String path){// 对参数进行校验if(user ==null|| goodsId <=0||StringUtils.isBlank(path)){returnfalse;}// 从redis中获取秒杀路径String redisPath =(String) redisTemplate.opsForValue().get("seckillPath:"+ user.getId()+":"+ goodsId);// 判断是否相等,并返回return path.equals(redisPath);}
4.SeckillController.java
@RequestMapping("/{path}/doSeckill")publicRespBeandoSeckill(Model model,User user,Long goodsId,@PathVariableString path){// 判断用户是否登录if(user ==null){returnRespBean.error(RespBeanEnum.SESSION_ERROR);}// 校验pathboolean check = orderService.checkPath(user, goodsId, path);if(!check){returnRespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}// 根据goodsId获取GoodsVoGoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId);// 判断是否有库存if(goodsVoByGoodsId.getStockCount()<1){returnRespBean.error(RespBeanEnum.EMPTY_STOCK);}// 从redis中判断是否复购if(redisTemplate.hasKey("order:"+ user.getId()+":"+ goodsId)){returnRespBean.error(RespBeanEnum.REPEATE_ERROR);}// 首先判断内存标记if(inventoryTagging.get(goodsId)){returnRespBean.error(RespBeanEnum.EMPTY_STOCK);}// 库存预减Long stock = redisTemplate.opsForValue().decrement("seckillGoods:"+ goodsId);// 判断库存是否充足if(stock <0){// 标记库存不足
            inventoryTagging.put(goodsId,true);// 库存不足,返回秒杀失败页面
            redisTemplate.opsForValue().increment("seckillGoods:"+ goodsId);returnRespBean.error(RespBeanEnum.EMPTY_STOCK);}// MQ实现异步秒杀// 封装秒杀信息SeckillMessage seckillMessage =newSeckillMessage(user, goodsId);// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送
        mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));// 返回排队中returnRespBean.success(RespBeanEnum.SEK_KILL_WAIT);}/**
     * 生成秒杀地址
     * @param user
     * @param goodsId
     * @return
     */@ResponseBody@RequestMapping("/path")publicRespBeangetPath(User user,Long goodsId){// 参数校验if(user ==null|| goodsId <=0){returnRespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}// 调用OrderService中的createPath方法生成秒杀地址String path = orderService.createPath(user, goodsId);returnRespBean.success(path);}
5.goodsDetail.html
1.秒杀首先获取路径

image-20240514132039228

2.解析环境变量,区分多环境

image-20240514120006961

3.新增两个方法,使用隐藏秒杀接口的方式秒杀商品

4.测试

image-20240514133435103

image-20240514133441653

2.验证码防止脚本攻击

1.思路分析

image-20240514134151357

2.具体实现
1.pom.xml 引入依赖
<dependency><groupId>com.ramostear</groupId><artifactId>Happy-Captcha</artifactId><version>1.0.1</version></dependency>
2.SeckillController.java 编写方法生成验证码
/**
     * 生成验证码
     * @param user
     * @param goodsId
     * @param request
     * @param response
     */@RequestMapping("/captcha")publicvoidhappyCaptcha(User user,Long goodsId,HttpServletRequest request,HttpServletResponse response){HappyCaptcha.require(request, response).style(CaptchaStyle.ANIM)//设置展现样式为动画.type(CaptchaType.NUMBER)//设置验证码内容为数字.length(6)//设置字符长度为 6.width(220)//设置动画宽度为 220.height(80)//设置动画高度为 80.font(Fonts.getInstance().zhFont())//设置汉字的字体.build().finish();//生成并输出验证码// 这个验证码的结果会存储在session中,可以通过request.getSession().getAttribute("happy-captcha")获取// 获取验证码的值,放入redis中String verifyCode = request.getSession().getAttribute("happy-captcha").toString();
        redisTemplate.opsForValue().set("captcha:"+ user.getId()+":"+ goodsId, verifyCode,60,TimeUnit.SECONDS);}
3.OrderService.java 校验用户输入的验证码
/**
     * 校验用户输入的验证码
     * @param user
     * @param goodsId
     * @param captcha
     * @return
     */booleancheckCaptcha(User user,Long goodsId,String captcha);
4.OrderServiceImpl.java
@OverridepublicbooleancheckCaptcha(User user,Long goodsId,String captcha){// 参数校验if(user ==null|| goodsId <=0||StringUtils.isBlank(captcha)){returnfalse;}// 从redis中获取验证码String verifyCode =(String) redisTemplate.opsForValue().get("captcha:"+ user.getId()+":"+ goodsId);return captcha.equals(verifyCode);}
5.SeckillController.java 加入验证码校验
6.goodsDetail.html
1.前端请求验证码
2.测试

image-20240514142410612

3.获取用户输入的验证码,并携带验证码

image-20240514142750293

image-20240514143247969

3.秒杀接口限流-防刷

1.思路分析

image-20240514144320230

2.简单接口限流
1.SeckillController.java

image-20240514151732430

2.测试

image-20240514151841132

4.通用接口限流防刷

1.思路分析

image-20240514153042300

2.编写自定义限流注解 AccessLimit.java
packagecom.sxs.seckill.config;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**
 * Description: 限流注解
 *
 * @Author sun
 * @Create 2024/5/14 15:38
 * @Version 1.0
 */@Retention(RetentionPolicy.RUNTIME)// 运行时生效@Target(ElementType.METHOD)// 作用在方法上public@interfaceAccessLimit{intseconds();// 时间范围intmaxCount();// 最大访问次数booleanneedLogin()defaulttrue;// 是否需要登录}
3.使用方式 SeckillController.java

image-20240514154353081

4.编写 config/UserContext.java 使用ThreadLocal存储user
packagecom.sxs.seckill.config;importcom.sxs.seckill.pojo.User;/**
 * Description:
 *
 * @Author sun
 * @Create 2024/5/14 15:46
 * @Version 1.0
 */publicclassUserContext{// 初始化ThreadLocal以存储用户信息privatestaticThreadLocal<User> threadLocal =newThreadLocal<>();publicstaticUsergetUser(){return threadLocal.get();}publicstaticvoidsetUser(User user){
        threadLocal.set(user);}// 清除ThreadLocal中的数据publicstaticvoidremoveUser(){
        threadLocal.remove();}}
5.编写自定义限流拦截器 config/AccessLimitInterceptor.java
packagecom.sxs.seckill.config;importcom.sxs.seckill.exception.GlobalException;importcom.sxs.seckill.pojo.User;importcom.sxs.seckill.service.UserService;importcom.sxs.seckill.utils.CookieUtil;importcom.sxs.seckill.vo.RespBeanEnum;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Component;importorg.springframework.web.method.HandlerMethod;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.util.concurrent.TimeUnit;/**
 * Description: 限流拦截器
 *
 * @Author sun
 * @Create 2024/5/14 15:55
 * @Version 1.0
 */@ComponentpublicclassAccessLimitInterceptorimplementsHandlerInterceptor{@ResourceprivateUserService userService;@ResourceRedisTemplate redisTemplate;/**
     * 拦截请求,进行限流处理,在目标方法前执行
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{if(handler instanceofHandlerMethod){// 如果是方法级别的拦截// 1.获取user对象,放到threadLocal中User user =getUser(request, response);UserContext.setUser(user);// 2.处理限流注解HandlerMethod handlerMethod =(HandlerMethod) handler;AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);if(accessLimit ==null){returntrue;}// 3.获取注解上的参数int seconds = accessLimit.seconds();int maxCount = accessLimit.maxCount();boolean needLogin = accessLimit.needLogin();String key = request.getRequestURI();if(needLogin){// 如果需要登录,但是没有登录,返回错误信息if(user ==null){// 如果需要登录,但是没有登录,返回错误信息thrownewGlobalException(RespBeanEnum.USER_NOT_LOGIN);}// 如果登录了,key加上用户id
                key +=":"+ user.getId();}// 4.对访问次数进行限制,如果登陆了就是对这个用户的访问次数进行限制,如果没有登录就是对这个接口的访问次数进行限制Integer count =(Integer) redisTemplate.opsForValue().get(key);if(count ==null){// 第一次访问
                redisTemplate.opsForValue().set(key,1, seconds,TimeUnit.SECONDS);}elseif(count < maxCount){// 访问次数加1
                redisTemplate.opsForValue().increment(key);}else{// 超过访问次数thrownewGlobalException(RespBeanEnum.ACCESS_LIMIT_REACHED);}}// 如果不是方法级别的拦截,直接放行returntrue;}// 单独编写方法,获取User对象privateUsergetUser(HttpServletRequest request,HttpServletResponse response){String ticket =CookieUtil.getCookieValue(request,"userTicket");if(ticket ==null){returnnull;}return userService.getUserByCookie(ticket, request, response);}}
6.config/WebConfig.java中注册拦截器

image-20240514165547226

7.修改自定义参数解析器UserArgumentResolver.java,直接从ThreadLocal中获取User

image-20240514165815611

8.测试

image-20240514170313513

9.解决库存遗留问题,为每个用户id加锁即可

image-20240515084923122


本文转载自: https://blog.csdn.net/m0_64637029/article/details/139536609
版权归原作者 S-X-S 所有, 如有侵权,请联系我们删除。

“秒杀优化+秒杀安全”的评论:

还没有评论