SpringBoot项目,实现亿万级电商秒杀系统
1. 解析思路
- 用schedule任务调度每分钟扫描数据库中的就近的秒杀项目,符合的项目以seckill::count::id - goodsId的形式放入redis的List中
 
- 前端用户下单,抢到的用户以seckill::users::id - userId的形式放入redis的set中,防重复,同时list中pop一个
 
- 为每位抢到的用户生成一个UUID的订单号orderNo,返回给用户,同时通知消息队列{userId,orderNo}进行订单创建
 
- 后台监听消息队列中的消息并创建出订单
 
- 让每位用户等待3秒后,凭借orderNo查询后台,如果订单已创建,则该用户可以进行支付
 
2. Apache jmeter压力测试
https://jmeter.apache.org/download_jmeter.cgi
To run JMeter, run the jmeter.bat (for Windows) or jmeter (for Unix) file. 
2.1. 新建线程组
主要配置,线程数和每个线程的请求次数

2.2. 配置请求的路径

2.3. 监听结果

2.4. 测试

3. 秒杀表设计
1 2 3 4 5 6 7 8 9 10
   | create table seckill_activity( 	id int auto_increment not null, 	goods_id varchar(50) not null, 	sec_count long not null, 	start_time datetime, 	end_time datetime, 	status int COMMENT'0 未开始 1 进行中 2 已结束', 	current_price decimal(10,2), 	primary key(id) )
   | 
 
查找未开始的活动
select * from seckill_activity where now() between start_time and end_time and status=0
4. 开启任务调度
@EnableScheduling
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | package com.runaccepted.gmall.seckill;
  import org.mybatis.spring.annotation.MapperScan; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.scheduling.annotation.EnableScheduling;
  @SpringBootApplication @MapperScan(basePackages = "com.runaccepted.gmall.seckill.mapper") @EnableCaching @EnableScheduling public class GmallSeckillApplication {
      public static void main(String[] args) {         SpringApplication.run(GmallSeckillApplication.class, args);     }
  }
   | 
 
@Scheduled(cron = “秒 分 时 日 月 周”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
   | private  String redisKey = "seckill::count::";
  @Override @Scheduled(cron = "0 1 * * * ?") public List<SeckillActivity> findUnstartSeckill() {     List<SeckillActivity> activities= mapper.findUnstartSeckill();     for (SeckillActivity activity:activities){                  System.out.println(activity.getId() + " 号商品秒杀活动已启动");                  redisTemplate.delete(redisKey+activity.getId());                  Long secCount = activity.getSecCount();         String obj = null;         for (int i = 0; i < secCount; i++) {                          redisTemplate.opsForList().rightPush(redisKey+activity.getId(),activity.getGoodsId());         }                  activity.setStatus(1);
          mapper.update(activity);     }     return activities; }
   | 
 
5. 用户抢单
5.1. 生成orderNo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
   | @GetMapping("/seckill/{id}/{userId}") @ResponseBody public CommonResult processSecKill(@PathVariable  int id, @PathVariable String userId){     String result = activityService.seckill(id,userId);     CommonResult commonResult = CommonResult.ok("抢到了,等待订单生成!").data("orderNo",result);     return commonResult; }
  @Override public String seckill(int id,String userId) {          SeckillActivity activity = mapper.findById(id);     if (activity==null){         throw new SecurityException("活动不存在");     }     else if (activity.getStatus()==0){         throw new SecurityException("活动未开始");     }     else if (activity.getStatus()==2){         throw new SecurityException("活动已结束");     }         Boolean member = redisTemplate.opsForSet().isMember(userRedisKey + activity.getId(), userId);     if(member){         throw new SecurityException("每位用户限购一个");     }          String s = redisTemplate.opsForList().leftPop(redisKey + activity.getId());
      if(s!=null) {                  redisTemplate.opsForSet().add(userRedisKey + activity.getId(), userId);     }else{         throw new SecurityException("抱歉,该商品已被抢光");     }
      String orderNo = orderService.sendOrdertoQueue(userId);     return orderNo; }
 
  @Autowired RabbitTemplate rabbitTemplate;
 
 
  public String sendOrdertoQueue(String userId){     String orderNo = UUID.randomUUID().toString().replace("-","");     Map<String,String> map = new HashMap<>();     map.put("userId",userId);     map.put("orderNo",orderNo);        rabbitTemplate.convertAndSend("pay.exchage","pay.binding",map);
      log.info("{} 创建了订单 {}",userId,orderNo);     return orderNo; }
  | 
 
5.2. 采用rabbitMQ进行流量削峰
先完成抢单,确定用户和订单号,后台慢慢完成订单的创建,而等到订单创建完成,用户才可以支付
配置
1 2 3 4
   |  spring.rabbitmq.listener.simple.prefetch=10
  spring.rabbitmq.listener.simple.acknowledge-mode=manual
 
  | 
 
监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   | @RabbitListener(queues = "pay.queue") @RabbitHandler public void getMsg(Message message,Channel channel,@Payload Map<String,String> map){          log.info("Message为 {}",message);     log.info("获取到订单 {}",map);
                try {         Thread.sleep(1000);     } catch (InterruptedException e) {         e.printStackTrace();     }
      Map<String,String> result = map;     String orderNo = result.get("orderNo");     String userId = result.get("userId");
           SeckillOrder order = new SeckillOrder();     order.setOrderNo(orderNo);     order.setUserId(userId);     order.setCreateTime(new Date());     order.setOrderStatus(0);     order.setTradeNo(UUID.randomUUID().toString().replace("-",""));
      int flag = orderMapper.insertOrder(order);
      try {         if(flag>0) {                          channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);         }else{                          channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);         }     } catch (IOException e) {         e.printStackTrace();     }
       }
   | 
 
5.3. 查找到订单后支付
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | @Override public SeckillOrder findOrder(String orderNo) {     return orderMapper.findbyOrderNo(orderNo); }
 
 
  @Controller public class SeckillOrderController {
      @Autowired     SeckillOrderService orderService;
      @GetMapping("/order/{orderNo}")     @ResponseBody     public CommonResult findOrder(@PathVariable String orderNo){         SeckillOrder order = orderService.findOrder(orderNo);         if(order!=null){             return CommonResult.ok("及时付款");         }else{             return CommonResult.error("继续等待");         }     }
      @GetMapping("/pay")     public ModelAndView pay(){         ModelAndView view = new ModelAndView("/pay");         return view;     } }
   | 
 
6. 用jmeter测试抢单
添加CSV Data Set Config
6.1. RabbitMQ

6.2. redis

6.3. 订单

7. 模拟下单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
   | <html> <meta charset="UTF-8"> <head>     <script charset="UTF-8" type="text/javascript" src="./js/jquery-3.5.0.min.js"></script> </head> <body>     <a href="#">秒杀开始</a> </body> <script type="text/javascript">     $(function() {         $("a").click(function () {             $.getJSON("/seckill/1/user-999",function(a){                 console.log(a)                 if(a.code == 20000){                     alert(a.msg)                     alert("正在创建订单...")                     var timer = setInterval(() => {                         $.getJSON("/order/"+a.data.orderNo,function (data) {                             if(data.code==20000){                                 alert(data.msg)                                 clearInterval(timer)                                 window.location.href="/pay";                             }else{                                 alert("正在创建订单...")                             }                         })                     }, 1000)                 }else{                     alert(a.msg)                 }             })             return false         })     });
  </script> </html>
   | 
 
8. 防恶意下单
设置拦截器,检查同一个ip的访问次数
>30停止服务 >100加入黑名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
   | package com.runaccepted.gmall.seckill.hander;
  import com.runaccepted.gmall.seckill.exception.SeckillException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import org.springframework.web.servlet.HandlerInterceptor;
  import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.concurrent.TimeUnit;
  @Component @Slf4j public class AntiHandler implements HandlerInterceptor {
      @Autowired     StringRedisTemplate redisTemplate;
      private String black = "anti::refresh::blacklist";     private String ipPrefix = "anti::refersh::";     private int stopTime = 30;     private int blackTime = 100;
      @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {                  response.setContentType("text/html;charset=utf-8");                String clientIp = request.getRemoteAddr();         System.out.println(clientIp);
          String user = request.getHeader("User-Agent");         String key = clientIp+"_"+user;
          System.out.println(key);
          String ip = ipPrefix+DigestUtils.md5DigestAsHex(key.getBytes());
          System.out.println(ip);
          boolean flag = redisTemplate.hasKey(black);                  if(flag) {             if (redisTemplate.opsForSet().isMember(black, ip)) {                 response.getWriter().print("该ip已被加入黑名单");                 return false;             }         }         Boolean hasKey = redisTemplate.hasKey(ip);         if(hasKey){             String s = redisTemplate.opsForValue().get(ip);             Integer num = Integer.parseInt(s);             if (num<stopTime) {                 redisTemplate.opsForValue().increment(ip, 1);             }else if(num<blackTime){
                  response.getWriter().print("访问次数过高,服务器已停止服务");                 redisTemplate.opsForValue().increment(ip, 1);                 return false;             }else{                 response.getWriter().print("访问次数过高,该ip已被加入黑名单");                 redisTemplate.opsForSet().add(black,ip);                 redisTemplate.expire(black,1,TimeUnit.DAYS);                 log.error("{} : {}",black,ip);                 return false;             }         }else{             redisTemplate.opsForValue().set(ip,1+"",1, TimeUnit.MINUTES);         }         log.error("{} 通过",ip);         return true;     } }
   |