找回密码
 立即注册
首页 资源区 代码 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mys ...

秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis

虾氲叱 2025-7-10 13:27:31
秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter ) -03

优化秒杀: Redis 预减库存+Decrement


  • Github:China-Rainbow-sea/seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )
  • Gitee:seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )

  • 前面我们防止超卖 是通过到数据库查询和到数据库抢购,来完成的, 代码如下:




  • 如果在短时间内,大量抢购冲击 DB, 造成洪峰, 容易压垮数据库
  • 解决方案:使用 Redis 完成预减库存,如果没有库存了,直接返回,减小对 DB 的压力。
  • 图示:


Redis 的预减,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。
修改 SeckillController.java 实现 InitializingBean 接口,
InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容

  1. package com.rainbowsea.seckill.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.rainbowsea.seckill.pojo.Order; import com.rainbowsea.seckill.pojo.SeckillOrder; import com.rainbowsea.seckill.pojo.User; import com.rainbowsea.seckill.service.GoodsService; import com.rainbowsea.seckill.service.OrderService; import com.rainbowsea.seckill.service.SeckillOrderService; import com.rainbowsea.seckill.vo.GoodsVo; import com.rainbowsea.seckill.vo.RespBeanEnum; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMapping; import javax.annotation.Resource; import java.util.List; @Controller @RequestMapping("/seckill") // InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 public class SeckillController implements InitializingBean { // 装配需要的组件/对象 @Resource private GoodsService goodsService; @Resource private SeckillOrderService seckillOrderService; @Resource private OrderService orderService; @Resource private RedisTemplate redisTemplate; /** * 方法: 处理用户抢购请求/秒杀 * 说明: 我们先完成一个 V3.0版本, * - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 * - 使用 优化秒杀: Redis 预减库存+Decrement * * @param model 返回给模块的 model 信息 * @param user User 通过用户使用了,自定义参数解析器获取 User 对象, * @param goodsId 秒杀商品的 ID 信息 * @return 返回到映射在 resources 下的 templates 下的页面 */ @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 2.0 "); if (null == user) { //用户没有登录 return "login"; } // 登录了,则返回用户信息给下一个模板内容 model.addAttribute("user", user); // 获取到 GoodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); // 判断库存 if (goodsVo.getStockCount() < 1) { // 没有库存,不可以购买 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // 判断用户是否复购-直接到 Redis 当中获取(因为我们抢购成功直接 // 将表单信息存储到了Redis 当中了。 key表示:order:userId:goodsId Value表示订单 seckillOrder), // 获取对应的秒杀订单,如果有,则说明该 // 用户已经桥抢购了,每人限购一个 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId()); // 因为我们在 Redis 当中的 value值就是 SeckillOrder 订单对象,所以这里可以直接强制类型转换 if (null != o) { // 不为null,说明 Redis 存在该用户订单信息,说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 抢购 Order order = orderService.seckill(user, goodsVo); if (order == null) { // 说明抢购失败了,由于什么原因 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // 走到这里,说明抢购成功了,将信息,通过 model 返回给页面 model.addAttribute("order", order); model.addAttribute("goods", goodsVo); System.out.println("秒杀 V 2.0 "); return "orderDetail"; // 进入到订单详情页 } /** * InitializingBean 接口当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 * 该方法是在类的所有属性,都是初始化后,自动执行的 * 这里我们就可以将所有秒杀商品的库存量,加载到 Redis 当中 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 获取所有可以秒杀的商品信息 List list = goodsService.findGoodsVo(); // 先判断是否为空 if (CollectionUtils.isEmpty(list)) { return; } // 遍历 List,然后将秒杀商品的库存量,放入到 Redis // key:秒杀商品库存量对应 key:seckillGoods:商品Id,value:该商品的库存量 list.forEach( goodsVo -> { redisTemplate.opsForValue() .set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()); } ); } }
复制代码
测试
启动项目测试时, 确保多用户的 userticket 已经正确的保存到 Redis 中

确保商品库存已经正确的加载/保存到 Redis 中

启动线程组,进行测试
测试结果, 不在出现超卖和复购问题


一个思考题:
预减库存的代码, 能否放在 防止复购代码之前? 试分析可能出现什么情况?
预减库存的代码, 能否放在 防止复购代码之前? 试分析可能出现什么情况?
不可以,这样会导致,我们的预减,减了之后,发现该用户其实已经复购了
,则Redis 当中存储的库存信息减了,但是该用户却时不能购买的,就会
导致,有DB数据库,库存存在遗留问题,10w 用户抢购,只有 1w个商品
却还有遗留的问题。

优化秒杀: 加入内存标记,避免总到 Redis 查询库存


  • 如果某个商品库存已经为空了, 我们仍然是到 Redis 去查询的, 还可以进行优化
  • 解决方案: 给商品进行内存标记(存储到我们自己的内存当中), 如果库存为空, 直接返回, 避免总是到 Redis 查询库存
**使用map进行内存标记的设计思路: **

  • 在本机JVM的 map 记录所有秒杀商品是否还有库存。
  • 在执行预减库存,先到 map 去查询是否该秒杀商品还有库存。如果没有库存,则直接返回,如果有库存,则继续到 Redis 预减库存
  • 操作本机 JVM内存,快于操作 Redis


修改 SeckillController.java 添加上一个 Map 属性用于,如果某个商品库存已经为空,
则标记到 entryStockMap


  1. package com.rainbowsea.seckill.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.rainbowsea.seckill.pojo.Order; import com.rainbowsea.seckill.pojo.SeckillOrder; import com.rainbowsea.seckill.pojo.User; import com.rainbowsea.seckill.service.GoodsService; import com.rainbowsea.seckill.service.OrderService; import com.rainbowsea.seckill.service.SeckillOrderService; import com.rainbowsea.seckill.vo.GoodsVo; import com.rainbowsea.seckill.vo.RespBeanEnum; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMapping; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; @Controller @RequestMapping("/seckill") // InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 public class SeckillController implements InitializingBean { // 装配需要的组件/对象 @Resource private GoodsService goodsService; @Resource private SeckillOrderService seckillOrderService; @Resource private OrderService orderService; // 如果某个商品库存已经为空, 则标记到 entryStockMap @Resource private RedisTemplate redisTemplate; // 定义 map- 记录秒杀商品 private HashMap entryStockMap = new HashMap<>(); /** * 方法: 处理用户抢购请求/秒杀 * 说明: 我们先完成一个 V 4.0版本, * - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 * - 使用 优化秒杀: Redis 预减库存+Decrement * - 优化秒杀: 加入内存标记,避免总到 Redis 查询库存 * * @param model 返回给模块的 model 信息 * @param user User 通过用户使用了,自定义参数解析器获取 User 对象, * @param goodsId 秒杀商品的 ID 信息 * @return 返回到映射在 resources 下的 templates 下的页面 */ @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 4.0 "); // 定义 map - 记录秒杀商品是否还有库存 if (null == user) { //用户没有登录 return "login"; } // 登录了,则返回用户信息给下一个模板内容 model.addAttribute("user", user); // 获取到 GoodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); // 判断库存 if (goodsVo.getStockCount() < 1) { // 没有库存,不可以购买 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // 判断用户是否复购-直接到 Redis 当中获取(因为我们抢购成功直接 // 将表单信息存储到了Redis 当中了。 key表示:order:userId:goodsId Value表示订单 seckillOrder), // 获取对应的秒杀订单,如果有,则说明该 // 用户已经桥抢购了,每人限购一个 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId()); // 因为我们在 Redis 当中的 value值就是 SeckillOrder 订单对象,所以这里可以直接强制类型转换 if (null != o) { // 不为null,说明 Redis 存在该用户订单信息,说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 对map进行判断[内存标记],如果商品在 map 已经标记为没有库存,则直接返回,无需进行 Redis 预减 if (entryStockMap.get(goodsId)) { model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 说明当前秒杀的商品,已经没有库存 entryStockMap.put(goodsId, true); // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 抢购 Order order = orderService.seckill(user, goodsVo); if (order == null) { // 说明抢购失败了,由于什么原因 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // 走到这里,说明抢购成功了,将信息,通过 model 返回给页面 model.addAttribute("order", order); model.addAttribute("goods", goodsVo); System.out.println("秒杀 V 4.0 "); return "orderDetail"; // 进入到订单详情页 } /** * InitializingBean 接口当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 * 该方法是在类的所有属性,都是初始化后,自动执行的 * 这里我们就可以将所有秒杀商品的库存量,加载到 Redis 当中 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 获取所有可以秒杀的商品信息 List list = goodsService.findGoodsVo(); // 先判断是否为空 if (CollectionUtils.isEmpty(list)) { return; } // 遍历 List,然后将秒杀商品的库存量,放入到 Redis // key:秒杀商品库存量对应 key:seckillGoods:商品Id,value:该商品的库存量 list.forEach( goodsVo -> { redisTemplate.opsForValue() .set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()); // 初始化 map // 如果 goodsId: false 表示有库存 // 如果 goodsId: true 表示没有库存 entryStockMap.put(goodsVo.getId(), false); }); } }
复制代码
测试
启动项目测试时, 确保多用户的 userticket 已经正确的保存到 Redis 中

确保商品库存已经正确的加载/保存到 Redis 中

启动线程组,进行测试
测试结果, 不在出现超卖和复购问题


优化秒杀: 加入消息队列,实现秒杀的异步请求

前面秒杀,没有实现异步机制,是完成下订单后,再返回的,当有大并发请求
加入消息队列,实现秒杀的异步请求下订单操作时,数据库来不及响应,容易造成线程堆积。
解决方案:

  • 加入消息队列,实现秒杀的异步请求。
  • 接收到客户端秒杀请求后,服务器立即返回,正在秒杀中...,有利于流量削峰。
  • 客户端进行轮询秒杀结果,接收到秒杀结果后,在客户端页面显示即可。
  • 秒杀消息发送设计:SeckillMessage - String



RabbitMQ 启动,配合 Spring Boot 配置
引入:Spring Boot 当中相关的 RabbitMQ 的 jar 包
  1. org.springframework.boot spring-boot-starter-amqp
复制代码
  1. spring: rabbitmq: host: 192.168.76.156 username: admin password: 123 #虚拟主机 virtual-host: / #端口 port: 5672 listener: simple: #消费者的最小数量 concurrency: 10 #消费者的最大数量 max-concurrency: 10 #限制消费者,每次只能处理一条消息,处理完才能继续下一条消息 prefetch: 1 #启动时,是否默认启动容器,默认true auto-startup: true #被拒绝后,重新进入队列 default-requeue-rejected: true template: retry: #启用重试机制,默认false enabled: true #设置初始化的重试时间间隔 initial-interval: 1000ms #重试最大次数,默认是3 max-attempts: 3 #重试最大时间间隔,默认是10s max-interval: 10000ms #重试时间间隔的乘数 #比如配置是2 :第1次等 1s, 第2次等 2s,第3次等 4s.. #比如配置是1 :第1次等 1s, 第2次等 1s,第3次等 1s.. multiplier: 1
复制代码
项目的完整 yaml 配置信息
  1. spring: thymeleaf: #关闭缓存 cache: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&useSSL=true username: root password: MySQL123 # 数据库连接池 hikari: #连接池名 pool-name: Hsp_Hikari_Poll #最小空闲连接 minimum-idle: 5 #空闲连接存活最大时间,默认60000(10分钟) idle-timeout: 60000 # 最大连接数,默认是10 maximum-pool-size: 10 #从连接池返回来的连接自动提交 auto-commit: true #连接最大存活时间。0表示永久存活,默认180000(30分钟) max-lifetime: 180000 #连接超时时间,默认30000(30秒) connection-timeout: 30000 #测试连接是否可用的查询语句 connection-test-query: select 1 # 配置Redis redis: host: 192.168.76.168 port: 6379 password: rainbowsea database: 0 timeout: 10000ms lettuce: pool: #最大连接数,默认是8 max-active: 8 #最大连接等待/阻塞时间,默认-1 max-wait: 10000ms #最大空闲连接 max-idle: 200 #最小空闲连接,默认0 min-idle: 5 # rabbimq 配置 rabbitmq: host: 192.168.76.156 username: admin password: 123 #虚拟主机 virtual-host: / #端口 port: 5672 listener: simple: #消费者的最小数量 concurrency: 10 #消费者的最大数量 max-concurrency: 10 #限制消费者,每次只能处理一条消息,处理完才能继续下一条消息 prefetch: 1 #启动时,是否默认启动容器,默认true auto-startup: true #被拒绝后,重新进入队列 default-requeue-rejected: true template: retry: #启用重试机制,默认false enabled: true #设置初始化的重试时间间隔 initial-interval: 1000ms #重试最大次数,默认是3 max-attempts: 3 #重试最大时间间隔,默认是10s max-interval: 10000ms #重试时间间隔的乘数 #比如配置是2 :第1次等 1s, 第2次等 2s,第3次等 4s.. #比如配置是1 :第1次等 1s, 第2次等 1s,第3次等 1s.. multiplier: 1 #mybatis-plus配置 mybatis-plus: #配置mapper.xml映射文件 mapper-locations: classpath*:/mapper/*Mapper.xml #配置mybatis数据返回类型别名 type-aliases-package: com.rainbowsea.seckill.pojo #mybatis sql 打印 #logging: # level: # com.rainbowsea.seckill.mapper: debug server: port: 8080
复制代码
创建一个 SeckillMessage pojo 类,用于 RabbitMQ 生产者,消费者之间发送信息的封装。同时我们后续需要将该对象转换为 JSON 格式的 String 进行在 RabbitMQ 消息队列当中发送传输处理。
引入 JSON 转换工具 的 jar 包类
  1. cn.hutool hutool-all 5.3.3
复制代码
  1. package com.rainbowsea.seckill.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * SeckillMessage 秒杀消息对象,用于 RabbitMQ 消息队列进行发送传输 */ @Data @NoArgsConstructor @AllArgsConstructor public class SeckillMessage { private User user; private Long goodsId; }
复制代码
创建 RabbitMQSecKillConfig ,作为 RabbitMQ 的消息队列的配置。
配置类,RabbitMQ 创建消息队列和交换机,以及消息队列和交换机的之间的关系
  1. package com.rainbowsea.seckill.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.amqp.core.Queue; /** * 配置类,RabbitMQ 创建消息队列和交换机,以及消息队列和交换机的之间的关系 */ @Configuration public class RabbitMQSecKillConfig { // 定义消息队列和交换机名 public static final String QUEUE = "seckillQueue"; public static final String EXCHANGE = "seckillExchange"; /** * 创建队列 * * @return Queue 队列 */ @Bean // 没有指明 value ,默认就是方法名 public Queue queue_seckill() { return new Queue(QUEUE); } /** * @return TopicExchange 主题交换机 */ @Bean public TopicExchange topicExchange_seckill() { return new TopicExchange(EXCHANGE); } /** * 将队列绑定到对应的交换机当中,并指定路由,"主题"(哪些信息发送给 seckill.# 哪个队列) * * @return */ @Bean public Binding binding_seckill() { return BindingBuilder.bind(queue_seckill()).to(topicExchange_seckill()) .with("seckill.#"); } }
复制代码
创建 MQSenderMessage 对象,作为消息队列的发送者,发送信息
消息的生产者/发送者 发送【秒杀消息】
  1. package com.rainbowsea.seckill.rabbitmq; import com.rainbowsea.seckill.config.RabbitMQSecKillConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * 消息的生产者/发送者 发送【秒杀消息】 */ @Slf4j @Service public class MQSenderMessage { // 装配 RabbitTemplate @Resource private RabbitTemplate rabbitTemplate; /** * 发送者,将信息发送给交换机 * * @param message */ public void sendSeckillMessage(String message) { log.info("发送消息: " + message); rabbitTemplate.convertAndSend(RabbitMQSecKillConfig.EXCHANGE, "seckill.message", // 对应队列的 routingKey message); } }
复制代码
创建:MQReceiverConsumer 对象类,作为:消息的接收者/消费者,接收生产者,发送过来的信息
接收到信息后,调用秒杀商品的方法,orderService.seckill(user, goodsVo);

  1. package com.rainbowsea.seckill.rabbitmq; import cn.hutool.json.JSONUtil; import com.rainbowsea.seckill.config.RabbitMQSecKillConfig; import com.rainbowsea.seckill.pojo.SeckillMessage; import com.rainbowsea.seckill.pojo.User; import com.rainbowsea.seckill.service.GoodsService; import com.rainbowsea.seckill.service.OrderService; import com.rainbowsea.seckill.vo.GoodsVo; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * 消息的接收者/消费者,接收生产者,发送过来的信息 * ,接收到信息后,调用秒杀商品的方法,orderService.seckill(user, goodsVo); */ @Service @Slf4j public class MQReceiverConsumer { @Resource private GoodsService goodsService; @Resource private OrderService orderService; /** * 接受这个 queues = RabbitMQSecKillConfig.QUEUE 队列的当中的信息 * * @param message 生产者发送的信息,其实就是 seckillMessage 对象信息,被我们转换为了 JSON * 格式的 String */ @RabbitListener(queues = RabbitMQSecKillConfig.QUEUE) public void queue(String message) { log.info("接收到的消息是: " + message); /* 这里我么们从队列中取出的是 String 类型 但是,我们需要的是 SeckillMessage,因此需要一个工具类 JSONUtil ,该工具需要引入 hutool 工具类的 jar 包 */ SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class); // 秒杀用户对象 User user = seckillMessage.getUser(); // 秒杀用户的商品ID Long goodsId = seckillMessage.getGoodsId(); // 通过商品ID,得到对应的 GoodsVo 秒杀商品信息对象 GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); // 下单操作 orderService.seckill(user, goodsVo); } }
复制代码
控制层处理:采用消息队列:SeckillController


  1. package com.rainbowsea.seckill.controller; import cn.hutool.json.JSONUtil; import com.rainbowsea.seckill.pojo.SeckillMessage; import com.rainbowsea.seckill.pojo.SeckillOrder; import com.rainbowsea.seckill.pojo.User; import com.rainbowsea.seckill.rabbitmq.MQSenderMessage; import com.rainbowsea.seckill.service.GoodsService; import com.rainbowsea.seckill.service.OrderService; import com.rainbowsea.seckill.service.SeckillOrderService; import com.rainbowsea.seckill.vo.GoodsVo; import com.rainbowsea.seckill.vo.RespBeanEnum; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMapping; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; @Controller @RequestMapping("/seckill") // InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 public class SeckillController implements InitializingBean { // 装配需要的组件/对象 @Resource private GoodsService goodsService; @Resource private SeckillOrderService seckillOrderService; @Resource private OrderService orderService; // 如果某个商品库存已经为空, 则标记到 entryStockMap @Resource private RedisTemplate redisTemplate; // 定义 map- 记录秒杀商品 private HashMap entryStockMap = new HashMap<>(); // 装配消息的生产者/发送者 @Resource private MQSenderMessage mqSenderMessage; /** * 方法: 处理用户抢购请求/秒杀 * 说明: 我们先完成一个 V 5.0版本, * - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 * - 使用 优化秒杀: Redis 预减库存+Decrement * - 优化秒杀: 加入内存标记,避免总到 Redis 查询库存 * - 优化秒杀: 加入消息队列,实现秒杀的异步请求 * * @param model 返回给模块的 model 信息 * @param user User 通过用户使用了,自定义参数解析器获取 User 对象, * @param goodsId 秒杀商品的 ID 信息 * @return 返回到映射在 resources 下的 templates 下的页面 */ @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 5.0 "); // 定义 map - 记录秒杀商品是否还有库存 if (null == user) { //用户没有登录 return "login"; } // 登录了,则返回用户信息给下一个模板内容 model.addAttribute("user", user); // 获取到 GoodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); // 判断库存 if (goodsVo.getStockCount() < 1) { // 没有库存,不可以购买 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // 判断用户是否复购-直接到 Redis 当中获取(因为我们抢购成功直接 // 将表单信息存储到了Redis 当中了。 key表示:order:userId:goodsId Value表示订单 seckillOrder), // 获取对应的秒杀订单,如果有,则说明该 // 用户已经桥抢购了,每人限购一个 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsVo.getId()); // 因为我们在 Redis 当中的 value值就是 SeckillOrder 订单对象,所以这里可以直接强制类型转换 if (null != o) { // 不为null,说明 Redis 存在该用户订单信息,说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 对map进行判断[内存标记],如果商品在 map 已经标记为没有库存,则直接返回,无需进行 Redis 预减 if (entryStockMap.get(goodsId)) { model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail"; // 返回一个错误页面 } // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 说明当前秒杀的商品,已经没有库存 entryStockMap.put(goodsId, true); // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } /* 抢购,向消息队列发送秒杀请求,实现了秒杀异步请求 这里我们发送秒杀消息后,立即快速返回结果【临时结果】- “比如排队中...” 客户端可以通过轮询,获取到最终结果 创建 SeckillMessage */ SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); // 将 seckillMessage 对象封装为 JSON 格式的 String 让RabbitMQ 生产者发送出去 // 被消费者接受消费 mqSenderMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage)); model.addAttribute("errmsg","排队中..."); System.out.println("秒杀 V 5.0 "); return "secKillFail"; // 进入到表示排队中信息 } /** * InitializingBean 接口当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 * 该方法是在类的所有属性,都是初始化后,自动执行的 * 这里我们就可以将所有秒杀商品的库存量,加载到 Redis 当中 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 获取所有可以秒杀的商品信息 List list = goodsService.findGoodsVo(); // 先判断是否为空 if (CollectionUtils.isEmpty(list)) { return; } // 遍历 List,然后将秒杀商品的库存量,放入到 Redis // key:秒杀商品库存量对应 key:seckillGoods:商品Id,value:该商品的库存量 list.forEach( goodsVo -> { redisTemplate.opsForValue() .set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()); // 初始化 map // 如果 goodsId: false 表示有库存 // 如果 goodsId: true 表示没有库存 entryStockMap.put(goodsVo.getId(), false); }); } }
复制代码
测试:和上述测试一样。重置相关数据表


补充:客户端轮询秒杀结果-思路分析示意图


6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册