redis+rabbitmq+jmeter模拟实现秒杀系统

SpringBoot项目,实现亿万级电商秒杀系统

1. 解析思路

  1. 用schedule任务调度每分钟扫描数据库中的就近的秒杀项目,符合的项目以seckill::count::id - goodsId的形式放入redis的List中
  2. 前端用户下单,抢到的用户以seckill::users::id - userId的形式放入redis的set中,防重复,同时list中pop一个
  3. 为每位抢到的用户生成一个UUID的订单号orderNo,返回给用户,同时通知消息队列{userId,orderNo}进行订单创建
  4. 后台监听消息队列中的消息并创建出订单
  5. 让每位用户等待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){
//Long date = activity.getEndTime().getTime()-new Date().getTime();
System.out.println(activity.getId() + " 号商品秒杀活动已启动");
//删除预存
redisTemplate.delete(redisKey+activity.getId());
//库存放入redis中
Long secCount = activity.getSecCount();
String obj = null;
for (int i = 0; i < secCount; i++) {
//seckill::count::id - goodsId
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("活动已结束");
}
//set中是否已有该用户
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) {
//放入set中 seckill::users::id - userId
redisTemplate.opsForSet().add(userRedisKey + activity.getId(), userId);
}else{
throw new SecurityException("抱歉,该商品已被抢光");
}

String orderNo = orderService.sendOrdertoQueue(userId);
return orderNo;
}


@Autowired
RabbitTemplate rabbitTemplate;
/**
*生成orderNo并加入消息队列
*/
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
#消费者最多同时处理10个消息
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){
//{"orderNo":"f0d2a256e9cd42278ba13ada597a87a7","userId":"user-3"}'
log.info("Message为 {}",message);
log.info("获取到订单 {}",map);

//TO DO LIST
//对接支付宝,物流,日志登记
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) {
//订单创建成功,确认消息,false表示每次仅接受一个请求
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}else{
//订单创建失败,拒绝消息,回滚给其他消费者
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
} catch (IOException e) {
e.printStackTrace();
}

//System.out.println(map);
}

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 {
//System.out.println("pre");
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);
//System.out.println(flag);
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;
}
}
本文结束  感谢您的阅读