微服务之秒杀

开发根据SpuId查询秒杀Sku列表信息

我们将秒杀的商品Spu列表查询出来, 当用户选择一个商品时, 我们要将这个商品的sku也查询出来

也就是根据SpuId查询Sku的列表, 创建SeckillSkuMapper

@Repository
public interface SeckillSkuMapper {
    // 根据spuId查询sku列表
    List<SeckillSku> findSeckillSkusBySpuId(Long spuId);

}
<sql id="SimpleField">
    <if test="true">
        id,
        sku_id,
        spu_id,
        seckill_stock,
        seckill_price,
        gmt_create,
        gmt_modified,
        seckill_limit
    </if>
</sql>
<select id="findSeckillSkusBySpuId" resultMap="BaseResultMap">
    select
        <include refid="SimpleField" />
    from
        seckill_sku
    where
        spu_id=#{spuId}
</select>

根据当前时间查询正在进行秒杀的商品

根据给定时间查询出正在进行秒杀的商品列表, 在秒杀过程中,一定会将当前时间正在进行秒杀商品查询出来的, 首先保证数据库中的seckill_spu表的数据正在秒杀时间段(检查数据, 如果不在秒杀时间段,将结束时间后移如2024年)

SeckillSpuMapper接口添加方法

// 根据指定时间,查询正在进行秒杀的商品
List<SeckillSpu> findSeckillSpusByTime(LocalDateTime time);
<!--  根据指定时间,查询正在进行秒杀的商品  -->
<select id="findSeckillSpusByTime" resultMap="BaseResultMap">
    select
        <include refid="SimpleField" />
    from
        seckill_spu
    where
        start_time &lt; #{time}
    and
        end_time &gt; #{time}
</select>

根据SpuId查询秒杀商品详情

SeckillSpuMapper接口添加方法  

// 根据SpuId查询spu秒杀信息
SeckillSpu findSeckillSpuById(Long spuId);
<!--  根据SpuId查询spu秒杀信息  -->
<select id="findSeckillSpuById" resultMap="BaseResultMap">
    select
        <include refid="SimpleField" />
    from
        seckill_spu
    where
        spu_id=#{spuId}
</select>

查询所有秒杀商品的SpuId

这个查询需要是为了后面布隆过滤器加载数据库中包含的所有SpuId时使用

因为布隆过滤器的特性,只需要查询出所有商品的spu_id即可

SeckillSpuMapper接口添加方法

// 布隆过滤器用:查询获得所有秒杀商品的SpuId数组
Long[] findAllSeckillSpuIds();
<!--  布隆过滤器用:查询获得所有秒杀商品的SpuId数组  -->
<select id="findAllSeckillSpuIds" resultType="long" >
    select spu_id from seckill_spu
</select>

缓存预热思路

 即将发生高并发业务之前,我们将一些高并发业务中需要的数据保存到Redis中,这种操作,就是"缓存预热",这样发生高并发时,这些数据就可以直接从Redis中获得,无需查询数据库了

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

例如每天的12:00 14:00 16:00 18:00进行秒杀, 那么就在 11:55 13:55 15:55 17:55 进行预热

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

    还要注意为了预防雪崩,在向Redis保存数据时,都应该添加随机数

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

  3. 在秒杀开始之前,生成每个商品对应的随机码,保存在Redis中,随机码可以绑定给有Spu,防止有用户知道SpuId暴力访问

设置定时任务

将库存和随机码保存到Redis

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

1.创建Job接口实现类

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

在seckill包下创建timer.job包

在seckill包下创建timer.config包

首先我们编写缓存预热的操作,在job包下创建类SeckillInitialJob

@Slf4j
public class SeckillInitialJob implements Job {

    // 查询秒杀sku库存数的mapper
    @Autowired
    private SeckillSkuMapper skuMapper;
    // 需要查询秒杀spu相关的信息
    @Autowired
    private SeckillSpuMapper spuMapper;
    // 操作Redis的对象
    @Autowired
    private RedisTemplate redisTemplate;
   
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 当前方法是缓存预热操作
        // 它执行的时间是秒杀开始前5分钟,所以获取一个5分钟后的时间对象
        LocalDateTime time=LocalDateTime.now().plusMinutes(5);
        // 查询这个时间所有进行秒杀的商品
        List<SeckillSpu> seckillSpus=spuMapper.findSeckillSpusByTime(time);
        // 遍历当前批次所有秒杀的spu
        for(SeckillSpu spu: seckillSpus){
            // spu是商品的品类,必须确定规格也就是确定sku后,才能明确库存
            // 所以我们要根据spuId查询sku,然后将sku的库存数保存到Redis
            List<SeckillSku> seckillSkus=skuMapper.
                    findSeckillSkusBySpuId(spu.getSpuId());
            // 遍历获得了当前spu对应的sku列表,还要遍历sku列表才能保存库存数
            for(SeckillSku sku: seckillSkus){
                log.info("开始将{}号sku商品库存预热到Redis",sku.getSkuId());
                // 要在操作Redis之前,获得key常量
                // SeckillCacheUtils.getStockKey是获得事先准备好的字符串常量的方法
                // 它的实际的值为  mall:seckill:sku:stock:1
                String skuStockKey=SeckillCacheUtils.getStockKey(sku.getSkuId());
                // 检查Redis中是否已经包含了这个key
                if(redisTemplate.hasKey(skuStockKey)){
                    // 如果key已经存在,证明之前已经缓存过了,直接跳过
                    log.info("{}号sku商品已经缓存过了",sku.getSkuId());
                }else{
                    // 如果key不存在,就要将我们sku对象的库存数保存到Redis
                    // 使用stringRedisTemplate保存库存数到Redis
                    stringRedisTemplate.boundValueOps(skuStockKey)
                            .set(sku.getSeckillStock()+"",
                                   10*60*1000+ RandomUtils.nextInt(10000),
                                    TimeUnit.MILLISECONDS);
                    log.info("成功为{}号sku商品预热缓存",sku.getSkuId());
                }
            }
            // 上面为止完成了库存数的预热
            // 下面开始预热每个spu商品对应的随机码
            // 随机码就是一个随机数,范围可以自己定
            // 随机码终会在生成订单前进行验证,以减轻服务器压力
            // 我们的操作就是生成这个随机数,保存到redis中
            // mall:seckill:spu:url:rand:code:2
            String randCodeKey=SeckillCacheUtils.getRandCodeKey(spu.getSpuId());
            // 判断随机码是否已经在redis中
            if(redisTemplate.hasKey(randCodeKey)){
                int randCode=(int)redisTemplate.boundValueOps(randCodeKey).get();
                log.info("{}号spu商品的随机码已经缓存:{}",spu.getSpuId(),randCode);
            }else{
                // 生成随机码  范围:100000~999999
                int randCode=RandomUtils.nextInt(900000)+100000;
                redisTemplate.boundValueOps(randCodeKey)
                        .set(randCode,10*60*1000+RandomUtils.nextInt(10000),
                        TimeUnit.MILLISECONDS);
                log.info("spuId为{}号的商品生成的随机码为:{}",spu.getSpuId(),randCode);
            }

        }
    }
}

RedisTemplate对象在保存数据到Redis时,会先将当前数据序列化后保存,  这样做的好处是序列化后的数据保存到Redis读写效率更高, 缺点是不能在Redis中修改数据,  我们现在要预热的是秒杀sku库存数, 这个库存数如果也用上面的redisTemplate保存的方式,  就容易在高并发情况下由于线程安全问题导致"超卖",  解决方案就是我们需要创建一个能够直接在Redis中修改数据的对象,  来避免超卖的发生, SpringDataRedis提供了一个可以直接在Redis中操作数值的对象象:StringRedisTemplate, 使用StringRedisTemplate向Redis保存数据,默认都会以字符串的方式保存(不序列化了), 又因为Redis支持直接将字符串类型的数值进行修改,所以可以将库存数保存并直接修改, 这样就不需要写java代码读取和修改了,直接使用Redis内部的功能修改库存数最后结合Redis天生单线程的特性,避免线程安全问题方式超卖

配置Quartz触发

@Configuration
public class QuartzConfig {

    // 向Spring容器中保存JobDetail对象
    @Bean
    public JobDetail initJobDetail(){
        return JobBuilder.newJob(SeckillInitialJob.class)
                .withIdentity("initSeckill")
                .storeDurably()
                .build();
    }
    // 向Spring容器中保存Trigger对象
    @Bean
    public Trigger initSeckillTrigger(){
        // 实际开发要写出正确的Cron表达式,让程序在11:55  13:55....运行
        // 0 55 11,13,15,17 * * ?
        // 但是学习过程中我们不可能去等这个时间,所有为了测试方便,我们设置它每分钟都运行
        CronScheduleBuilder cron=
                CronScheduleBuilder.cronSchedule("0 0/1 * * * ?");
        return TriggerBuilder.newTrigger()
                .forJob(initJobDetail())
                .withIdentity("initTrigger")
                .withSchedule(cron)
                .build();
    }

}

开发查询秒杀商品详情的功能

完成了缓存预热, 下面要根据SpuId查询正在秒杀的商品, 和普通的SpuId查询商品详情相比, 它的业务判断更复杂

1.页面上显示秒杀价和剩余秒杀时间等信息

2.判断请求的spuId是否在布隆过滤器中(后续完成)

3.判断Redis 中是否包含商品信息

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

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

在业务逻辑层SeckillSpuServiceImpl类中编写新的方法

// 秒杀所有信息都有保存到Redis中
@Autowired
private RedisTemplate redisTemplate;
// 没有定义SpuDetail对应的常量,所以这个要自己定义
// PREFIX是"前缀"的意思
public static final String
        SECKILL_SPU_DETAIL_VO_PREFIX="seckill:spu:detail:vo:";

@Override
public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
    // 获得常量Key
    String seckillSpuDetailKey=SECKILL_SPU_DETAIL_VO_PREFIX+spuId;
    // 先声明一个当前方法的返回值类型的对象,设值为null,以备后续使用
    SeckillSpuDetailSimpleVO simpleVO=null;
    // 判断redis中是否已经包含这个key
    if(redisTemplate.hasKey(seckillSpuDetailKey)){
        // 如果已经保存在Redis中,直接赋值
        simpleVO=(SeckillSpuDetailSimpleVO)redisTemplate
                .boundValueOps(seckillSpuDetailKey).get();
    }else{
        // 如果Redis中没有这个对象,就需要按spuId到数据库中查询
        // 使用Dubbo调用product模块的方法查询
        SpuDetailStandardVO spuDetailStandardVO=
            dubboSeckillSpuService.getSpuDetailById(spuId);
        // 实例化SeckillSpuDetailSimpleVO对象
        simpleVO=new SeckillSpuDetailSimpleVO();
        BeanUtils.copyProperties(spuDetailStandardVO,simpleVO);
        // simpleVO赋值完成后,将它保存到Redis中,以便后续获取
        redisTemplate.boundValueOps(seckillSpuDetailKey)
                .set(simpleVO,10*60*1000+ RandomUtils.nextInt(10000),
                        TimeUnit.MILLISECONDS);
    }
    // 最后别忘了写返回值!!!!
    return simpleVO;
}

控制层 

// localhost:10007/seckill/spu/2/detail
@GetMapping("/{spuId}/detail")
@ApiOperation("根据spuId查询detail详情")
@ApiImplicitParam(value = "spuId",name="spuId",example = "2")
public JsonResult<SeckillSpuDetailSimpleVO> getSeckillDetail(
        @PathVariable Long spuId){
    SeckillSpuDetailSimpleVO simpleVO=
            seckillSpuService.getSeckillSpuDetail(spuId);
    return JsonResult.ok(simpleVO);
}

据SpuId查询秒杀商品详情

SeckillSpuServiceImpl业务逻辑层实现类

// 根据SpuId查询spu详情(返回值包含秒杀信息和常规信息)
@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
    // 最终完整版的代码,这里要先获取布隆过滤器
    // 判断参数spuId是否在布隆过滤器中,如果不在直接抛出异常

    // 获取要使用的spu对应的常量
    // mall:seckill:spu:vo:2
    String seckillSpuKey= SeckillCacheUtils.getSeckillSpuVOKey(spuId);
    // 声明要返回的对象类型
    SeckillSpuVO seckillSpuVO=null;
    // 判断Redis中是否包含这个Key
    if(redisTemplate.hasKey(seckillSpuKey)){
        // 直接从Redis中获取即可
        seckillSpuVO=(SeckillSpuVO)redisTemplate
                .boundValueOps(seckillSpuKey).get();
    }else{
        // 如果Redis中没有这个Key
        // 要从数据库中查询秒杀spu信息和常规spu信息赋值到seckillSpuVO对象中
        // 先查询秒杀信息:
        SeckillSpu seckillSpu= seckillSpuMapper.findSeckillSpuById(spuId);
        // 判断seckillSpu是否为空(因为布隆过滤器可能产生误判)
        if(seckillSpu==null){
            throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                    "您访问的商品不存在");
        }
        // 到此为止,我们已经查询出了spu商品的秒杀信息,下面要查询常规信息
        // dubbo调用product模块的方法获得spu常规信息
        SpuStandardVO spuStandardVO= dubboSeckillSpuService
                                                .getSpuById(spuId);
        seckillSpuVO=new SeckillSpuVO();
        // 将常规spu信息对象的同名属性赋值给seckillSpuVO
        BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
        // 最后将秒杀信息赋值到seckillSpuVO
        seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
        seckillSpuVO.setStartTime(seckillSpu.getStartTime());
        seckillSpuVO.setEndTime(seckillSpu.getEndTime());
        // seckillSpuVO 就完成了常规spu信息和秒杀spu信息的赋值过程
        // 将这个对象保存到Redis中
        redisTemplate.boundValueOps(seckillSpuKey).set(
                seckillSpuVO,10*60*1000+RandomUtils.nextInt(10000),
                TimeUnit.MILLISECONDS);
    }
    // 返回前最后的步骤是给seckillSpuVO的url属性赋值
    // 一旦给url属性赋值,就意味着当前用户可以提交购买订单了
    // 必须判断当前时间是否在秒杀时间段内,才能决定是否给url赋值
    LocalDateTime nowTime=LocalDateTime.now();
    // 当前是高并发状态,不要连接数据库去判断时间
    // 我们使用seckillSpuVO中的开始时间和结束时间属性去判断
    // 判断的基本原则是开始时间小于当前时间小于结束时间
    // 本次我们使用"时间差"对象Duration来判断时间关系
    // 我们会利用Duration提供的between方法来获得两个时间的时间差
    // 这个方法有个特征,如果时间差是负数,会返回一个negative的状态
    // 判断当前时间大于开始时间
    //                             2022-10-12 16:45   , 2022-10-12 16:40
    Duration afterTime=Duration.between(nowTime,seckillSpuVO.getStartTime());
    // 判断结束时间大于当前时间
    Duration beforeTime=Duration.between(seckillSpuVO.getEndTime(),nowTime);
    // 简单来说, between方法中两个时间参数,前面的大后面的小就会返回negative
    // 上面两个变量如果返回值都是negative,
    // 就证明当前时间大于开始时间,结束时间大于当前时间
    if(afterTime.isNegative()  &&  beforeTime.isNegative()){
        // 进入if表示时间是正确的,要授权url,允许秒杀购买
        // 根据spuId获得redis事先预热好的随机码
        String randCodeKey=SeckillCacheUtils.getRandCodeKey(spuId);
        // 从redis中获取随机码
        String randCode=redisTemplate.boundValueOps(randCodeKey).get()+"";
        // 将随机码赋值到url
        seckillSpuVO.setUrl("/seckill/"+randCode);
        log.info("url赋值随机码为:{}",randCode);
    }
    // 最后别忘了返回
    // 最后返回的seckillSpuVO实际上是 秒杀spu信息+常规spu信息+url
    return seckillSpuVO;
}

开发控制层SeckillSpuController  

//  localhost:10007/seckill/spu/2
@GetMapping("/{spuId}")
@ApiOperation("根据spuId查询spu相关信息")
@ApiImplicitParam(value = "spuId",name="spuId",example = "2")
public JsonResult<SeckillSpuVO> getSeckillSpuVO(
        @PathVariable Long spuId){
    SeckillSpuVO seckillSpuVO=seckillSpuService.getSeckillSpu(spuId);
    return JsonResult.ok(seckillSpuVO);
}

根据SpuId查询秒杀Sku列表

我们也需要将SpuId对应的Sku信息保存到Redis, 在service.impl包中创建SeckillSkuServiceImpl类中编写代码如下

@Service
@Slf4j
public class SeckillSkuServiceImpl implements ISeckillSkuService {
    @Autowired
    private SeckillSkuMapper skuMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    // Dubbo调用product模块查询sku常规信息
    @DubboReference
    private IForSeckillSkuService dubboSkuService;

    // 根据spuId查询sku列表
    @Override
    public List<SeckillSkuVO> listSeckillSkus(Long spuId) {
        //SeckillSkuVO作为返回值集合的泛型,其中也是既包含秒杀sku信息,又包含常规sku信息
        // 我们最终也要将两个部分都赋值之后返回
        List<SeckillSkuVO> seckillSkuVOs=new ArrayList<>();
        // 根据spuId查询sku列表
        List<SeckillSku> seckillSkus=skuMapper.findSeckillSkusBySpuId(spuId);
        // 遍历所有sku
        for(SeckillSku sku : seckillSkus){
            // 获取当前sku的id以备后续使用
            Long skuId=sku.getSkuId();
            // 声明sku对象的Key
            // mall:seckill:sku:vo:1
            String seckillSkuVOKey= SeckillCacheUtils.getSeckillSkuVOKey(skuId);
            // 声明返回值类型对象,先赋null
            SeckillSkuVO seckillSkuVO=null;
            // 判断Redis中是否已经包含这个Key
            if(redisTemplate.hasKey(seckillSkuVOKey)){
                seckillSkuVO=(SeckillSkuVO) redisTemplate
                        .boundValueOps(seckillSkuVOKey).get();
            }else{
                // Redis没有这个sku就需要到数据库中去查询
                // 先查询常规信息
                SkuStandardVO skuStandardVO=dubboSkuService.getById(skuId);
                // 当前循环正在遍历的对象sku就是秒杀信息
                // 两个方面信息都有了,就实例化返回值seckillSkuVO赋值
                seckillSkuVO=new SeckillSkuVO();
                // 常规同名属性赋值
                BeanUtils.copyProperties(skuStandardVO,seckillSkuVO);
                // 秒杀信息手动赋值
                seckillSkuVO.setSeckillPrice(sku.getSeckillPrice());
                seckillSkuVO.setStock(sku.getSeckillStock());
                seckillSkuVO.setSeckillLimit(sku.getSeckillLimit());
                // 将sku保存到Redis中
                redisTemplate.boundValueOps(seckillSkuVOKey)
                        .set(seckillSkuVO,10*60*1000+ RandomUtils.nextInt(10000),
                                TimeUnit.MILLISECONDS);
            }
            // 将赋好值的seckillSkuVO对象添加到返回值集合中
            // seckillSkuVO可能是Redis直接获取的,也可能是数据库查出来的
            seckillSkuVOs.add(seckillSkuVO);
        }
        // 最后别忘了返回集合!!!
        return seckillSkuVOs;
    }
}

控制层 

@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",example = "2")
    public JsonResult<List<SeckillSkuVO>> listSeckillSkus(
            @PathVariable Long spuId){
        List<SeckillSkuVO> list=seckillSkuService.listSeckillSkus(spuId);
        return JsonResult.ok(list);
    }
}

创建流控和降级的处理类

秒杀业务肯定是一个高并发的处理,并发数超过程序设计的限制时,就需要对请求的数量进行限流Sentinel是阿里提供的SpringCloud组件,主要用于外界访问当前服务器的控制器方法的限流操作

编写限流异常处理类, 创建一个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  

// 秒杀业务的降级处理类
@Slf4j
public class SeckillFallback {

    // 降级方法参数可以和控制器完全一致,也可以添加一个Throwable类型的参数
    // 如果想知道是什么异常导致了服务降级,最后声明一下这个参数,输出它的信息
    public static JsonResult seckillFallback(String randCode,
                                             SeckillOrderAddDTO seckillOrderAddDTO,
                                             Throwable throwable){
        log.error("一个请求被降级了");
        throwable.printStackTrace();
        return JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR,
                "发生异常!异常信息为:"+throwable.getMessage());

    }

开发执行秒杀的业务逻辑层

预热中完成了秒杀商品sku库存数,spu随机码(布隆过滤器)保存在redis中的操作, 也完成了查询秒杀商品列表,和显示秒杀商品详情的方法, 下面要开始进行秒杀商品生成订单的操作, 如果用户选择商品规格(sku)提交订单,那么就要按照提交秒杀订单的业务流程处理, 秒杀提交订单和普通订单的区别

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

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

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

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

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

@Service
@Slf4j
public class SeckillServiceImpl implements ISeckillService {

    // 秒杀业务中,使用redis都是在判断数值,直接使用字符串类型的Redis对象即可
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 需要Dubbo调用mall_order模块生成普通订单的业务逻辑方法
    @DubboReference
    private IOmsOrderService dubboOrderService;
    // 我们的业务中要将秒杀成功的信息发送给消息队列,所以准备发送消息的对象
    @Autowired
    private RabbitTemplate rabbitTemplate;

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

    @Override
    public SeckillCommitVO commitSeckill(SeckillOrderAddDTO seckillOrderAddDTO) {
        // 第一阶段:利用redis检查重复购买和库存数
        // 从方法参数中获得要购买的商品skuId
        Long skuId=seckillOrderAddDTO.getSeckillOrderItemAddDTO().getSkuId();
        // 从SpringSecurity中获得用户id
        Long userId=getUserId();
        // 这样就明确了是哪个userId要购买哪个skuId
        // 秒杀业务规定,每个用户只能购买每个skuId一次
        // 所以我们可以根据当前userId和skuId进行重复购买的检查
        // 先获得检查重复购买的Key
        // mall:seckill:reseckill:[skuId]:[userId]
        String reSeckillCheckKey= SeckillCacheUtils
                        .getReseckillCheckKey(skuId,userId);
        // 使用上面生成的key来调用redis的increment()方法
        // increment是增长的意思
        // 1.如果当前key不存在,redis会创建这个key,并保存它的值为1
        // 2.如果当前key存在,redis会在当前值的基础上加1 ,例如现在值为1,运行后会变为2
        // 3.运行完上面的操作,会将最终的值返回
        // 也就是说如果返回的值为1,表示这个用户没有成功购买过这个商品
        Long seckillTimes=
             stringRedisTemplate.boundValueOps(reSeckillCheckKey).increment();
        // 如果这个秒杀次数值大于1,表示这个用户已经购买过这个商品
        if(seckillTimes>1){
            // 不允许重复购买,抛出异常终止程序
            throw new CoolSharkServiceException(ResponseCode.FORBIDDEN,
                    "您已经购买过该商品了,本商城秒杀禁止重复购买");
        }
        // 程序运行到此处,表示用户没有购买过这个商品
        // 开始检查库存
        // 获得指定skuId库存数的key
        // mall:seckill:sku:stock:1
        String seckillSkuCountKey=SeckillCacheUtils.getStockKey(skuId);
        // 根据当前key获取redis中保存的sku库存数
        // 使用decrement()方法,将当前库存数-1之后返回
        Long leftStock=stringRedisTemplate
                .boundValueOps(seckillSkuCountKey).decrement();
        // leftStock是用户购买后剩余库存数
        // 如果leftStock是0,表示当前用户购买了库存中的最后一件
        // 只有leftStock小于0时才表示售罄了
        if(leftStock<0){
            // 如果已经没有库存,就要终止当前用户本次购买
            // 将当前用户购买此商品的次数修改为0
            stringRedisTemplate.boundValueOps(reSeckillCheckKey).decrement();
            // 抛出异常
            throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,
                                "对不起您所要购买的商品已经售罄");
        }
        // 到此为止,当前用户经过了重复购买和库存检查的判断,可以开始生成订单了!
        // 第二阶段:将秒杀订单转换为普通订单
        // SeckillOrderAddDTO转换成OrderAddDTO,以实现Dubbo调用
        // 自定义一个转换方法,参数是秒杀订单,返回值是普通订单
        OrderAddDTO orderAddDTO=convertSeckillOrderToOrder(seckillOrderAddDTO);
        // 上面完成了转换,所有订单相关信息都赋值到了orderAddDTO
        // 但是前端信息中不会包含用户id,所有要单独赋值
        orderAddDTO.setUserId(userId);
        // 信息完成后,Dubbo调用新增订单的方法
        OrderAddVO orderAddVO=dubboOrderService.addOrder(orderAddDTO);
        // 第三阶段:秒杀成功信息消息队列的发送
        // 我们向RabbitMQ中发送Success对象,实现消息队列的方法新增success信息到数据库
        // 因为Success对象作为秒杀成功记录来讲,并不是急迫的操作
        // 又因为它要操作数据库,所以最后在服务器不忙时再运行,进行"削峰填谷"
        // 实例化Success对象
        Success success=new Success();
        // Success中大多数属性是sku实体具备的,我们可操作的对象中,秒杀订单项是sku
        // 所以可以将秒杀订单项的同名属性赋值到success
        BeanUtils.copyProperties(
                seckillOrderAddDTO.getSeckillOrderItemAddDTO(),success);
        // 然后补全缺少的信息
        success.setUserId(userId);
        success.setOrderSn(orderAddVO.getSn());
        success.setSeckillPrice(
                seckillOrderAddDTO.getSeckillOrderItemAddDTO().getPrice());
        // success信息完备,向RabbitM发送消息
        rabbitTemplate.convertAndSend(
                RabbitMqComponentConfiguration.SECKILL_EX,
                RabbitMqComponentConfiguration.SECKILL_RK,
                success);

        // 上面发送给RabbitMQ的消息会在专门的消息处理类中去接收,当前方法不用关注
        // 最后按系统设计的返回值返回,经过分析SeckillCommitVO中的属性和OrderAddVO完全一致
        SeckillCommitVO commitVO=new SeckillCommitVO();
        // 将OrderAddVO的同名属性赋值给commitVO即可
        BeanUtils.copyProperties(orderAddVO,commitVO);
        //  最后别忘了返回!!!
        return commitVO;
    }

    private OrderAddDTO convertSeckillOrderToOrder(SeckillOrderAddDTO seckillOrderAddDTO) {
        // 首先实例化最终要返回的普通订单对象
        OrderAddDTO orderAddDTO=new OrderAddDTO();
        // 将秒杀订单对象中同名属性直接赋值到普通订单对象中
        BeanUtils.copyProperties(seckillOrderAddDTO,orderAddDTO);
        // seckillOrderAddDTO包含的订单项对象SeckillOrderItemAddDTO
        // 但是普通订单包含的订单项是List<OrderItemAddDTO>
        // 所以我们要将SeckillOrderItemAddDTO对象转换为OrderItemAddDTO类型
        // 然后再实例化一个集合添加进去,赋值给普通订单
        OrderItemAddDTO orderItemAddDTO=new OrderItemAddDTO();
        BeanUtils.copyProperties(
                seckillOrderAddDTO.getSeckillOrderItemAddDTO(),
                orderItemAddDTO);
        // 实例化普通订单中的集合属性
        List<OrderItemAddDTO> list=new ArrayList<>();
        // 将赋值好的对象添加到集合中
        list.add(orderItemAddDTO);
        // 将集合赋值到普通订单中
        orderAddDTO.setOrderItems(list);
        // 转换完成,orderAddDTO包含了订单信息和订单项信息,可以返回了
        return orderAddDTO;
    }


    public CsmallAuthenticationInfo getUserInfo(){
        // 编码获得SpringSecurity上下文中保存的权限
        UsernamePasswordAuthenticationToken authenticationToken=
                (UsernamePasswordAuthenticationToken)
                        SecurityContextHolder.getContext().getAuthentication();
        // 为了保险起见,判断一下从SpringSecurity中获得的信息是不是null
        if(authenticationToken == null){
            throw new CoolSharkServiceException(ResponseCode.UNAUTHORIZED,
                    "请您先登录!");
        }
        // 上下文信息确定存在后,获取其中的用户信息
        // 这个信息就是有JWT解析获得的
        CsmallAuthenticationInfo csmallAuthenticationInfo=
                (CsmallAuthenticationInfo) authenticationToken.getCredentials();
        // 返回登录信息
        return csmallAuthenticationInfo;
    }
    // 业务逻辑层大多数方法需要用户的信息实际上就是用户的ID,编写一个只返回用户ID的方法方便调用
    public Long getUserId(){
        return getUserInfo().getId();
    }
}

 控制层

@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)
    @PreAuthorize("hasRole('user')")
    // Sentinel配置限流和降级
    @SentinelResource(value = "seckill",
      blockHandlerClass = SeckillBlockHandler.class,blockHandler = "seckillBlock",
      fallbackClass = SeckillFallback.class,fallback = "seckillFallback")
    public JsonResult<SeckillCommitVO> commitSeckill(
            @PathVariable String randCode,
            @Validated SeckillOrderAddDTO seckillOrderAddDTO){
        // 获得spuId
        Long spuId=seckillOrderAddDTO.getSpuId();
        // 从Redis中获取spuId对应的随机码
        // 先获得Key mall:seckill:spu:url:rand:code:10
        String randCodeKey= SeckillCacheUtils.getRandCodeKey(spuId);
        // 判断redis中是否有这个key
        if(redisTemplate.hasKey(randCodeKey)){
            // 如果redis中有这个key,取出这个key的值
            String redisRandCode=
                    redisTemplate.boundValueOps(randCodeKey).get()+"";
            // 判断如果redis中的随机码和控制器方法参数接收的随机码不同
            if(!redisRandCode.equals(randCode)){
                //  两个随机码不一致,不允许运行购买业务
                throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                        "没有找到指定商品");
            }
            // 运行到此处表示redis中的随机码和传入的随机码一致,可以执行购买
            SeckillCommitVO commitVO=
                        seckillService.commitSeckill(seckillOrderAddDTO);
            return JsonResult.ok(commitVO);
        }else{
            // 当redis中没有保存这个spuId商品的信息时,也是抛出异常
            throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                    "没有找到指定商品");
        }
    }
}

success秒杀成功信息的处理

向RabbitMQ队列中,输出了添加秒杀成功信息的消息, 但是我们没有任何处理

将秒杀成功信息发送到消息队列的原因:,秒杀成功信息用于统计秒杀数据,是秒杀结束后才需要统计的, 所以在秒杀并发高时,消息队列的发送可以延缓,在服务器不忙时,再运行(削峰填谷)

开发持久层

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

我们要连接数据库,对这个表进行新增, 还有对秒杀数据库sku库存的修改

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

// 根据skuId减少指定库存数
int updateReduceStockBySkuId(@Param("skuId") Long skuId,
                             @Param("quantity") Integer quantity);
<!-- 根据skuId减少指定库存数-->
<update id="updateReduceStockBySkuId">
    update
        seckill_sku
    set
        seckill_stock=seckill_stock - #{quantity}
    where
        sku_id=#{skuId}
</update>

下面再编写新增Success的方法, 创建SuccessMapper接口编写方法

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

开发消息的接收功能

我们当前触发新增Success的方法并不是常规的业务逻辑层, 而是由RabbitMQ消息收发机制中接收消息的对象来调用, 所有我们编写一个接收消息的监听器类来完成这个操作

@Component
@RabbitListener(queues = RabbitMqComponentConfiguration.SECKILL_QUEUE)
public class SeckillQueueConsumer {
    @Autowired
    private SeckillSkuMapper skuMapper;
    @Autowired
    private SuccessMapper successMapper;
    // 下面方法会在消息队列中有出现消息时自动运行
    @RabbitHandler
    public void process(Success success){
        // 先减少库存
        skuMapper.updateReduceStockBySkuId(
                success.getSkuId(),success.getQuantity());
        // 新增Success对象到数据库
        successMapper.saveSuccess(success);

        // 上面两个数据库操作发送异常,可能会引发事务问题
        // 我们可以编写异常处理方法,将消息发送给死信队列,后续人工处理
        // 死信队列的办法是最后手段,不要频繁使用
    }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值