# SecKill **Repository Path**: masuo_git/SecKill ## Basic Information - **Project Name**: SecKill - **Description**: springboot+mysql+redis+rabbitmq+... - **Primary Language**: Java - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-04-24 - **Last Updated**: 2022-05-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项目搭建 - springboot:2.2.2.RELEASE - mysql:8.0 - redis:6.2.6 - rabbitmq:3.8-management ## 安装环节 略 ## 数据准备 SQL文件位于resource/sql文件中。 ## 配置文件 ![image-20220325170520616](https://masuo-github-image.oss-cn-beijing.aliyuncs.com/image/20220325170521.png) 修改redis、mysql、rabbitmq的服务器地址及用户信息 项目到此搭建完成。 # 服务优化 ## 缓存商品列表页面 > 随着网用户数量的增加,假设网站已经达到其瓶颈。那么我们就需要思考如何优化系统。 > > 优化思路:将页面与数据不需要经常改动的页面加入redis缓存。 **原代码** ```java @RequestMapping("/toGoods") public String toGoods(Model model) { model.addAttribute("goodsList", goodsService.findGoodsVo()); // 动态获取页面并渲染数据 return "goods/list"; } ``` > 动态渲染页面,每次该请求都会重新获取`ModelAndView`,重新渲染,如果在短时间内多次请求,页面基本不会发生太大的改变 , 但是重复的请求会触发多次数据库查询等操作,费时费力,且没有好处. > > 此时我们可以思考如何加快用户的访问速度 > > - 缓存,缓存加快了我们的访问速度,但是当页面数据发生变化时需要重新加载缓存 > > - 将页面静态化,在访问页面时利用Ajax技术去动态获取数据并渲染 ### 将页面存入Redis 首次访问时,手动渲染页面将其放到Redis缓存。 **优化之后的代码** ```java /** * 将其缓存到redis,加快访问速度 */ @RequestMapping(value = "/toGoods", produces = "text/html;charset=utf-8") @ResponseBody public String toGoodsStatic(User user, Model model, HttpServletRequest request, HttpServletResponse response) { //获取redis ValueOperations operations = redisTemplate.opsForValue(); //判断redis是否有缓存 String html = (String) operations.get("goods:detail"); //有缓存,直接返回 if (!StringUtils.isEmpty(html)) { return html; } //提前准备好model数据 model.addAttribute("user", user); model.addAttribute("goodsList", goodsService.findGoodsVo()); //没有缓存,则需要手动渲染页面 //准备WebContext WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap()); //手动渲染页面 html = thymeleafViewResolver.getTemplateEngine().process("goods/list", webContext); if (!StringUtils.isEmpty(html)) { //将页面放入redis,并设置过期时间为60s operations.set("goods:detail", html, 60L, TimeUnit.SECONDS); } return html; } ``` ## 静态化商品详情页 > 对于商品详情页面,整体的页面框架不会发生太大的改变,唯一需要经常改变的就是商品信息,即商品数据。对于此类页面,我们可以使用页面静态化来优化访问速度。 **原请求** ```java @RequestMapping("/toDetailDynamic/{id}") public String toDetailDynamic(@PathVariable("id") Long id, Model model, User user) { if (user == null) { return "/login"; } GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(id); //获取秒杀开始时间 Date startDate = goodsVo.getStartDate(); //获取秒杀结束时间 Date endDate = goodsVo.getEndDate(); //获取当前时间 Date now = new Date(); //秒杀活动状态 -1(未开始) 0(进行中) 1(已结束) int state = 0; //倒计时,-1代表已开始或已结束 int remainSeconds = 0; if (now.before(startDate)) { //秒杀未开始 state = -1; //计算倒计时时间 remainSeconds = (int) ((startDate.getTime() - System.currentTimeMillis()) / 1000); } else if (now.after(endDate)) { //秒杀已结束 state = 1; remainSeconds = -1; } // System.out.println("state:" + state); model.addAttribute("remainSeconds", remainSeconds); model.addAttribute("state", state); model.addAttribute("goods", goodsVo); model.addAttribute("user", user); return "goods/detail"; } ``` **原页面** ```html 商品详情
秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
商品名称
商品图片 iphone
秒杀开始时间 秒杀倒计时: 秒杀正在进行中 秒杀已结束
商品原价
秒杀价
库存数量
``` 原页面基于thyme leaf的引擎,动态渲染页面。 **配置静态资源处理** ```yaml # 静态资源处理 resources: # 自动默认静态资源处理,默认启用 add-mappings: true cache: cachecontrol: # 缓存时间,单位秒 max-age: 3600 chain: # 资源自动缓存,默认启用 cache: true # 启用资源链 ,默认禁用 enabled: true # 压缩资源,默认禁用,如:gzip compressed: true # H5的默认缓存,默认禁用 html-application-cache: true # 静态资源位置 static-locations: classpath:/static/ ``` **静态资源配置类** ```java @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { // controller入参解析器 private final UserArgumentResolver userArgumentResolver; public WebConfig(UserArgumentResolver userArgumentResolver){ this.userArgumentResolver = userArgumentResolver; } @Override public void addArgumentResolvers(List resolvers) { // 添加用户入参解析器 resolvers.add(userArgumentResolver); } // 静态资源处理器 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } } ``` **静态化请求** **templates/goods/list.html** ```html 详情 ``` **静态化页面** ```html 商品详情
秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
商品名称
商品图片 iphone
秒杀开始时间
商品原价
秒杀价
库存数量
``` **GoodsController.java** ```java @RequestMapping("/detail/{id}") @ResponseBody public ResponseBean toDetail(@PathVariable("id") Long id, User user) { if (user == null) { return ResponseBean.error(ResponseBeanEnum.UNLOGIN_ERROR); } GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(id); //获取秒杀开始时间 Date startDate = goodsVo.getStartDate(); //获取秒杀结束时间 Date endDate = goodsVo.getEndDate(); //获取当前时间 Date now = new Date(); //秒杀活动状态 -1(未开始) 0(进行中) 1(已结束) int state = 0; //倒计时,-1代表已开始或已结束 int remainSeconds = 0; if (now.before(startDate)) { //秒杀未开始 state = -1; //计算倒计时时间 remainSeconds = (int) ((startDate.getTime() - System.currentTimeMillis()) / 1000); } else if (now.after(endDate)) { //秒杀已结束 state = 1; remainSeconds = -1; } GoodsDetailVo goodsDetailVo = new GoodsDetailVo(); goodsDetailVo.setGoodsVo(goodsVo); goodsDetailVo.setState(state); goodsDetailVo.setUser(user); goodsDetailVo.setRemainSeconds(remainSeconds); return ResponseBean.success(goodsDetailVo); } ``` 其余页面同上。 ## 异步通信优化 **pom文件添加rabbitmq依赖** ```xml org.springframework.boot spring-boot-starter-amqp ``` **配置rabbitmq** 采用主题模式进行消息通信 ```java @Configuration public class RabbitMQTopicConfig { /** * 主题模式支持通配符 * 1、* :匹配路由键的一个词( 》= 1) * 2、# :匹配路由键的一个或多个词( 》= 0) */ // 秒杀相关 private static final String SKILL_QUEUE = "skillQueue"; private static final String SKILL_EXCHANGE = "skillExchange"; @Bean public Queue secKillQueue() { return new Queue(SKILL_QUEUE); } @Bean public TopicExchange secKillExchange() { return new TopicExchange(SKILL_EXCHANGE); } @Bean public Binding bindSecKill() { return BindingBuilder.bind(secKillQueue()).to(secKillExchange()).with("secKill.#"); } } ``` **RabbitMQReceiver.java** ```java @Service @Slf4j public class RabbitMQReceiver { private OrderService orderService; private GoodsService goodsService; private RedisTemplate redisTemplate; public RabbitMQReceiver() { } @Autowired public RabbitMQReceiver(GoodsService goodsService, RedisTemplate redisTemplate, OrderService orderService) { this.goodsService = goodsService; this.orderService = orderService; this.redisTemplate = redisTemplate; } @RabbitListener(queues = "skillQueue") public void secKillReceive(String msg) { log.info("skillQueue已接收到:{}", msg); //处理消息,下单 SecKillMessage secKillMessage = JsonUtil.jsonStr2Object(msg, SecKillMessage.class); if (secKillMessage != null) { Long goodsId = secKillMessage.getGoodsId(); User user = secKillMessage.getUser(); GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); if (goodsVo == null) { return; } if (goodsVo.getStockCount() < 1) { return; } // 判断是否重复加购 Order order = (Order) redisTemplate.opsForValue().get("secOrder:" + user.getId() + ":" + goodsId); if (order != null) { return; } // 下单 order = orderService.secKill(user, goodsVo); //放到缓存中 redisTemplate.opsForValue().set("secOrder:" + user.getId() + ":" + goodsId, order); } } } ``` **RabbitMQSender.java** ```java @Service @Slf4j public class RabbitMQSender { private RabbitTemplate rabbitTemplate; public RabbitMQSender() { } @Autowired public RabbitMQSender(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } public void sendSecKillMsg(Object msg){ log.info("skillExchange发送:{}",msg); rabbitTemplate.convertAndSend("skillExchange","secKill",msg); } } ``` **秒杀接口优化** ```java @RequestMapping(value = "/doSecKill", method = RequestMethod.POST) @ResponseBody public ResponseBean doSecKillWithRedisAndRabbitMQ(User user, Long goodsID) { //判断用户是否登录 if (user == null) { return ResponseBean.error(ResponseBeanEnum.UNLOGIN_ERROR); } //判断标记位 if (isEmpty.get(goodsID)) { return ResponseBean.error(ResponseBeanEnum.SKILL_ERROR_GOODS_SHORTAGE); } //获取redis ValueOperations operations = redisTemplate.opsForValue(); // 判断是否重复加购 Order order = (Order) operations.get("secOrder:" + user.getId() + ":" + goodsID); if (order != null) { return ResponseBean.error(ResponseBeanEnum.SKILL_ERROR_GOODS_LOP); } // 预减库存,实现InitializingBean,重写afterPropertiesSet方法 // 预减库存操作 + 修改标记位 Long stock = operations.decrement("secKillGoods:" + goodsID); if (stock != null && stock < 0) { operations.increment("secKillGoods:" + goodsID); isEmpty.put(goodsID, true); //库存不足 return ResponseBean.error(ResponseBeanEnum.SKILL_ERROR_GOODS_SHORTAGE); } // 下单,rabbitMQ, SecKillMessage secKillMessage = new SecKillMessage(user, goodsID); // 流量削峰 rabbitMQSender.sendSecKillMsg(JsonUtil.object2JsonStr(secKillMessage)); // 返回0,页面显示正在排队ing return ResponseBean.success(0); } ```