【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【28】秒杀

48 篇文章 3 订阅
31 篇文章 0 订阅

持续学习&持续更新中…

守破离


秒杀

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存 + 动静分离 + 独立部署。

限流方式:

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

高并发有三宝

  • 缓存
  • 异步
  • 对排好(消息队列…)

秒杀商品定时上架

在这里插入图片描述

思路

  • 每天晚上3点上架最近三天的秒杀商品。【这个时间段服务器压力较小,并且比较空闲】,而且上架最近3天的商品,可以预告给用户最近三天哪个商品什么时间将要开启秒杀

  • 缓存到redis

  • 商品秒杀随机码:

    • 没有随机码的话,秒杀接口假如是这样:seckill?skuId=1,这样不安全容易引起脚本攻击秒杀
    • 有了随机码:seckill?skuId=1&key=sfuhgregsfds2fdsf4 ,商品开始了秒杀这个随机码才会暴露出来;就算你知道哪个商品要秒杀没有随机码你也秒杀不了
    • 随机码还可以用于当作该商品的分布式信号量,来限流
  • redis中数据保存结构设计:

    • 秒杀场次:

      • key: "seckill:sessions:" + startTime + "_" + endTime
      • value: [getPromotionSessionId() + "_" + getSkuId(), ...]:是一个List
    • 秒杀商品信息:

      • key: seckill:skus
      • value: HashMap
        • HashMap里面存着:
        • key: getPromotionSessionId() + "_" + getSkuId()
        • value: 商品的信息Json字符串
    • 分布式信号量RSemaphore:使用商品的数量作为分布式的信号量【限流】。只有请求里携带了秒杀商品的随机码,才可以减信号量,如果不带随机码直接减信号量的话,可能秒杀还没开始,有一些恶意请求,就把信号量就减了。

      • key: "seckill:stock:" + 商品随机码
      • value: getSeckillCount()
  • redis中的数据要有过期时间

  • 使用分布式锁+业务判断保证幂等性:锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到锁去执行业务,会拿到最新的状态去判断,做好判断就不会重复执行

    • 分布式锁:Redisson【集群环境下,让一个服务实例去执行上架即可】,加上分布式锁后,在业务方法里做好判断,即可保证幂等性
    • 业务判断:
      • 如果当前这个场次已经上架就不需要上架
      • 如果当前这个场次的这个商品的库存信息已经上架就不需要上架
  • 注意

    • 上架成功后,锁定对应数量的库存,一切都去Redis操作
    • 秒杀结束后,从Redis中根据销售情况,将没卖完的库存加回去

代码实现:

/**
 * 秒杀商品的定时上架;
 *     每天晚上3点;上架最近三天需要秒杀的商品。
 *     当天00:00:00  - 23:59:59
 *     明天00:00:00  - 23:59:59
 *     后天00:00:00  - 23:59:59
 */
@Slf4j
@Service
public class SeckillSkuScheduled {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    private static final String SECKILL_UPLOAD_LOCK = "seckill:upload:lock";

    @Async
    @Scheduled(cron = "*/3 * * * * ?")
//    @Scheduled(cron = "0 * * * * ?") //每分钟执行一次吧,上线后调整为每天晚上3点执行
//    @Scheduled(cron = "0 0 3 * * ?") //线上模式
    public void uploadSeckillSkuLatest3Days(){

        log.info("上架秒杀的商品信息...");

//        seckillService.uploadSeckillSkuLatest3Days();

        //TODO 写博客
        // 分布式锁【集群环境下,让一个服务实例去执行上架即可】
        // 加上分布式锁后,在业务方法里做好判断,即可保证幂等性:
        // 锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到锁去执行业务,会拿到最新的状态去判断,不会重复执行
        RLock lock = redissonClient.getLock(SECKILL_UPLOAD_LOCK);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        } finally {
            lock.unlock();
        }

    }

}
    private static final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private static final String SKUKILL_CACHE_PREFIX = "seckill:skus";
    private static final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码

     /**
     * TODO 上架成功后,锁定对应数量的库存,一切都去Redis操作
     *      秒杀结束后,从Redis中根据销售情况,将没卖完的库存加回去
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
        //扫描最近三天需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession();
        if (session.getCode() == 0) {
            //上架商品
            List<SeckillSesssionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSesssionsWithSkus>>() {
            });
            //缓存到redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }

    private void saveSessionInfos(List<SeckillSesssionsWithSkus> sesssions) {
        if (sesssions != null) sesssions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();

            //  如果当前这个场次已经上架就不需要上架
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            if (!redisTemplate.hasKey(key)) {
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId()).collect(Collectors.toList());
                //缓存活动信息
                redisTemplate.opsForList().leftPushAll(key, collect);
                //过期时间
                redisTemplate.expireAt(key, new Date(endTime));
            }
        });
    }

    private void saveSessionSkuInfos(List<SeckillSesssionsWithSkus> sesssions) {
        if (sesssions != null) sesssions.stream().forEach(sesssion -> {
            //准备hash操作
            List<SeckillSkuVo> relationSkus = sesssion.getRelationSkus();
            if (relationSkus != null && relationSkus.size() > 0) {

                BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                relationSkus.stream().forEach(seckillSkuVo -> {
                    //4、随机码?  seckill?skuId=1&=dadlajldj
                    final String token = UUID.randomUUID().toString().replace("-", "");

                    //  如果当前这个场次的这个商品的库存信息已经上架就不需要上架
                    final String cacheKey = seckillSkuVo.getPromotionSessionId() + "_" + seckillSkuVo.getSkuId();
                    if (!ops.hasKey(cacheKey)) {
                        //缓存商品
                        SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                        //1、sku的基本数据
                        R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                        if (skuInfo.getCode() == 0) {
                            SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                            });
                            redisTo.setSkuInfo(info);
                        }

                        //2、sku的秒杀信息
                        BeanUtils.copyProperties(seckillSkuVo, redisTo);

                        //3、设置上当前商品的秒杀时间信息
                        redisTo.setStartTime(sesssion.getStartTime().getTime());
                        redisTo.setEndTime(sesssion.getEndTime().getTime());

                        redisTo.setRandomCode(token);
                        String jsonString = JSON.toJSONString(redisTo);
                        //每个商品的过期时间不一样。所以,我们在获取当前商品秒杀信息的时候,做主动删除,代码在 getSkuSeckillInfo 方法里面
                        ops.put(cacheKey, jsonString);

                        //TODO 写博客
                        //  使用商品的数量作为分布式的信号量【限流】
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        //商品可以秒杀的数量作为信号量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                        //设置过期时间。
                        semaphore.expireAt(sesssion.getEndTime());
                    }
                });
            }
        });
    }

redis保存结果:

场次信息:

在这里插入图片描述

商品信息:

在这里插入图片描述

分布式信号量:

在这里插入图片描述

秒杀核心流程

流程1:(购物车流程)

在这里插入图片描述

流程2:【使用】

在这里插入图片描述

秒杀中消息队列的使用:

在这里插入图片描述

    /**
     * 秒杀
     *
     * @param killId 商品id: 2_1 【场次_skuId】
     * @param key    随机码
     * @param num    秒杀的数量
     */
    // TODO 上架秒杀商品的时候,每一个数据都有过期时间。√
    // TODO 秒杀后续的流程
    public String kill(String killId, String key, Integer num) {
        long s1 = System.currentTimeMillis();

        //获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        } else {
            SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
            //校验合法性
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            long currentTime = new Date().getTime();

            //1、校验时间的合法性
            if (currentTime >= startTime && currentTime <= endTime) {
                //2、校验随机码和商品id
                String randomCode = redis.getRandomCode();
                String promotionSessionId_skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if (randomCode.equals(key) && killId.equals(promotionSessionId_skuId)) {
                    //3、验证购物数量是否合理
                    if (num <= redis.getSeckillLimit()) {
                        //4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。  userId_promotionSessionId_skuId
                        //TODO 占位也可以放到一个Redis的Hash里面
                        MemberRespVo userInfo = LoginUserInterceptor.loginUser.get();
                        String redisKey = userInfo.getId() + "_" + promotionSessionId_skuId;
                        //占位得自动过期
                        long ttl = endTime - currentTime;
                        //SETNX
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功说明从来没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            boolean b = semaphore.tryAcquire(num);
//                            boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            if (b) {
                                //秒杀成功;
                                //快速下单。发送MQ消息
                                String timeId = IdWorker.getTimeId(); //订单号
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(userInfo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                //TODO  MQ监听消息处理
                                //      用户秒杀成功,信号量不用恢复【商品已经卖出去了】
                                //      用户如果超时未支付或者取消订单,还得去恢复信号量,让其他人能参与秒杀
                                long s2 = System.currentTimeMillis();
                                log.info("耗时...{}", (s2 - s1));

                                return timeId;
                            }
                            return null;
                        } else {
                            //说明已经买过了
                            return null;
                        }
                    }
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }

        return null;
    }

Java8的日期API简单使用

        LocalDate now = LocalDate.now();
        LocalDate dayAfter2 = now.plusDays(2);
        System.out.println("now : " + now);
        System.out.println("两天后 : " + dayAfter2);

        LocalTime min = LocalTime.MIN;
        LocalTime max = LocalTime.MAX;
        System.out.println("最小时间:" + min);
        System.out.println("最大时间:" + max);

//        最近三天开始的活动:2024-07-17 00:00:00  到  2024-07-19 23:59:59
        LocalDateTime start = LocalDateTime.of(now, min);
        LocalDateTime end = LocalDateTime.of(dayAfter2, max);
        System.out.println("开始时间:" + start);
        System.out.println("结束时间:" + end);
        System.out.println("结束时间:" + start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println("结束时间:" + end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该项目是采用目前比较流行的SpringBoot/SpringCloud构建微服务电商项目,项目叫 《果然新鲜》,实现一套串联的微服务电商项目。完全符合一线城市微服务电商的需求,对学习微服务电商架构,有非常大的帮助,该项目涵盖从微服务电商需求讨论、数据库设计、技术选型、互联网安全架构、整合SpringCloud各自组件、分布式基础设施等实现一套完整的微服务解决方案。 项目使用分布式微服务框架,涉及后台管理员服务、地址服务、物流服务、广告服务、商品服务、商品类别服务、品牌服务、订单服务 、购物车服务、首页频道服务、公告服务、留言服务、搜索服务、会员服务等。  系统架构图   SpringBoot+SpringCloud+SSM构建微服务电商项目使用SpringCloud Eureka作为注册中心,实现服务治理使用Zuul网关框架管理服务请求入口使用Ribbon实现本地负载均衡器和Feign HTTP客户端调用工具使用Hystrix服务保护框架(服务降级、隔离、熔断、限流)使用消息总线Stream RabbitMQ和 Kafka微服务API接口安全控制和单点登录系统CAS+JWT+OAuth2.0分布式基础设施构建分布式任务调度平台XXL-JOB分布式日志采集系统ELK分布式事务解决方案LCN分布式锁解决方案Zookeeper、Redis分布式配置中心(携程Apollo)高并发分布式全局ID生成(雪花算法)分布式Session框架Spring-Session分布式服务追踪与调用链Zipkin项目运营与部署环境分布式设施环境,统一采用Docker安装使用jenkins+docker+k8s实现自动部署微服务API管理ApiSwagger使用GitLab代码管理(GitHub  GitEE)统一采用第三方云数据库使用七牛云服务器对静态资源实现加速 开发环境要求JDK统一要求:JDK1.8Maven统一管理依赖 统一采用Docker环境部署编码统一采用UTF-8开发工具IDEA 或者 Eclipse 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值