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; } }
|