商城:秒杀服务

秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。

限流方式:

1. 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
2. nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
3. 网关限流,限流的过滤器
4. 代码中使用分布式信号量
5. rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

定时任务

cron表达式在线生成:https://cron.qqe2.com/

SpringBoot整合定时任务与异步任务

/**
 * 定时任务
 *      1、@EnableScheduling 开启定时任务
 *      2、@Scheduled开启一个定时任务
 *
 * 异步任务
 *      1、@EnableAsync:开启异步任务
 *      2、@Async:给希望异步执行的方法标注
 */
/**
 * @author yaoxinjia
 */
@Slf4j
@Component
// @EnableAsync
// @EnableScheduling
public class HelloScheduled {

    /**
     * 1、在Spring中表达式是6位组成,不允许第七位的年份
     * 2、在周几的的位置,1-7代表周一到周日
     * 3、定时任务不该阻塞。默认是阻塞的
     *      1)、可以让业务以异步的方式,自己提交到线程池
     *              CompletableFuture.runAsync(() -> {
     *         },execute);
     *
     *      2)、支持定时任务线程池;设置 TaskSchedulingProperties
     *        spring.task.scheduling.pool.size: 5
     *
     *      3)、让定时任务异步执行
     *          异步任务  标注@Async
     *          spring.task.execution.pool.size: 5 
     *          spring.task.execution.pool.size: 50
     *
     *      解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
     *
     */
     @Async
     @Scheduled(cron = "*/5 * * ? * 4")
     public void hello() {
         log.info("hello...");
         try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    
     }

 时间日期处理

    @Override
    public List<SeckillSessionEntity> getLate3DaySession() {
        //计算最近三天
        List<SeckillSessionEntity> start_time = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
        return start_time;
    }
    //开始的时间  如:2022-10-19 00:00:00
    private String startTime(){
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now,min);
        String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }
    //结束的时间  如:2022-10-22 23:59:59
    private String endTime(){
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalDateTime of = LocalDateTime.of(localDate,LocalTime.MAX);
        String format = of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

 秒杀商品上架

随机码:防止用户直接访问接口完成抢购

库存作为分布式的信号量 限流:

  • 计数信号量是一种锁,他可以让用户限制一项资源最多能够同时被多少个进程访问

计数信号量和其他锁的区别:

  • [客户端获取锁失败时],客户端会选择等待
  • [获取信号量失败时]通常直接退出,并向用户提示"资源繁忙",由用户决定下一步如何处理

 

秒杀上架的相关代码

   private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    private final String SKUSTOCK_SEMAPHONE = "seckill:stock:"; // +商品随机码

    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1.扫描最近三天要参加秒杀的商品
        R r = couponFeignService.getLate3DaySession();
        if(r.getCodr() == 0){
            List<SeckillSessionWithSkusVo> sessions = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {});
            // 2.缓存活动信息
            saveSeesionInfos(sessions);
            // 3.缓存活动的关联的商品信息
            saveSessionSkuInfos(sessions);
       }
    }
   //缓存活动信息
    public void saveSeesionInfos(List<SeckillSessionWithSkusVo> sessions){
        sessions.stream().forEach(session -> {
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
            //缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,collect);
        });
    }

    //缓存活动的关联商品信息
    public void saveSessionSkuInfos(List<SeckillSessionWithSkusVo> sessions){
        sessions.stream().forEach(session->{
            //准备 hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //缓存商品
                SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                //1.sku的基本数据
                R r = productFeignService.skuInfo(seckillSkuVo.getSkuId());
                if(r.getCodr()==0){
                    SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(skuInfo);
                }

                //2.sku的秒杀信息
                BeanUtils.copyProperties(seckillSkuVo,redisTo);
                //3.设置上当前商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());
                //4.随机码  seckill?skuId=1&key=adasdcsd
                String token = UUID.randomUUID().toString().replace("-", "");
                redisTo.setRandomCode(token);
                //使用库存作为分布式的信号量 限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                String jsonString = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
            });
        });
    }

 

幂等性保护

方式一个秒杀场次的商品多次上架到Redis中,加锁处理

com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl

查询秒杀商品

前端Ajax请求http://seckill.mall.com/currentSeckillSkus 查询出所有参与秒杀的商品

com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#getCurrentSeckillSkus

前端 详细页传入商品ID 查询出是否参与秒杀 与 其相关信息

com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#getSkuSeckillInfo

秒杀系统设计

秒杀(高并发)关注的问题

  • 服务独立部署
  • 秒杀链接加密
  • 库存预热  快速扣减
  • 动静分类
  • 恶意请求
  • 流量错峰
  • 限流&熔断&降级
  • 队列削峰

登录检查

前端js判断 商品是否在秒杀的商品中,在显示"立即抢购",不在显示"立即购买"

js实现点击立即抢购判断是否为登录用户,跳转:http://seckill.mall.com/kill?killId= &key= &num= ;

秒杀流程

主要步骤:去Redis中获取信号量,获取成功就生成订单信息

代码:com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#kill

秒杀效果

方法的实现

    @Override
    public String kill(String killId, String key, Integer num) {

        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
        //获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

        String json = hashOps.get(killId);
        if(StringUtils.isEmpty(json)){
            return null;
        }else {
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            //校验合法性
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            long time = new Date().getTime();

            long l = endTime - time;
            //校验时间的合法性
            if(time>=startTime && time<=endTime){
                //校验随机码 和 商品id
                String randomCode = redisTo.getRandomCode();
                String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                if(randomCode.equals(key)&&killId.equals(skuId)){
                    //3.验证购物数量是否合理
                    if(num<=redisTo.getSeckillLimit()){
                        //4.验证这个人是否已经买过,幂等性 如果秒杀成功就去站位
                        //SETNX
                        String s = respVo.getId() + "_" + skuId;
                        //自动过期
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(s, num.toString(), l, TimeUnit.MILLISECONDS);
                        if(aBoolean){
                            //占位成功说明从来没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE);

                                semaphore.tryAcquire(num);
                                //秒杀成功
                                // 快速下单  发送MQ消息
                                String orderSn = IdWorker.getTimeId();
                                SecKillOrderTo orderTo = new SecKillOrderTo();
                                orderTo.setOrderSn(orderSn);
                                orderTo.setMemberId(respVo.getId());
                                orderTo.setNum(num);
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);


                                return orderSn;
                        }

                    }
                }else {
                    return null;
                }
            }else {
                return null;
            }
        }
        return null;
    }

前端页面请求

http://seckill.mall.com/kill?killId=&key=&num=

还要添加一个拦截器 /kill 是否登录,登录了就跳转success页面

阅读剩余
THE END