秒杀业务Demo

准备工作概述

商城秒杀业务的性质是高并发

我们的基本数据

并发1~2万

同时在线4~5万用户

日活跃用户10万

学习完秒杀业务,我们能具备处理一般高并发业务的基本逻辑

下面我们要做的是准备工作

除了基本的CRUD之外

我们还要做一些缓存预热工作

秒杀模块是mall-seckill,这个模块操作的数据库是mall-seckill

数据库中包含秒杀spu信息和秒杀sku信息以及秒杀成功记录

我们要利用Quartz周期性的将每个批次的秒杀商品,预热到Redis

所谓"预热"就是将即将出现高并发查询的数据提前保存在Redis中

我们的业务只是将每个商品的库存数保存在Redis即可

查询秒杀商品列表

mall-seckill-webapi项目

开发持久层

创建mapper包,创建SeckillSpuMapper

代码如下

@Repository
public interface SeckillSpuMapper {
  // 查询秒杀商品列表
  List findSeckillSpus();
}

SeckillSpuMapper.xml文件添加内容



  
    id,
    spu_id,
    list_price,
    start_time,
    end_time,
    gmt_create,
    gmt_modified
	


开发业务逻辑层

创建包service.impl

包中创建SeckillSpuServiceImpl实现ISeckillSpuService

@Service
@Slf4j
public class SeckillSpuServiceImpl implements ISeckillSpuService {

  // 查询秒杀spu表的数据
  @Autowired
  private SeckillSpuMapper seckillSpuMapper;
  // 秒杀spu表中没有商品详细介绍,需要根据spuid借助Dubbo查询product模块
  // pms数据库中的spu表,获得商品信息
  @DubboReference
  private IForSeckillSpuService dubboSeckillSpuService;

  @Override
  public JsonPage listSeckillSpus(Integer page, Integer pageSize) {
    // 分页查询秒杀表中spu信息
    PageHelper.startPage(page,pageSize);
    List seckillSpus=seckillSpuMapper.findSeckillSpus();
    // 我们返回给页面的,应该是包含商品详细信息的对象,不能只是SeckillSpu中的信息
    // 业务逻辑层返回值泛型类型SeckillSpuVO中包含秒杀信息和商品详细信息,返回它的集合可以满足查询需要
    List seckillSpuVOs=new ArrayList<>();
    // 循环遍历秒杀列表,根据秒杀商品列表中元素的spuId查询spu详情
    for(SeckillSpu seckillSpu: seckillSpus){
      // 获得SpuId
      Long spuId=seckillSpu.getSpuId();
      // dubbo调用查询商品详情
      SpuStandardVO spuStandardVO=dubboSeckillSpuService.getSpuById(spuId);
      // 实例化包含秒杀信息和商品信息的对象
      SeckillSpuVO seckillSpuVO=new SeckillSpuVO();
      // 将商品详情信息赋值给同名属性
      BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
      // 为了防止已有的值被意外覆盖,我们剩下的属性单独赋值
      // 赋值秒杀价
      seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
      // 赋值秒杀的开始时间和结束时间
      seckillSpuVO.setStartTime(seckillSpu.getStartTime());
      seckillSpuVO.setEndTime(seckillSpu.getEndTime());
      // 将包含商品详情和秒杀信息的seckillSpuVO对象保存在循环前定义的集合中
      seckillSpuVOs.add(seckillSpuVO);
    }
    // 翻页返回查询结果
    return JsonPage.restPage(new PageInfo<>(seckillSpuVOs));
  }

  @Override
  public SeckillSpuVO getSeckillSpu(Long spuId) {
    return null;
  }

  @Override
  public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
    return null;
  }
}

开发控制层

创建controller包

创建SeckillSpuController类

@RestController
@RequestMapping("/seckill/spu")
@Api(tags = "秒杀Spu模块")
public class SeckillSpuController {
  @Autowired
  private ISeckillSpuService seckillSpuService;

  @GetMapping("/list")
  @ApiOperation("分页查询秒杀列表Spu信息")
  @ApiImplicitParams({
    @ApiImplicitParam(value = "页码",name="page",required = true,dataType = "int"),
  @ApiImplicitParam(value = "每页条数",name="pageSize",required = true
                    ,dataType = "int")
  })
  // 查询秒杀列表不需要登录
  public JsonResult> listSeckillSpus(
    Integer page,Integer pageSize){
    JsonPage list=seckillSpuService.listSeckillSpus(page,pageSize);
    return JsonResult.ok(list);
  }

}

下面可以测试

Nacos\Redis\Seata

服务需要依次启动

Leaf\Product\passport\seckill

测试10007秒杀端口号

正常配置登录JWT

查询秒杀商品的Sku列表(开发持久层)

我们将秒杀的商品Spu列表查询出来

当用户选择一个商品时

我们要将这个商品的sku也查询出来

也就是根据SpuId查询Sku的列表

创建SeckillSkuMapper

@Repository
public interface SeckillSkuMapper {
  // 根据SpuId查询Sku列表
  List findSeckillSkusBySpuId(Long spuId);
}

SeckillSkuMapper.xml文件添加内容


  
    id,
    sku_id,
    spu_id,
    seckill_stock,
    seckill_price,
    gmt_create,
    gmt_modified,
    seckill_limit
  
  

根据时间查询正在进行秒杀的商品(开发持久层)

根据给定时间查询出正在进行秒杀的商品列表

首先保证数据库中的seckill_spu表的数据正在秒杀时间段(检查数据,如果不在秒杀时间段,将结束时间后移如2024年)

SeckillSpuMapper添加方法

// 根据给定的时间查询正在秒杀的商品
List findSeckillSpusByTime(LocalDateTime time);

SeckillSpuMapper.xml


查询所有秒杀商品的SpuId(开发持久层)

这次查询主要是因为后面我们要学习的布隆过滤器,方式缓存穿透使用的

SeckillSpuMapper,添加一个方法

// 查询所有秒杀商品的SpuId数据
Long[] findAllSeckillSpuIds();

SeckillSpuMapper.xml


缓存预热思路

我们要使用Quartz调度工具完成任务调度

按照秒杀的批次在秒杀开始前很短的时间内进行进行缓存预热工作

例如每天的12:00 14:00 16:00 18:00进行秒杀

那么就在 11:55 13:55 15:55 17:55 进行预热

1.我们预热的内容是将参与秒杀商品的sku查询出来,根据skuid将该商品的库存保存在Redis中

2.在秒杀开始后,当有用户查询秒杀商品后,将该商品保存在Redis中,还要注意防止雪崩(有效时间添加随机数)

3(待完善).在秒杀开始前,生成布隆过滤器,访问时先判断布隆过滤器,如果判断商品存在,再继续访问

设置定时任务

将库存和随机码保存到Redis

利用Quartz将库存和随机码保存到Redis

1.创建Job接口实现类

2.创建配置类,配置JobDetail和Trigger

在seckill包下创建timer.job包

包中创建SeckillInitialJob类

代码如下

@Slf4j
public class SeckillInitialJob implements Job {

  @Autowired
  private SeckillSpuMapper spuMapper;
  @Autowired
  private SeckillSkuMapper skuMapper;
  // 查询出来的数据要保存到Redis中
  @Autowired
  private RedisTemplate redisTemplate;

  // 上面的RedisTemplate对象是向Redis中保存对象用的,内部会将数据序列化后,
  // 以二进制的格式保存在Redis中,读写速度确实快,但是数据无法修改
  // 这种设计无法在Redis内部完成对象属性或值的修改
  // 我们的库存是一个数字,Redis支持直接在Redis内部对数字进行增减,减少java操作
  // 而前提是必须保存字符串格式数据,而不是二进制格式
  // 我们需要声明一个用字符串操作Redis的对象
  @Autowired
  private StringRedisTemplate stringRedisTemplate;


  @Override
  public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    //我们做的是预热,在秒杀还没有开始的时候,将要开始参与秒杀的商品库存保存到redis
    // 这个方法运行时,距离下次秒杀开始还有5分钟
    // 所以我们创建一个5分钟之后的时间,查询5分钟后要参与秒杀的商品
    LocalDateTime time=LocalDateTime.now().plusMinutes(5);
    // 查询这个时间的所有秒杀商品
    List seckillSpus=spuMapper.findSeckillSpusByTime(time);
    // 遍历seckillSpus(查询到的所有秒杀商品)
    // 目标是将这些商品对应的sku库存保存到Redis
    // 为了方便随机数生成定一个对象
    Random ran=new Random();
    for(SeckillSpu spu:seckillSpus){
      // spu 是一个商品品类,库存不在spu中,而是sku保存库存
      // 所以要根据spuId查询Sku集合
      List seckillSkus=skuMapper
        .findSeckillSkusBySpuId(spu.getSpuId());
      // 遍历当前spu的所有sku列表
      for(SeckillSku sku: seckillSkus){
        log.info("开始将{}号商品的库存保存到Redis",sku.getSkuId());
        // 在编程过程中,涉及RedisKey值时,最好声明常量,如果再业务中使用大量Key值
        // 建议创建一个保存RedisKey值常量的类,SeckillCacheUtils类就是
        // mall:seckill:sku:stock:1
        //      ↑↑↑↑↑
        String skuStockKey=
          SeckillCacheUtils.getStockKey(sku.getSkuId());
        // 检查当前Redis中是否已经包含这个Key
        if(redisTemplate.hasKey(skuStockKey)){
          // 如果Redis已经包含了这个key(可能是前一场秒杀就包含的商品)
          log.info("{}号商品已经在缓存中",sku.getSkuId());
        }else{
          // 不在缓存中的,就要将库存数据保存到Redis中
          // 利用stringRedisTemplate保存,方便减少库存数
          stringRedisTemplate.boundValueOps(skuStockKey)
            .set(sku.getSeckillStock()+"",
                 60*60*4+ran.nextInt(60*30),
                 TimeUnit.SECONDS);
          log.info("成功为{}号商品添加库存",sku.getSkuId());
        }
      }
      // 仍然在遍历所有Spu对象的集合中
      // 将当前Spu包含所有sku保存到Redis之后
      // 我们要为Spu生成随机码
      // 无论任何请求都是访问控制器的路径,秒杀购买商品也是
      // 正常情况下我们输入的路径中,包含要购买商品的id即可
      // 例如 /seckill/spu/,如果这个商品的id已经暴露,
      // 那么就可能有人在秒杀未开始前就访问这个路径
      // 如果不断访问,数据库就需要反复查询这个商品是否在秒杀时间段内,反复查询数据库影响性能
      // 我们要防止这个事情,就要做到秒杀购买商品的路径,平时就不存在
      // 而在秒杀开始5分钟前,生成随机码,有了随机码,购买秒杀商品的路径才出现
      // 我们的随机码生成后也是保存在Redis中
      // 获得随机码key
      String randomCodeKey=SeckillCacheUtils.getRandCodeKey(spu.getSpuId());
      // 判断随机码是否已经生成过
      // 如果没有生成过再生成
      if(!redisTemplate.hasKey(randomCodeKey)){
        // 生成随机数,随机数越大越不好猜
        int randCode=ran.nextInt(900000)+100000;
        redisTemplate.boundValueOps(randomCodeKey)
          .set(randCode,1,TimeUnit.DAYS);
        log.info("spuId为{}的商品随机码为{}",spu.getSpuId(),randCode);
      }
    }
  }
}

配置Quartz触发

上面的类中的代码只是编写了

我们需要在Quartz中触发才能生效

我们创建time.config包

包中创建QuartzConfig类编写Job的触发

@Configuration
@Slf4j
public class QuartzConfig {

  // 声明JobDetail保存到Spring容器
  @Bean
  public JobDetail initJobDetail(){
    log.info("预热任务绑定!");
    return JobBuilder.newJob(SeckillInitialJob.class)
      .withIdentity("initSeckill")
      .storeDurably()
      .build();
  }

  // 定义Quartz的触发,保存到Spring容器
  @Bean
  public Trigger initSeckillTrigger(){
    log.info("预热触发器运行");
    // 学习过程中,每分钟加载一次方便测试,实际开发时,根据业务编写正确Cron表达式即可
    // Cron表达式
    CronScheduleBuilder cronScheduleBuilder=
      CronScheduleBuilder.cronSchedule("0 0/1 * * * ?");
    return TriggerBuilder.newTrigger()
      .forJob(initJobDetail())
      .withIdentity("initSeckillTrigger")
      .withSchedule(cronScheduleBuilder)
      .build();
  }

}

启动Nacos\Redis\Seata

项目启动Leaf\product\seckill

显示秒杀商品详情

上面章节我们完成了缓存预热

下面要根据SpuId查询正在秒杀的商品

和普通的SpuId查询商品详情相比

它的业务判断更复杂

1.布隆过滤器判断(后期完成)

2.判断商品是否在Redis中

3.判断要购买的商品是否在秒杀时间段内

4.如果一切正常在返回详情信息前,要为url属性赋值,其实就是固定路径+随机码

完成根据SpuId查询商品detail详情

在SeckillSpuServiceImpl类中编写新的方法

@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
    return null;
}

// 常量类中每没有定义Detail用的常量Key,我们声明一个
public static final String
        SECKILL_SPU_DETAIL_VO_PREFIX="seckill:spu:detail:vo:";
@Autowired
private RedisTemplate redisTemplate;
// 根据SpuId查询detail详细信息
@Override
public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
  // 将spuId拼接在常量后返回
  String seckillDetailKey=SECKILL_SPU_DETAIL_VO_PREFIX+spuId;
  // 声明返回值类型对象,初值为null即可
  SeckillSpuDetailSimpleVO seckillSpuDetailSimpleVO=null;
  // 判断Redis中是否已经有这个Key
  if(redisTemplate.hasKey(seckillDetailKey)){
    // 如果Redis中已经有这个key,就获得这个key的值赋值给上面声明的实体类
    seckillSpuDetailSimpleVO=(SeckillSpuDetailSimpleVO)redisTemplate
      .boundValueOps(seckillDetailKey).get();
  }else{
    // 如果Redis中没有这个key,我们就需要从数据库中查询后新增到Redis中
    // dubbo调用根据spuId查询detail的方法
    SpuDetailStandardVO spuDetailStandardVO=
      dubboSeckillSpuService.getSpuDetailById(spuId);
    // 判断当前SpuId查询的对象不能为空
    if(spuDetailStandardVO==null){
      // 可以将null保存到Redis中
      // 抛出异常
      throw new CoolSharkServiceException(
        ResponseCode.NOT_FOUND,"您查找的商品不存在");
    }
    // 确认商品存在实例化返回值类型的对象
    seckillSpuDetailSimpleVO=new SeckillSpuDetailSimpleVO();
    // 同名属性赋值
    BeanUtils.copyProperties(spuDetailStandardVO,seckillSpuDetailSimpleVO);
    // 保存到Redis中
    redisTemplate.boundValueOps(seckillDetailKey)
      .set(seckillSpuDetailSimpleVO,
           60*60*24+ RandomUtils.nextInt(60*60*2),
           TimeUnit.SECONDS);
  }
  // 返回查询出的对象
  return seckillSpuDetailSimpleVO;
}

根据SpuId查询Detail详情

上次课完成了查询根据SpuId查询Detail详情的业务逻辑层

下面开发控制层

开发控制层代码

SeckillSpuController添加方法

@GetMapping("/{spuId}/detail")
@ApiOperation("根据SpuId查询Detail详情")
@ApiImplicitParam(value = "spuId", name="spuId",required = true,
        dataType = "long",example = "1")
public JsonResult getSeckillDetail(
        @PathVariable Long spuId){
    SeckillSpuDetailSimpleVO detailSimpleVO=seckillSpuService
            .getSeckillSpuDetail(spuId);
    return JsonResult.ok(detailSimpleVO);
}

启动测试

Nacos\Seata\Redis

Leaf\Product\Seckill\passport

http://localhost:10007/doc.html测试根据spuId查询Detail的功能

根据SpuId查询秒杀商品详情

1.布隆过滤器判断(后期完成)

2.判断商品是否在Redis中

3.判断要购买的商品是否在秒杀时间段内

4.如果一切正常在返回详情信息前,要为url属性赋值,其实就是固定路径+随机码

开发持久层

SeckillSpuMapper添加方法

// 根据SpuId查询秒杀Spu实体
SeckillSpu findSeckillSpuById(Long spuId);

SeckillSpuMapper.xml添加内容


开发业务逻辑层

SeckillSpuServiceImpl业务逻辑层实现类

// 根据SpuId查询Spu详情
@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
    // 先判断参数spuId是否在布隆过滤器中
    // 如果不在直接返回/抛出异常(后期会实现)
	
    // 声明一个返回值用于返回
    SeckillSpuVO seckillSpuVO=null;
    // 获取当前SpuId对应的Redis的Key
    // mall:seckill:spu:vo:1
    String seckillSpuKey= SeckillCacheUtils.getSeckillSpuVOKey(spuId);
    // 执行判断这个Key是否已经保存在Redis中
    if(redisTemplate.hasKey(seckillSpuKey)){
        // 如果在redis中,直接获取
        seckillSpuVO=(SeckillSpuVO)redisTemplate
                        .boundValueOps(seckillSpuKey).get();
    }else{
        // 如果Redis中没有个这个Key
        // 先按spuId查询秒杀spu信息
        SeckillSpu seckillSpu=seckillSpuMapper.findSeckillSpuById(spuId);
        // 当前商品是否存在
        if(seckillSpu==null){
            throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                    "您访问的商品不存在");
        }
        // 上面SeckillSpu对象只有秒杀信息,没有商品信息
        // 商品信息需要根据Dubbo来查询pms数据库
        SpuStandardVO spuStandardVO=dubboSeckillSpuService
                                                .getSpuById(spuId);
        // 将商品详情赋值给seckillSpuVO,先实例化
        seckillSpuVO=new SeckillSpuVO();
        BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
        // 秒杀信息手动赋值
        seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
        seckillSpuVO.setStartTime(seckillSpu.getStartTime());
        seckillSpuVO.setEndTime(seckillSpu.getEndTime());
        // 查询出的对象保存在Redis中
        redisTemplate.boundValueOps(seckillSpuKey)
                .set(seckillSpuVO,60*60*24*1000+
                        RandomUtils.nextInt(1000*60*60*2),
                        TimeUnit.MILLISECONDS);
    }
    // 判断当前Spu是否在秒杀时间段内
    // 获得当前时间
    LocalDateTime nowTime= LocalDateTime.now();
    // 判断当前时间是否在秒杀开始之后
    // between比较两个时间参数
    // 前大后小返回negative
    Duration afterTime=Duration.between(nowTime,seckillSpuVO.getStartTime());
    // 判断当前时间是否在秒杀结束之前
    Duration beforeTime=Duration.between(seckillSpuVO.getEndTime(),nowTime);
    // 判断两个Duration是否同时为"negative",
    // 如果是,表示当前时间确实再当前spu商品的秒杀时间段内
    if(afterTime.isNegative() && beforeTime.isNegative()){
        //从Redis中获得随机码赋值给seckillSpuVO的url属性
        String randCodeKey=SeckillCacheUtils.getRandCodeKey(spuId);
        seckillSpuVO.setUrl("/seckill/"+redisTemplate
                            .boundValueOps(randCodeKey).get());
    }
    // 别忘了返回
    // 前端根据seckillSpuVO对象的url属性是否为null判断是否可以进行秒杀购买操作
    // 如果为空,提示无法购买,如果有值并赋值了随机码,就可以进行下一步提交操作
    return seckillSpuVO;
}

开发控制层

SeckillSpuController添加方法

@GetMapping("/{spuId}")
@ApiOperation("根据SpuId查询秒杀Spu详情")
@ApiImplicitParam(value = "spuId",name="spuId",required = true,
                    dataType = "long",example = "2")
public JsonResult getSeckillSpuVO(
        @PathVariable Long spuId){
    SeckillSpuVO seckillSpuVO=seckillSpuService.getSeckillSpu(spuId);
    return JsonResult.ok(seckillSpuVO);
}

重启Seckill模块

测试10007端口功能

根据SpuId查询秒杀Sku列表

之前编写加载数据的Mapper时,完成了根据SpuId查Sku列表的功能

下面我们从业务逻辑层开始编写

开发业务逻辑层

我们也需要讲SpuId对应的Sku信息保存到Redis

在service.impl包中创建SeckillSkuServiceImpl类中编写代码如下

@Service
@Slf4j
public class SeckillSkuServiceImpl implements ISeckillSkuService {
  @Autowired
  private SeckillSkuMapper skuMapper;
  // Dubbo查询sku详细信息的生产者
  @DubboReference
  private IForSeckillSkuService dubboSkuService;
  // 保存到Redis的支持
  @Autowired
  private RedisTemplate redisTemplate;

  // 根据SpuId查询秒杀Sku列表
  @Override
  public List listSeckillSkus(Long spuId) {
    // 调用根据spuId查询所有Sku列表的方法
    List seckillSkus=skuMapper.findSeckillSkusBySpuId(spuId);
    // 声明包含sku详情和秒杀信息类型泛型的List
    List seckillSkuVOs=new ArrayList<>();
    // 遍历数据库查询出来的所有sku列表
    for(SeckillSku sku:seckillSkus){
      // 声明既包含秒杀信息,又包含详情的sku对象
      SeckillSkuVO seckillSkuVO= null;
      // 获取skuId保存在变量中
      Long skuId= sku.getSkuId();
      // 在检查Redis是否包含对象前,先准备Key
      String seckillSkuVOKey= SeckillCacheUtils.getSeckillSkuVOKey(skuId);
      // 判断Redis是否包含Key
      if(redisTemplate.hasKey(seckillSkuVOKey)){
        seckillSkuVO=(SeckillSkuVO)redisTemplate
          .boundValueOps(seckillSkuVOKey).get();
      }else{
        // Redis中没有这个key,需要我们从数据库查询后,保存在Redis中
        // Dubbo根据skuId查询sku详情信息
        SkuStandardVO skuStandardVO=dubboSkuService.getById(skuId);
        // 将seckillSkuVO实例化后,将详情赋值给它
        seckillSkuVO=new SeckillSkuVO();
        BeanUtils.copyProperties(skuStandardVO,seckillSkuVO);
        // 将秒杀信息也赋值给seckillSkuVO,这样它就包含详情信息和秒杀信息了
        seckillSkuVO.setStock(sku.getSeckillStock());
        seckillSkuVO.setSeckillPrice(sku.getSeckillPrice());
        seckillSkuVO.setSeckillLimit(sku.getSeckillLimit());
        // 将对象保存到Redis中
        redisTemplate.boundValueOps(seckillSkuVOKey).set(
          seckillSkuVO,
          60*60*24*1000+ RandomUtils.nextInt(2*60*60*1000),
          TimeUnit.MILLISECONDS);
      }
      seckillSkuVOs.add(seckillSkuVO);
    }
    // 别忘了返回!!
    return seckillSkuVOs;
  }
}

开发控制层

新建SeckillSkuController添加方法

@RestController
@RequestMapping("/seckill/sku")
@Api(tags = "秒杀sku模块")
public class SeckillSkuController {
  @Autowired
  private ISeckillSkuService seckillSkuService;
  @GetMapping("/list/{spuId}")
  @ApiOperation("根据SpuId查询秒杀Sku列表")
  @ApiImplicitParam(value = "spuId",name = "spuId",required = true,
                    dataType = "long",example = "2")
  public JsonResult> listSeckillSkus(
    @PathVariable Long spuId){
    List list=seckillSkuService.listSeckillSkus(spuId);
    return JsonResult.ok(list);
  }

}

准备流控和降级的处理类

Sentinel是阿里提供的SpringCloud组件,主要用于限制外界访问当前服务器的控制器方法

之前的课程中,我们已经比较详细的学习的Sentinel使用的方式

下面我们要先编写Sentinel限流和服务降级时,运行的自定义异常处理类

我们酷鲨前台项目seckill-webapi模块

创建一个exception包,包中新建SeckillBlockHandler代码如下

// 秒杀执行业务限流异常处理类
@Slf4j
public class SeckillBlockHandler {

  // 声明的限流方法,返回值必须和控制器方法一致
  // 参数是包含控制器方法参数前提下,额外添加BlockException异常参数
  // 这个方法我们定义为静态方法,方便调用者不实例化对象,直接用类名调用
  public static JsonResult seckillBlock(String randCode,
                                         SeckillOrderAddDTO seckillOrderAddDTO, BlockException e){
    log.error("请求被限流");
    return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
                             "服务器忙");
  }
}

再创建降级类SeckillFallBack

// Sentinel秒杀降级方法
@Slf4j
public class SeckillFallBack {
  // 参数和返回值的要求基本和秒杀限流方法一致
  // 只是降级方法是因为控制器发生了异常才会运行的,我们使用Throwable类型类接收
  public static JsonResult seckillFall(String randCode,
                                        SeckillOrderAddDTO seckillOrderAddDTO,Throwable throwable){
    log.error("秒杀业务降级");
    return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
                             throwable.getMessage());
  }
}

提交秒杀订单

开发业务逻辑层

我们之前完成了缓存预热的准备工作

用户也可以在秒杀开始后访问当前的商品信息了

如果用户选择商品规格(sku)提交订单,那么就要按照提交秒杀订单的业务流程处理

秒杀提交订单和普通订单的区别

1.从Redis中判断是否有库存

2.要判断当前用户是否为重复购买

3.秒杀订单转换成普通订单,需要使用dubbo在order模块完成

4.用消息队列(RabbitMQ)的方式将秒杀成功信息保存在success表中

创建一个SeckillServiceImpl业务逻辑层实现类,完成上面的业务

@Service
@Slf4j
public class SeckillServiceImpl implements ISeckillService {

    // 需要普通订单生成的方法,dubbo调用
    @DubboReference
    private IOmsOrderService dubboOrderService;
    // 减少Redis库存,是操作Redis字符串类型的数据
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 秒杀订单提交成功,需要发送到消息队列后续处理
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     *
     * 1.从Redis中判断是否有库存
     * 2.要判断当前用户是否为重复购买
     * 3.秒杀订单转换成普通订单,需要使用dubbo在order模块完成
     * 4.用消息队列(RabbitMQ)的方式将秒杀成功信息保存在success表中
     */

  @Override
  public SeckillCommitVO commitSeckill(SeckillOrderAddDTO seckillOrderAddDTO) {
    // 第一阶段 从Redis中判断是否有库存和检查重复购买
    // 防止超卖:Redis中预热了sku的库存,判断用户请求的skuid是否有库存
    // 如果有可以购买,如果没有阻止购买
    Long skuId=seckillOrderAddDTO.getSeckillOrderItemAddDTO().getSkuId();
    // 获得秒杀用户的Id
    Long userId=getUserId();
    // 获得skuId和userId就能明确知道谁买了什么商品
    // 秒杀业务规定,一个用户只能购买一个sku一次
    // 我们将该用户和sku的购买关系保存到Redis中,以防止重复购买
    // 生成用户和此sku关联的key
    String reSeckillKey= SeckillCacheUtils.
    getReseckillCheckKey(skuId,userId);
    // mall:seckill:reseckill:2:1
    // 向这个key中保存一个数据(一般保存一个1表示购买了一次),可以使用自增的方式
    // increment()我们调用这个方法实现两个功能
    // 1.如果key为空,会自动将这个key保存值为1
    // 2.如果key不为空,会自增当前的值 1->2   2->3   3->4
    // 最后将这个数值返回给seckillTimes
    Long seckillTimes=stringRedisTemplate
      .boundValueOps(reSeckillKey).increment();
    if(seckillTimes>1){
      // 购买次数超过1,证明不是第一次购买,抛出异常,终止业务
      throw new CoolSharkServiceException(
        ResponseCode.FORBIDDEN,"您已经购买过该商品");
    }
    // 运行到此处证明用户符合购买资格(之前没买过)
    // 下面检查是否有库存,先确定库存的Key
    String seckillSkuCountKey=SeckillCacheUtils.getStockKey(skuId);
    // 从Redis中获得库存
    // 调用decrement()方法,将库存数减1,返回的数值,就是减1后的库存数
    Long leftStock=stringRedisTemplate
      .boundValueOps(seckillSkuCountKey).decrement(1);
    // leftStock表示当前用户购买完之后剩余的库存数
    // 如果是0表示次用户购买完库存为0,所以只有返回值小于0时才是没有库存了
    if(leftStock<0){
      throw new CoolSharkServiceException(ResponseCode.NOT_ACCEPTABLE,
                                          "对不起您购买的商品已经无货");
    }
    // 运行到此处,表示用户可以生成订单,进入第二阶段
    // 第二阶段 开始生成订单秒杀订单转换成普通订单
    // 我们现获得的秒杀订单参数seckillOrderAddDTO
    // 这个参数信息也是前端收集并发送到后端的,它的信息量和普通订单发送的内容基本相同
    // 我们可以直接dubbo调用order模块新订单的业务来完成
    // 通过调用转换方法将seckillOrderAddDTO转换为OrderAddDTO类型对象
    OrderAddDTO orderAddDTO=convertSeckillOrderToOrder(seckillOrderAddDTO);
    // 转换过程中没有UserId需要手动赋值
    orderAddDTO.setUserId(userId);
    // 信息全了,发送Dubbo请求调用Order模块新增订单
    OrderAddVO orderAddVO=dubboOrderService.addOrder(orderAddDTO);
    // 订单生成完毕下面进入第三阶段
    // 第三阶段 消息队列(RabbitMQ)发送消息
    // 我们的目的是将购买成功的信息保存到success表中
    // 但是这个业务并不需要立即完成,可以将它发送给消息队列,
    // 异步完成,减轻当前业务的运行压力
    Success success=new Success();
    // Success主要保存秒杀成功的信息,或者描述为秒杀卖出的商品
    // 具体到商品是Sku信息,所以它的属性和SeckillOrderItemAddDTO更相似
    // 所以用SeckillOrderItemAddDTO给Success同名属性赋值
    BeanUtils.copyProperties(
      seckillOrderAddDTO.getSeckillOrderItemAddDTO(),success);
    // 缺少的其他属性后面赋值
    success.setUserId(userId);
    success.setOrderSn(orderAddVO.getSn());
    // Success信息收集完成,将消息发送给RabbitMQ
    rabbitTemplate.convertAndSend(
      RabbitMqComponentConfiguration.SECKILL_EX,
      RabbitMqComponentConfiguration.SECKILL_RK,
      success);
    // 消息队列会处理后续的操作
    // 最终返回值SeckillCommitVO和OrderAddVO属性一致,实例化后赋值返回即可
    SeckillCommitVO seckillCommitVO=new SeckillCommitVO();
    BeanUtils.copyProperties(orderAddVO,seckillCommitVO);
    return seckillCommitVO;
  }

  // 将秒杀订单转换为普通订单的方法
  private OrderAddDTO convertSeckillOrderToOrder(SeckillOrderAddDTO seckillOrderAddDTO) {
    // 实例化OrderAddDTO
    OrderAddDTO orderAddDTO=new OrderAddDTO();
    // 先将属性一致的值赋值到orderAddDTO
    BeanUtils.copyProperties(seckillOrderAddDTO,orderAddDTO);
    // SeckillOrderAddDTO秒杀订单中只可能对应一个OrderItem对象
    // 但是普通订单OrderAddDTO对象中可能包含多个OrderItem对象
    // 所以OrderAddDTO对象中的OrderItem是一个List,而秒杀订单是单个对象
    // 我们需要讲秒杀订单的单个对象添加到OrderAddDTO对象中OrderItem的集合里
    // 实例化OrderItemAddDTO
    OrderItemAddDTO orderItemAddDTO=new OrderItemAddDTO();
    // 将SeckillOrderItemAddDTO同名属性赋值给orderItemAddDTO
    BeanUtils.copyProperties(
      seckillOrderAddDTO.getSeckillOrderItemAddDTO(),orderItemAddDTO);
    // 要想将赋好值的orderItemAddDTO对象添加到orderAddDTO的集合中
    // 需要先实例化这个类型的集合
    List orderItemAddDTOs=new ArrayList<>();
    // 将orderItemAddDTO添加到集合中
    orderItemAddDTOs.add(orderItemAddDTO);
    // 再将集合赋值到orderAddDTO对象中
    orderAddDTO.setOrderItems(orderItemAddDTOs);
    // 最终返回转换完成的orderAddDTO对象
    return orderAddDTO;
  }

  public CsmallAuthenticationInfo getUserInfo(){
    // 获得SpringSecurity容器对象
    UsernamePasswordAuthenticationToken authenticationToken=
      (UsernamePasswordAuthenticationToken) SecurityContextHolder.
    getContext().getAuthentication();
    // 判断获取的容器信息是否为空
    if(authenticationToken!=null){
      // 如果容器中有内容,证明当前容器中有登录用户信息
      // 我们获取这个用户信息并返回
      CsmallAuthenticationInfo csmallAuthenticationInfo=
        (CsmallAuthenticationInfo)authenticationToken.getCredentials();
      return csmallAuthenticationInfo;
    }
    throw new CoolSharkServiceException(ResponseCode.UNAUTHORIZED,"没有登录信息");
  }
  // 业务逻辑层中大多数方法都是获得用户id,所以编写一个返回用户id的方法
  public Long getUserId(){
    return getUserInfo().getId();
  }
}
秒杀业务Demo

redis储存

开发控制层

随机码判断逻辑

秒杀业务Demo

随机码

controller包下创建SeckillController

代码如下

@RestController
@RequestMapping("/seckill")
@Api(tags = "提交秒杀模块")
public class SeckillController {

    @Autowired
    private ISeckillService seckillService;
    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/{randCode}")
    @ApiOperation("验证随机码并提交秒杀订单")
    @ApiImplicitParam(value = "随机码",name = "randCode",required = true,
            dataType = "string")
    @PreAuthorize("hasRole('ROLE_user')")
    // Sentinel限流和降级的配置
  @SentinelResource(value = "seckill",
       blockHandlerClass = SeckillBlockHandler.class,blockHandler = "seckillBlock",
       fallbackClass = SeckillFallBack.class,fallback = "seckillFall")
  public JsonResult commitSeckill(
    @PathVariable String randCode, SeckillOrderAddDTO seckillOrderAddDTO){
    // 先获取spuId
    Long spuId=seckillOrderAddDTO.getSpuId();
    // 确定spuId对应随机码的key
    String randCodeKey= SeckillCacheUtils.getRandCodeKey(spuId);
    // 判断Redis中是否有randCodeKey
    if(redisTemplate.hasKey(randCodeKey)){
      // 如果有这个Key 判断随机码是否正确
      // 获取随机码
      String redisRandCode=redisTemplate
        .boundValueOps(randCodeKey).get()+"";
      //判断redisRandCode是否为null
      if (redisRandCode==null){
        // Redis中随机码丢失,服务器内部错误
        throw new CoolSharkServiceException(
          ResponseCode.INTERNAL_SERVER_ERROR,
          "服务器内部异常,请联系管理员");
      }
      // 判断Redis的随机码和参数随机码是否一致,防止投机购买
      if(!redisRandCode.equals(randCode)){
        // 如果不一致,认为是投机购买,抛出异常
        throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                                            "没有这个商品");
      }
      // 调用业务逻辑层提交订单
      SeckillCommitVO seckillCommitVO=
        seckillService.commitSeckill(seckillOrderAddDTO);
      return JsonResult.ok(seckillCommitVO);
    }else{
      // 没有Key对应随机码,秒杀列表中没有这个商品
      throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                                          "没有指定商品");
    }
  }

}

为了knife4j测试顺利,我们在SeckillInitialJob类的最后位置输出一下spuId为2的随机码用于测试

修改后代码如下

if(!redisTemplate.hasKey(randomCodeKey)){
  // 生成随机数,随机数越大越不好猜
  int randCode=ran.nextInt(900000)+100000;
  redisTemplate.boundValueOps(randomCodeKey)
    .set(randCode,1,TimeUnit.DAYS);
  log.info("spuId为{}的商品随机码为{}",spu.getSpuId(),randCode);
}else{
  // 输出spuId对应的随机码用于测试
  String code=redisTemplate.boundValueOps(randomCodeKey).get()+"";
  log.info("spuId为{}的商品随机码为{}",spu.getSpuId(),code);
}

启动Nacos\Seata\Redis\Sentinel\RabbitMQ

项目 Leaf\product\passport\order\seckill

注意yml配置文件中的RabbitMQ的用户名和密码

如果说已经购买过,就修改允许购买的数量 >1 >100

如果说没有库存,可以吧判断库存的if注释掉

测试成功即可

还可以测试sentinel的限流

success成功信息的处理

我们再上次课提交秒杀信息业务最后

向RabbitMQ队列中,输出了添加秒杀成功信息的消息

但是我们没有任何处理

将秒杀成功信息发送到消息队列的原因:

秒杀成功信息用于统计秒杀数据,是秒杀结束后才需要统计的

所以在秒杀并发高时,消息队列的发送可以延缓,在服务器不忙时,再运行(削峰填谷)

开发持久层

秒杀数据库中有success表

其中的信息就是保存秒杀成功的数据(userId,skuId等)

我们要连接数据库,对这个表进行新增

还有对秒杀数据库sku库存的修改

SeckillSkuMapper接口中添加方法来修改指定skuId的库存数

// 根据skuId修改库存数
void updateReduceStockBySkuId(@Param("skuId") Long skuId,
       @Param("quantity") Integer quantity);

SeckillSkuMapper.xml



  update
  	seckill_sku
  set
  	seckill_stock=seckill_stock - #{quantity}
  where
  	sku_id=#{skuId}

下面编写新增Success的方法

@Repository
public interface SuccessMapper {
  // 新增Success对象到数据库的方法
  void saveSuccess(Success success);  
}

SuccessMapper.xml


  insert into
  success(
    user_id,
    user_phone,
    sku_id,
    title,
    main_picture,
    seckill_price,
    quantity,
    bar_code,
    data,
    order_sn
  ) values(
    #{userId},
    #{userPhone},
    #{skuId},
    #{title},
    #{mainPicture},
    #{seckillPrice},
    #{quantity},
    #{barCode},
    #{data},
    #{orderSn}
	)

开发消息的接收

我们当前触发新增Success的方法并不是常规的业务逻辑层

而是由RabbitMQ消息收发机制中接收消息的对象来调用

所有我们编写一个接收消息的监听器类来完成这个操作

创建consumer包,包中创建类SekillQueueConsumer代码如下

// 必须交由Spring容器管理
@Component
// RabbitMQ监听器声明
@RabbitListener(queues = {RabbitMqComponentConfiguration.SECKILL_QUEUE})
public class SeckillQueueConsumer {

  // 将业务需要的对象都装配过来
  @Autowired
  private SuccessMapper successMapper;
  @Autowired
  private SeckillSkuMapper skuMapper;
  // 编写监听队列调用的方法
  // 保证方法的参数和发送时参数类型一致
  @RabbitHandler
  public void process(Success success){
    // 扣库存
    // 扣库存操作不会在并发高时和数据库同步,只会在服务器较闲时,影响数据库
    // 真正的实时库存保存在Redis中
    skuMapper.updateReduceStockBySkuId(success.getSkuId(),
                                       success.getQuantity());
    // 新增success
    successMapper.saveSuccess(success);
    // 如果上面方法有失败情况
    // 需要在下面发送异常消息,可能发送给秒杀模块处理
    // 也可以直接发送给死信队列,让人工处理
  }

}

环境方面

Nacos\Sentinel\Seata\redis\RabbitMQ

服务方面

Leaf\product\order\passport\seckill

学习记录,如有侵权请联系删除

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章