做电影下载网站好,图形设计网站,禹城做网站的公司,高等院校网站建设方案1.Redis预减库存
1.OrderServiceImpl.java 问题分析 2.具体实现 SeckillController.java
1.实现InitializingBean接口的afterPropertiesSet方法#xff0c;在bean初始化之后将库存信息加载到Redis /*** 系统初始化#xff0c;将秒杀商品库存加载到redis中** throws Excepti…1.Redis预减库存
1.OrderServiceImpl.java 问题分析 2.具体实现 SeckillController.java
1.实现InitializingBean接口的afterPropertiesSet方法在bean初始化之后将库存信息加载到Redis /*** 系统初始化将秒杀商品库存加载到redis中** throws Exception*/Overridepublic void afterPropertiesSet() throws Exception {// 将秒杀商品库存加载到redis中ListGoodsVo 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 2.清空订单表和秒杀商品表设置一号商品库存为10 3.将项目部署上线
4.UserUtil.java生成100个用户 5.发送5000次请求
1.线程组配置 2.cookie管理器 3.秒杀请求 4.QPS为307从80提升到了307提升了283% 5.但是出现了库存遗留问题 5.缓存遗留原因分析 2.内存标记优化高并发
1.问题分析
在未使用内存标记时每次请求都需要对库存进行预减来判断是否有库存即使库存为0所以采用内存标记的方式当库存为0的时候就不用进行库存预减
2.具体实现 SeckillController.java
1.首先定义一个标记是否有库存的map 2.在系统初始化时初始化map 3.如果库存预减发现没有库存了就设置内存标记 4.在库存预减前判断内存标记减少redis访问 3.测试
1.将项目上线
2.清空订单表和秒杀商品表设置一号商品库存为10
3.清空Redis
4.发送5000次请求QPS为330从307提高到了330 3.消息队列实现异步秒杀
1.问题分析 2.思路分析 3.构建秒杀消息对象 SeckillMessage.java
package com.sxs.seckill.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** Description: 秒杀消息** Author sun* Create 2024/5/13 14:15* Version 1.0*/
Data
NoArgsConstructor
AllArgsConstructor
public class SeckillMessage {private User user;private Long goodsId;
}
4.秒杀RabbitMQ配置
package com.sxs.seckill.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** Description: 秒杀RabbitMQ配置** Author sun* Create 2024/5/13 14:23* Version 1.0*/
Configuration
public class RabbitMQSeckillConfig {// 定义一个消息队列和一个topic交换机的名字public static final String SECKILL_QUEUE seckillQueue;public static final String SECKILL_EXCHANGE seckillExchange;// 创建一个消息队列Beanpublic Queue seckillQueue() {return new Queue(SECKILL_QUEUE, true);}// 创建一个topic交换机Beanpublic TopicExchange seckillExchange() {return new TopicExchange(SECKILL_EXCHANGE);}// 将消息队列绑定到交换机Beanpublic Binding binding() {// 绑定消息队列到交换机并指定routingKey表示只接收routingKey为seckill.#的消息return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with(seckill.#);}
}
5.生产者和消费者
1.生产者 MQSendMessage.java
package com.sxs.seckill.rabbitmq;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;/*** Description: 消息队列发送消息** Author sun* Create 2024/5/13 15:14* Version 1.0*/
Service
Slf4j
public class MQSendMessage {Resourceprivate RabbitTemplate rabbitTemplate;// 发送秒杀消息public void sendSeckillMessage(String message) {log.info(发送消息 message);rabbitTemplate.convertAndSend(seckillExchange, seckill.message, message);}
}
2.消费者进行秒杀
1.引入hutool工具类 dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.3.3/version/dependency
2. MQReceiverMessage.java
package com.sxs.seckill.rabbitmq;import cn.hutool.json.JSONUtil;
import com.sxs.seckill.pojo.SeckillMessage;
import com.sxs.seckill.pojo.User;
import com.sxs.seckill.service.GoodsService;
import com.sxs.seckill.service.OrderService;
import com.sxs.seckill.service.SeckillGoodsService;
import com.sxs.seckill.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;import javax.annotation.Resource;/*** Description: 消息队列接收消息** Author sun* Create 2024/5/13 15:17* Version 1.0*/
Service
Slf4j
public class MQReceiverMessage {Resourceprivate GoodsService goodsService;Resourceprivate OrderService orderService;// 接收秒杀消息RabbitListener(queues seckillQueue)public void receiveSeckillMessage(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 // MQ实现异步秒杀// 封装秒杀信息SeckillMessage seckillMessage new SeckillMessage(user, goodsId);// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));// 返回排队中页面model.addAttribute(errmsg, RespBeanEnum.QUEUE_ERROR.getMessage());return secKillFail;2.RespBeanEnum.java 新增响应枚举类 7.测试
1.将项目上线
2.清空订单表和秒杀商品表设置一号商品库存为10
3.清空Redis
4.发送5000次请求QPS为363 秒杀安全
1.秒杀接口隐藏
1.需求分析 2.思路分析 3.具体实现
1.RespBeanEnum.java 新增几个响应
2.OrderService.java 新增方法 /*** 方法生成秒杀路径* param user* param goodsId* return*/String createPath(User user, Long goodsId);/*** 方法校验秒杀路径* param user* param goodsId* param path* return*/boolean checkPath(User user, Long goodsId, String path);3.OrderServiceImpl.java Overridepublic String createPath(User user, Long goodsId) {// 对参数进行校验if (user null || goodsId 0) {return null;}// 生成秒杀路径String path MD5Util.md5(UUIDUtil.uuid() 123456);// 保存到redis中设置过期时间为60秒redisTemplate.opsForValue().set(seckillPath: user.getId() : goodsId, path, 60, TimeUnit.SECONDS);return path;}Overridepublic boolean checkPath(User user, Long goodsId, String path) {// 对参数进行校验if (user null || goodsId 0 || StringUtils.isBlank(path)) {return false;}// 从redis中获取秒杀路径String redisPath (String) redisTemplate.opsForValue().get(seckillPath: user.getId() : goodsId);// 判断是否相等并返回return path.equals(redisPath);}4.SeckillController.java RequestMapping(/{path}/doSeckill)public RespBean doSeckill(Model model, User user, Long goodsId, PathVariable String path) {// 判断用户是否登录if (user null) {return RespBean.error(RespBeanEnum.SESSION_ERROR);}// 校验pathboolean check orderService.checkPath(user, goodsId, path);if (!check) {return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}// 根据goodsId获取GoodsVoGoodsVo goodsVoByGoodsId goodsService.findGoodsVoByGoodsId(goodsId);// 判断是否有库存if (goodsVoByGoodsId.getStockCount() 1) {return RespBean.error(RespBeanEnum.EMPTY_STOCK);}// 从redis中判断是否复购if (redisTemplate.hasKey(order: user.getId() : goodsId)) {return RespBean.error(RespBeanEnum.REPEATE_ERROR);}// 首先判断内存标记if (inventoryTagging.get(goodsId)) {return RespBean.error(RespBeanEnum.EMPTY_STOCK);}// 库存预减Long stock redisTemplate.opsForValue().decrement(seckillGoods: goodsId);// 判断库存是否充足if (stock 0) {// 标记库存不足inventoryTagging.put(goodsId, true);// 库存不足返回秒杀失败页面redisTemplate.opsForValue().increment(seckillGoods: goodsId);return RespBean.error(RespBeanEnum.EMPTY_STOCK);}// MQ实现异步秒杀// 封装秒杀信息SeckillMessage seckillMessage new SeckillMessage(user, goodsId);// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));// 返回排队中return RespBean.success(RespBeanEnum.SEK_KILL_WAIT);}/*** 生成秒杀地址* param user* param goodsId* return*/ResponseBodyRequestMapping(/path)public RespBean getPath(User user, Long goodsId) {// 参数校验if (user null || goodsId 0) {return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}// 调用OrderService中的createPath方法生成秒杀地址String path orderService.createPath(user, goodsId);return RespBean.success(path);}5.goodsDetail.html
1.秒杀首先获取路径 2.解析环境变量区分多环境 3.新增两个方法使用隐藏秒杀接口的方式秒杀商品 4.测试 2.验证码防止脚本攻击
1.思路分析 2.具体实现
1.pom.xml 引入依赖 dependencygroupIdcom.ramostear/groupIdartifactIdHappy-Captcha/artifactIdversion1.0.1/version/dependency2.SeckillController.java 编写方法生成验证码 /*** 生成验证码* param user* param goodsId* param request* param response*/RequestMapping(/captcha)public void happyCaptcha(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*/boolean checkCaptcha(User user, Long goodsId, String captcha);4.OrderServiceImpl.java Overridepublic boolean checkCaptcha(User user, Long goodsId, String captcha) {// 参数校验if (user null || goodsId 0 || StringUtils.isBlank(captcha)) {return false;}// 从redis中获取验证码String verifyCode (String) redisTemplate.opsForValue().get(captcha: user.getId() : goodsId);return captcha.equals(verifyCode);}5.SeckillController.java 加入验证码校验
6.goodsDetail.html
1.前端请求验证码
2.测试 3.获取用户输入的验证码并携带验证码 3.秒杀接口限流-防刷
1.思路分析 2.简单接口限流
1.SeckillController.java 2.测试 4.通用接口限流防刷
1.思路分析 2.编写自定义限流注解 AccessLimit.java
package com.sxs.seckill.config;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** Description: 限流注解** Author sun* Create 2024/5/14 15:38* Version 1.0*/
Retention(RetentionPolicy.RUNTIME) // 运行时生效
Target(ElementType.METHOD) // 作用在方法上
public interface AccessLimit {int seconds(); // 时间范围int maxCount(); // 最大访问次数boolean needLogin() default true; // 是否需要登录
}
3.使用方式 SeckillController.java 4.编写 config/UserContext.java 使用ThreadLocal存储user
package com.sxs.seckill.config;import com.sxs.seckill.pojo.User;/*** Description:** Author sun* Create 2024/5/14 15:46* Version 1.0*/
public class UserContext {// 初始化ThreadLocal以存储用户信息private static ThreadLocalUser threadLocal new ThreadLocal();public static User getUser() {return threadLocal.get();}public static void setUser(User user) {threadLocal.set(user);}// 清除ThreadLocal中的数据public static void removeUser() {threadLocal.remove();}
}
5.编写自定义限流拦截器 config/AccessLimitInterceptor.java
package com.sxs.seckill.config;import com.sxs.seckill.exception.GlobalException;
import com.sxs.seckill.pojo.User;
import com.sxs.seckill.service.UserService;
import com.sxs.seckill.utils.CookieUtil;
import com.sxs.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;/*** Description: 限流拦截器** Author sun* Create 2024/5/14 15:55* Version 1.0*/
Component
public class AccessLimitInterceptor implements HandlerInterceptor {Resourceprivate UserService userService;ResourceRedisTemplate redisTemplate;/*** 拦截请求进行限流处理在目标方法前执行** param request* param response* param handler* return* throws Exception*/Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod) {// 如果是方法级别的拦截// 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) {return true;}// 3.获取注解上的参数int seconds accessLimit.seconds();int maxCount accessLimit.maxCount();boolean needLogin accessLimit.needLogin();String key request.getRequestURI();if (needLogin) {// 如果需要登录但是没有登录返回错误信息if (user null) {// 如果需要登录但是没有登录返回错误信息throw new GlobalException(RespBeanEnum.USER_NOT_LOGIN);}// 如果登录了key加上用户idkey : user.getId();}// 4.对访问次数进行限制如果登陆了就是对这个用户的访问次数进行限制如果没有登录就是对这个接口的访问次数进行限制Integer count (Integer) redisTemplate.opsForValue().get(key);if (count null) {// 第一次访问redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);} else if (count maxCount) {// 访问次数加1redisTemplate.opsForValue().increment(key);} else {// 超过访问次数throw new GlobalException(RespBeanEnum.ACCESS_LIMIT_REACHED);}}// 如果不是方法级别的拦截直接放行return true;}// 单独编写方法获取User对象private User getUser(HttpServletRequest request, HttpServletResponse response) {String ticket CookieUtil.getCookieValue(request, userTicket);if (ticket null) {return null;}return userService.getUserByCookie(ticket, request, response);}
}
6.config/WebConfig.java中注册拦截器 7.修改自定义参数解析器UserArgumentResolver.java直接从ThreadLocal中获取User 8.测试 9.解决库存遗留问题为每个用户id加锁即可