秒杀项目

软件架构

技术 版本
SpringBoot 2.6.13
MyBatis Plus 3.5.2
MySql 8
Redis 7.2
RebbitMQ 3.13.3-management

数据库表

基础功能

登录页

LoginController

商品列表页

GoodsController

商品详情页

SeckillOrderController

订单详情页

OrderController

秒杀功能

大致经过三个阶段的优化

  1. 在该项目中核心就是秒杀的实现:不能超卖、不能重复抢
    • 不能超卖在doSeckill中通过update的排他性实现(乐观锁)。而在doSeckill1中通过redis预减库存(redis的原子性实现)
    • 不能重复抢通过唯一索引实现,默认建表时没有添加,压测可以把用户加少点商品多一点就可以复现重复购买
  2. 优化不过就是把数据库的重复访问,能放到redis就放到redis;而如果访问redis太多了就再加一层内存标记
  3. redis和mysql要么都在远程,要么都在本地,否则可能会出现redis缓存优化了但QPS没提升

秒杀的接口有三个

  1. doSeckill:update排他+唯一索引实现秒杀 解决超买超卖
1
2
3
4
5
6
7
8
9
10
// 到这里为了实现不超卖(减少库存的同时判断库存数量)且单一下单(唯一索引)
boolean update = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>()
.setSql("stock_count=" + "stock_count-1")
.eq("goods_id", goodsDto.getId())
.gt("stock_count", 0));

// 否则下单事务直接结束。update是排他锁,一定不会超卖
if(!update){
return null;
}

2.doSeckill1:redis预减库存 + 内存标记 + MQ+前端轮询 提高qps

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
//用redis预减库存(减少数据库访问),redis是原子操作,可以防止超卖,并减少redis访问次数:引入内存标记
List<GoodsDto> goodsDto = goodsService.findGoodsDto();
if (CollectionUtils.isEmpty(goodsDto)) {
return;
}
goodsDto.forEach(
goodDto -> {
hashMap.put(goodDto.getId(), false);
redisTemplate.opsForValue().set("seckillGoods:" + goodDto.getId(), goodDto.getStockCount());
}
);

//满足还有库存后进入MQ队列,进行异步下单
@RabbitListener(queues = "seckillQueue")
public void receive(String message){
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue()
.get("order:" + seckillMessage.getUser().getId() + ":" + seckillMessage.getGoodsId());
if (seckillOrder != null) {
return ;
}
GoodsDto goodsDto = goodsService.fintGoodsDtoById(seckillMessage.getGoodsId());
if(goodsDto.getStockCount()<1){
return;
}
orderService.seckill(seckillMessage.getUser(),goodsDto);
}

//前端轮询

3.doSeckill2:最终秒杀方案 一些安全上的优化(验证码,限流)

压测

准备用户

使用utils包下的UserUtil生成模拟用户(把用户信息存放到mysql数据库中并生成config.txt用于压测时设置CSV数据文件,并把用户信息ticket存放在redis中,因为有拦截器每次进入任意接口都需要校验用户ticket)

压测过程

测试计划->添加线程组

线程组->添加配置元件->HTTP默认请求

线程组->添加配置元件->HTTP Cookie 管理器

线程组->添加配置元件->CSV Data Set Config

线程组->取样器->HTTP请求

线程组->监听器->聚合报告/查看结果树

压测结果

  • QPS:每秒请求次数
  • TPS:每秒事务(吞吐量)次数
  • 一个页面一个TPS可能多次QPS

/seckillorder/doseckill接口 压测结果 1955.2

/seckillorder/doseckill1接口 压测结果 5841.1

可以看到提升还是比较明显的,这得益于我们使用redis预减库存 + 内存标记 + MQ提高了qps