30.秒杀

流程

后台添加秒杀商品

秒杀的商品应该独立部署放在一个独立的模块里

在网关里配置优惠系统的断言信息 这里给出完整的网关配置

spring:
  cloud:
    gateway:
      routes:
        #商品的路由
        #localhost:88/api/product/category/list/tree--->localhost:10000/product....
        #优先级比下面的哪个路由要高所以要放在上面,不然会被截断
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**,/hello
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        - id: coupon_route
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        #第三方路由配置
        - id: third_party_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
            - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
        #会员服务的路由
        - id: member_route
          uri: lb://gulimall-member
          predicates:
            - Path=/api/member/**
          filters:
            #api前缀去掉剩下的全体保留
            - RewritePath=/api/(?<segment>.*),/$\{segment}
              #会员服务的路由
        - id: ware_route
          uri: lb://gulimall-ware
          predicates:
            - Path=/api/ware/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        - id: admin_route
          #lb表示负载均衡
          uri: lb://renren-fast
          #规定前端项目必须带有一个api前缀
          #原来验证码的uri ...localhost:88/api/captcha.jpg
          #应该改成的uri ...localhost:88/renrenfast/captcha.jpg
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

        - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
            - Host=gulimall.com,item.gulimall.com

        - id: gulimall_search_route
          uri: lb://gulimall-search
          predicates:
            - Host=search.gulimall.com

        - id: gulimall_auth_route
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.gulimall.com
        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com
        - id: gulimall_order_route
          uri: lb://gulimall-order
          predicates:
            - Host=order.gulimall.com
        - id: gulimall_member_route
          uri: lb://gulimall-member
          predicates:
            - Host=member.gulimall.com

修改原生的秒杀服务的page方法让他可以根据场次查询

package com.wuyimin.gulimall.coupon.service.impl;

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<>();
        String id = (String) params.get("promotionSessionId");
        if(!StringUtils.isEmpty(id)){
            queryWrapper.eq("promotion_session_id",id);
        }
        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),queryWrapper
                
        );
        return new PageUtils(page);
    }
}

创建秒杀微服务

同样的需要依赖common模块

 主函数

package com.wuyimin.gulimall.seckill;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
public class GulimallSeckillApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallSeckillApplication.class, args);
    }

}

配置文件

# 应用名称
spring.application.name=gulimall-seckill
# 应用服务 WEB 访问端口
server.port=25000
spring.cloud.nacos.server-addr=127.0.0.1:8848
spring.redis.host=192.168.116.128

定时任务

Cron表达式


Cron表达式参数分别表示:

秒(0~59) 例如0/5表示每5秒
分(0~59)
时(0~23)
日(0~31)的某天,需计算
月(0~11)
周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
@Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
// Cron表达式范例:

每隔5秒执行一次:*/5 * * * * ?

每隔1分钟执行一次:0 */1 * * * ?

每天23点执行一次:0 0 23 * * ?

每天凌晨1点执行一次:0 0 1 * * ?

每月1号凌晨1点执行一次:0 0 1 1 * ?

每月最后一天23点执行一次:0 0 23 L * ?

每周星期天凌晨1点实行一次:0 0 1 ? * L

在26分、29分、33分执行一次:0 26,29,33 * * * ?

每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

定时任务整合springBoot

package com.wuyimin.gulimall.seckill.scheduled;

/**
 * @ Author wuyimin
 * @ Date 2021/8/31-16:54
 * @ Description
 */
@Slf4j
@Component
@EnableScheduling
public class HelloSchedule {
    //只允许六位,不允许第七位的年份
    @Scheduled(cron = "* * * * * ?")//每秒打印
    public void hello(){
        log.info("hello");
    }
}

定时任务默认是阻塞的,开发中定时任务不应该被阻塞

  • 可以使用异步编排来处理定时任务中,执行时间超长的逻辑
  • springBoot有一个定时任务的线程池在配置里使用spring.task.scheduling.pool.size=5(有的版本有bug,不一定好使)
  • 异步任务(采用):类开启异步任务@EnableAsync,方法@Async
@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class HelloSchedule {
    //只允许六位,不允许第七位的年份
    @Async
    @Scheduled(cron = "* * * * * ?")//每秒打印
    public void hello() throws InterruptedException {
        log.info("hello");
        Thread.sleep(3000);
    }
}

测试结果

可以看到并没有阻塞

 时间日期的处理

创建一个配置类用来开启异步和定时功能

package com.wuyimin.gulimall.seckill.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/31-18:27
 * @ Description
 */
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {
}

获得当前时间和两天后的时间字符串的方法

  package com.wuyimin.gulimall.coupon.service.impl;
private String getStartTime(){
        LocalDate now = LocalDate.now();//拿到当前时间
        LocalTime min = LocalTime.MIN;//00:00
        return LocalDateTime.of(now, min).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
    }
    private String getEndTime(){
        LocalDate now = LocalDate.now();//拿到当前时间
        LocalDate plusDays = now.plusDays(2);//加两天
        LocalTime max = LocalTime.MAX;//23:59.9999
        return LocalDateTime.of(plusDays, max).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
    }

秒杀商品上架

获得秒杀服务中商品远程方法

package com.wuyimin.gulimall.coupon.service.impl;  
  @Override
    public List<SeckillSessionEntity> getLates3DaySession() {
        String startTime = getStartTime();
        String endTime = getEndTime();
        //筛选日期
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime, endTime));
        if(list!=null&&list.size()>0){
            List<SeckillSessionEntity> collect = list.stream().peek(item -> {
                Long id = item.getId();
                //设置商品
                List<SeckillSkuRelationEntity> entities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                item.setRelationEntities(entities);
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

使用redission分布式信号量需要引入的依赖和配置

 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
package com.wuyimin.gulimall.seckill.config;

/*
 * @ Author wuyimin
 * @ Date 2021/8/17-9:42
 * @ Description 分布式锁配置文件
 */
@Configuration
public class RedissonConfig {
    // redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        // 创建单节点模式的配置
        config.useSingleServer().setAddress("redis://192.168.116.128:6379");
        return Redisson.create(config);
    }
}
package com.wuyimin.gulimall.seckill.service.impl;

/**
 * @ Author wuyimin
 * @ Date 2021/8/31-18:33
 * @ Description
 */
@Service
public class SeckillServiceImpl implements SecKillService {
    private final String SESSIONS_CACHE_PREFIX="seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX="seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE="seckill:stock:";//+商品随机码
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    RedissonClient redissonClient;
    @Override
    public void uploadSeckillSkuLatest3Days() {
        //1.去扫描需要参与秒杀的活动
        R r = couponFeignService.getLates3DaySession();
        if(r.getCode()==0){
            //上架商品数据
            List<SeckillSessionsWithSkus> data = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            //缓存活动信息
            saveSessionInfos(data);
            //缓存活动的关联商品信息
            saveSessionSkuInfos(data);
        }
    }

    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> data) {

        data.forEach(item->{
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            item.getRelationEntities().forEach(vo->{
                SeckillSkuRedisTo to = new SeckillSkuRedisTo();
                //除了秒杀信息我们还需要sku的详细信息
                R info = productFeignService.info(vo.getSkuId());
                if(info.getCode()==0){
                    SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    if(skuInfo!=null){
                        to.setSkuInfoVo(skuInfo);
                    }
                }
                //秒杀信息
                BeanUtils.copyProperties(vo,to);
                //保存开始结束时间
                to.setStartTime(item.getStartTime().getTime());
                to.setEndTime(item.getEndTime().getTime());
                //保存随机码--公平秒杀
                String token = UUID.randomUUID().toString().replace("_", "");
                to.setRandomCode(token);
                //获取信号量
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                semaphore.trySetPermits(vo.getSeckillCount());//商品可以秒杀的数量作为信号量
                String s = JSON.toJSONString(to);
                ops.put(vo.getSkuId().toString(),s);
            });
        });
    }
    private void saveSessionInfos(List<SeckillSessionsWithSkus> data){
        data.forEach(session->{
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            //等会会与前缀结合作为key值传入redis
            String key=startTime+"_"+endTime;
            List<String> collect = session.getRelationEntities().stream().map(item->item.getSkuId().toString()
            ).collect(Collectors.toList());
            //以集合的方式向左边批量添加元素
            redisTemplate.opsForList().leftPushAll(SESSIONS_CACHE_PREFIX+key,collect);
        });
    }

}

定时方法

package com.wuyimin.gulimall.seckill.scheduled;
/**
 * @ Author wuyimin
 * @ Date 2021/8/31-18:26
 * @ Description 秒杀服务定时上架
 */
@Slf4j
@Service
public class SeckillSkuScheduled {
    @Autowired
    SecKillService secKillService;
    //每分钟上架以后三天要秒杀的商品
    @Scheduled(cron ="0 * * * * ?")
    public void uploadSeckillSkuLatest3Days(){
        log.info("上架商品信息");
        //1.重复上架无需处理
        secKillService.uploadSeckillSkuLatest3Days();
    }
}

上架的幂等性问题

打开redis我们发现,会场的信息在不停的往里面存而且都是重复的

 而且在分布式环境下,如果有多台机器,那么每台机器都来执行这个定时任务就保证不了幂等性,这时就需要分布式锁

package com.wuyimin.gulimall.seckill.scheduled;
@Slf4j
@Service
public class SeckillSkuScheduled {
    @Autowired
    SecKillService secKillService;
    @Autowired
    RedissonClient redissonClient;
    private final String UPLOAD_LOCK="seckill:upload:lock";
    //每分钟上架以后三天要秒杀的商品
    @Scheduled(cron ="0 * * * * ?")
    public void uploadSeckillSkuLatest3Days(){
        log.info("上架商品信息");
        //1.重复上架无需处理
        //添加分布式锁
        RLock lock = redissonClient.getLock(UPLOAD_LOCK);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            secKillService.uploadSeckillSkuLatest3Days();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

需要处理的问题:

1.如果一个物品的库存信息(信号量)在第一场秒杀活动上架了,第二场需要再上架该商品的库存信息,不能因为第一场商品有了第二场就不上架了

2.如果一个商品在两场活动中都有,我们根据前缀+id的方式就没办法判断他来自哪个场次,所以更改key未前缀+id+场次

更改后的方法

package com.wuyimin.gulimall.seckill.service.impl;
/**
 * @ Author wuyimin
 * @ Date 2021/8/31-18:33
 * @ Description
 */
@Service
public class SeckillServiceImpl implements SecKillService {
    private final String SESSIONS_CACHE_PREFIX="seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX="seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE="seckill:stock:";//+商品随机码
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    RedissonClient redissonClient;
    @Override
    public void uploadSeckillSkuLatest3Days() {
        //1.去扫描需要参与秒杀的活动
        R r = couponFeignService.getLates3DaySession();
        if(r.getCode()==0){
            //上架商品数据
            List<SeckillSessionsWithSkus> data = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            //缓存活动信息
            saveSessionInfos(data);
            //缓存活动的关联商品信息
            saveSessionSkuInfos(data);
        }
    }

    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> data) {

        data.forEach(item->{
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            item.getRelationEntities().forEach(vo->{
                if(!ops.hasKey(vo.getPromotionSessionId()+"_"+vo.getSkuId().toString())){
                    SeckillSkuRedisTo to = new SeckillSkuRedisTo();
                    //除了秒杀信息我们还需要sku的详细信息
                    R info = productFeignService.info(vo.getSkuId());
                    if(info.getCode()==0){
                        SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        if(skuInfo!=null){
                            to.setSkuInfoVo(skuInfo);
                        }
                    }
                    //秒杀信息
                    BeanUtils.copyProperties(vo,to);
                    //保存开始结束时间
                    to.setStartTime(item.getStartTime().getTime());
                    to.setEndTime(item.getEndTime().getTime());
                    //保存随机码--公平秒杀
                    String token = UUID.randomUUID().toString().replace("_", "");
                    to.setRandomCode(token);
                    //获取信号量,限流
                    //如果两个场次都有这个秒杀商品,当前这个商品场次的库存信息已经上架了就不需要再上架了
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    semaphore.trySetPermits(vo.getSeckillCount());//商品可以秒杀的数量作为信号量
                    String s = JSON.toJSONString(to);
                    //解决问题2
                    ops.put(vo.getPromotionSessionId().toString()+"_"+vo.getSkuId().toString(),s);
                }
            });
        });
    }
    private void saveSessionInfos(List<SeckillSessionsWithSkus> data){
        data.forEach(session->{
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key=SESSIONS_CACHE_PREFIX+startTime+"_"+endTime;
            Boolean aBoolean = redisTemplate.hasKey(key);
            if(!aBoolean){
                List<String> collect = session.getRelationEntities().stream().map(item->item.getPromotionSessionId()+"_"+item.getSkuId().toString()
                ).collect(Collectors.toList());
                //以集合的方式向左边批量添加元素
                redisTemplate.opsForList().leftPushAll(key,collect);
            }
        });
    }

}

查询到秒杀商品

controller--》把商品的基本信息放在首页去渲染

package com.wuyimin.gulimall.seckill.controller;
/**
 * @ Author wuyimin
 * @ Date 2021/9/1-8:52
 * @ Description
 */
@RestController
public class SeckillController {
    @Autowired
    SecKillService secKillService;
    /**
     * 返回当前时间可以参与秒杀的商品信息
     * @return
     */
    @GetMapping("/currentSeckillSkus")
    public R getCurrentKillSkus() {
        List<SeckillSkuRedisTo> vos=secKillService.getCurrentSeckillSkus();
        return  R.ok().setData(vos);
    }
}

方法

 @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //确定当前时间是哪个秒杀场次的
        Date date = new Date();
        long time = date.getTime();
        Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");//获得所有带有这个前缀的key,只要后面截取的时间段包含现在的时间就表面在场次内
        for (String key : keys) {
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            Long start=Long.parseLong(s[0]);
            Long end =Long.parseLong(s[1]);
            if(start<=time&&time<=end){
                //是当前场次,准备返回商品信息
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, Object> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<Object> objects = hashOps.multiGet(range);
                if(objects!=null){
                    List<SeckillSkuRedisTo> collect = objects.stream().map(item -> {
                        SeckillSkuRedisTo to = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
                        return to;
                        //当前秒杀开始了需要随机码
                    }).collect(Collectors.toList());
                    return collect;
                }
            }
        }
        return null;
    }

}

获取秒杀的商品的具体item信息


package com.wuyimin.gulimall.seckill.controller;
@GetMapping(value = "/getSeckillSkuInfo/{skuId}")
    public R getSeckillSkuInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = secKillService.getSeckillSkuInfo(skuId);
        return R.ok().setData(to);
    }

具体方法

package com.wuyimin.gulimall.seckill.service.impl;
    @Override
    public SeckillSkuRedisTo getSeckillSkuInfo(Long skuId) {
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = ops.keys();//获取所有的key
        if(keys!=null&&keys.size()>0){
            //但是这里其实并没有考虑到一个商品参与到多个会场的秒杀但是价格信息不同的情况
            String regx="\\d_"+skuId;//匹配用的正则表达式 代表以数字开头加一个下划线加skuId
            for (String key : keys) {
                boolean matches = Pattern.matches(regx, key);//返回key和我们的正则表达式是否一致
                if(matches){
                    String s = ops.get(key);
                    SeckillSkuRedisTo skuRedisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
                    //处理随机码,如果当前商品查询出来不在时间内就让随机码制空,让别人无法访问
                    long current=new Date().getTime();
                     if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()) {

                    }else {
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }
            }
        }
        return  null;
    }

来到product项目调用秒杀服务的这个请求

package com.wuyimin.gulimall.product.service.impl;    
//运行的顺序 1 2 6同时 345要在1运行完之后运行
    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();
        //第一个异步任务的结果别人还要用,所以就使用supply
        CompletableFuture<SkuInfoEntity> futureInfo = CompletableFuture.supplyAsync(() -> {
            //1.查询当前sku的基本信息 sku_info表
            SkuInfoEntity skuInfoEntity = getById(skuId);
            skuItemVo.setInfo(skuInfoEntity);
            return skuInfoEntity;
        }, executor);
        //需要接受结果
        CompletableFuture<Void> futureSaleAttrs = futureInfo.thenAcceptAsync(res -> {
            //3.获取spu的销售属性组合
            List<SkuItemSaleAttrsVo> skuItemSaleAttrsVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttrsVos(skuItemSaleAttrsVos);
        }, executor);

        CompletableFuture<Void> futureInfoDesc = futureInfo.thenAcceptAsync(res -> {
            //4.获取spu的介绍
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);

        CompletableFuture<Void> futureItemAttrGroups = futureInfo.thenAcceptAsync(res -> {
            //5.获取spu的规格参数
            List<SpuItemAttrGroup> spuItemAttrGroups = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(spuItemAttrGroups);
        }, executor);
        //没有什么返回结果
        CompletableFuture<Void> futureImage = CompletableFuture.runAsync(() -> {
            //2.sku的图片信息 pms_sku_image
            List<SkuImagesEntity> skuImagesEntities = imagesService.list(new QueryWrapper<SkuImagesEntity>().eq("sku_Id", skuId));
            skuItemVo.setImages(skuImagesEntities);
        }, executor);
        //等到任务全部做完,可以不用写info,因为image完了info肯定完了
        //6.查询到当前商品是否参加秒杀活动
        CompletableFuture<Void> futureSeckill = CompletableFuture.runAsync(() -> {
            R seckillSkuInfo = seckillFeignService.getSeckillSkuInfo(skuId);
            if (seckillSkuInfo.getCode() == 0) {
                SeckillSkuRedisTo data = seckillSkuInfo.getData(new TypeReference<SeckillSkuRedisTo>() {
                });
                skuItemVo.setSeckillSkuRedisTo(data);
            }
        }, executor);


        CompletableFuture.allOf(futureImage, futureInfoDesc, futureItemAttrGroups, futureSaleAttrs,futureSeckill).get();
        return skuItemVo;
    }

秒杀接口的系统设计

  • 服务单一职责+独立部署:秒杀服务顶不住压力挂掉不会影响别人(已经完成)
  • 秒杀链接加密:防止恶意攻击,1000次/s的请求攻击,防止链接暴露,工作人员提前秒杀商品(已经完成--随机码)
  • 库存预热+快速扣减:秒杀读多写少,无需每次实时校验库存,我们库存预热放到redis中,信号量控制进来秒杀的请求(已经完成)
  • 动静分离:nginx做好动静分离,保证秒杀和商品详情页的动态请求才打到后端服务器集群,使用cdn网络,分担本集群的压力(已经完成)
  • 恶意请求拦截:识别非法攻击请求并且拦截
  • 流量错峰:使用各种手段,将流量分担到更大宽度的时间点,比如验证码,加入购物车
  • 限流,熔断,降级:前端限流+后端限流,限制次数,限制总量,快速失败降级运行,熔断防止雪崩
  • 队列削封:所有秒杀成功的请求,进入队列,慢慢创建,扣减库存即可

登录检查

RedisSession依赖

  <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

Session配置

package com.wuyimin.gulimall.seckill.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/23-14:50
 * @ Description
 */
@EnableRedisHttpSession
@Configuration
public class RedisSessionConfig {
    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}

拦截器配置

package com.wuyimin.gulimall.seckill.interceptor;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-10:52
 * @ Description 拦截未登录用户
 */
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
    //只拦截秒杀请求
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri=request.getRequestURI();
        boolean match = new AntPathMatcher().match("/kill", uri);
        if(match){
            HttpSession session = request.getSession();//获取session
            MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
            if(attribute!=null){
                //已经登录
                loginUser.set(attribute);
                return true;
            }else{
                //给前端用户的提示
                request.getSession().setAttribute("msg","请先进行登录");
                //未登录
                response.sendRedirect("http://auth.gulimall.com/login.html");//重定向到登录页
                return false;
            }
        }
        return true;
    }
}

写了拦截器以后一定要加入到web的配置中

package com.wuyimin.gulimall.seckill.config;

/**
 * @ Author wuyimin
 * @ Date 2021/9/1-12:58
 * @ Description
 */
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
    @Autowired
    LoginUserInterceptor loginUserInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

秒杀流程

 

引入rabbitMQ

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

 配置信息,这里就没有开启手动ack了,想看手动ack去订单里看

 配置文件

package com.wuyimin.gulimall.seckill.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/25-18:58
 * @ Description MQ回调机制测试
 */
@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();//转成json
    }
}

 封装mq用来传递消息的to

@Data
public class SecKillTo {
    private String orderSn;//订单号
    private Long promotionSessionId;
    private Long skuId;
    private BigDecimal seckillPrice;
    private Integer num;//购买数量
    private Long memberId;//谁买的
}

秒杀服务

   package com.wuyimin.gulimall.seckill.service.impl; 
@Override
    public String kill(String killId, String key, Integer num) {
        MemberRespVo user = LoginUserInterceptor.loginUser.get();//获得用户信息
        //获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String s = ops.get(killId);
        if (StringUtils.isEmpty(s)) {
            return null;
        } else {
            SeckillSkuRedisTo to = JSON.parseObject(s, SeckillSkuRedisTo.class);
            //校验合法性,判断时间区间(略)
            //校验随机码
            String randomCode = to.getRandomCode();
            if (randomCode.equals(key)) {
                //校验购物数量是否合理
                if (num <= to.getSeckillLimit()) {
                    //判断此用户是否已经买过了,只要秒杀成功了,就去redis里占位 用户id_sessionId_skuId
                    String userKey = user.getId() + "_" + to.getPromotionSessionId() + "_" + to.getSkuId();
                    //存的是买的商品的数量,超时时间设置(本来应该是整个会场持续的时间这里模拟30s)
                    Boolean hasNotBought = redisTemplate.opsForValue().setIfAbsent(userKey, num.toString(), 30, TimeUnit.SECONDS);//不存在的时候才占位
                    if (hasNotBought) {
                        //从来没买过就获取信号量
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        try {
                            boolean isGet = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);//这个获取信号量的方法是非阻塞的,等待时间未100ms
                            String orderSn = IdWorker.getTimeId()+ UUID.randomUUID().toString().replace("_","");
                            if(isGet){
                                //给mq发送消息
                                SecKillTo secKillTo = new SecKillTo();
                                secKillTo.setMemberId(user.getId());
                                secKillTo.setNum(num);
                                secKillTo.setPromotionSessionId(to.getPromotionSessionId());
                                secKillTo.setOrderSn(orderSn);
                                secKillTo.setSeckillPrice(to.getSeckillPrice());
                                secKillTo.setSkuId(to.getSkuId());
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",secKillTo);
                                return orderSn;
                            }
                        } catch (InterruptedException e) {
                            return null;
                        }
                    }
                }
            }
        }
        return null;
    }
}

发送了消息之后,订单服务就要接受处理消息,创建队列和绑定关系

 package com.wuyimin.gulimall.order.config;


@Bean
    public Queue orderSeckillOrderQueue(){
        return new Queue("order.seckill.order.queue",true,false,false);
    }

    @Bean
    public Binding orderSeckillOther(){
        return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE,
                "order-event-exchange","order.seckill.order",null);
    }

 创建监听器监听此队列

package com.wuyimin.gulimall.order.listener;

/**
 * @ Author wuyimin
 * @ Date 2021/9/1-14:06
 * @ Description
 */
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSecKillListener {
    @Autowired
    OrderService orderService;
    @RabbitHandler
    public void listener(SecKillTo to, Channel channel, Message message) throws IOException {
        System.out.println("准备创建秒杀单的详细信息"+to);
        //手动签收消息(拿到原生消息,选择不批量告诉)
        try {
            orderService.createSeckill(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

设置几个基本的内容意思一下

    @Override
    public void createSeckill(SecKillTo to) {
        //保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(to.getOrderSn());
        orderEntity.setMemberId(to.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        orderEntity.setPayAmount(to.getSeckillPrice().multiply(new BigDecimal(to.getNum().toString())));
        save(orderEntity);
        //保存订单项目信息
        OrderItemEntity itemEntity = new OrderItemEntity();
        itemEntity.setOrderSn(to.getOrderSn());
        itemEntity.setRealAmount(to.getSeckillPrice().multiply(new BigDecimal(to.getNum().toString())));
        itemEntity.setSkuQuantity(to.getNum());
         orderItemService.save(itemEntity);
    }

至此秒杀的后台基本就完成了

处理秒杀结果

    
    @GetMapping("/kill")
    public String secKill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num, Model model){
        //1.判断是否登录-整合拦截器机制和springSession
        //2.秒杀成功就返回这个订单号
        String orderSn=secKillService.kill(killId,key,num);
        model.addAttribute("orderSn",orderSn);
        return "success";
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值