商城秒杀实现

本文详细介绍了一种针对秒杀业务的限流策略,包括前端、nginx、网关和代码层面的实现,并重点讲解了如何利用Redis进行缓存,如活动信息、商品信息的存储,以及秒杀商品的实时查询。通过实例展示了如何结合Spring Boot和Redis进行高效秒杀操作的优化。
摘要由CSDN通过智能技术生成

秒杀业务

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

限流方式

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

1.将三天之内参与秒杀的商品存入redis

private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

private final String SECKILL_CACHE_PREFIX = "seckill:skus";

private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

@Override
    public void uploadSeckillSkulatest3Days() {
        //1、扫描最近三天需要参与秒杀的活动
        //计算最近三天
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between(
                "start_time", startTime(), endTime()));
        if(list != null && list.size() > 0){
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                List<SeckillSkuRelationEntity> relationEntities =
                        relationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id",
                                id));
                session.setRelationSkus(relationEntities);
                return session;
            }).collect(Collectors.toList());
            //缓存到redis
            //1、缓存活动信息
            saveSessionInfos(collect);
            //2、缓存活动关联的商品信息
            saveSessionSkuInfos(collect);
        }
    }

	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;
    }
	private String endTime(){
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalDateTime end = LocalDateTime.of(localDate, LocalTime.MAX);
        String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

2.将活动信息存入redis

private void saveSessionInfos(List<SeckillSessionEntity> collect){
   collect.stream().forEach(session->{
        //获取当前活动的开始时间和结束时间的时间戳
        long startTime = session.getStartTime().getTime();
        long endTime = session.getEndTime().getTime();

        //缓存key
        String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
        //判断redis中是否有此活动信息key
        Boolean hasKey = redisTemplate.hasKey(key);
        if(!hasKey){
            List<String> skuIds = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());

            //缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,skuIds);
        }
    });

}

3.将活动关联的商品存入redis

private void saveSessionSkuInfos(List<SeckillSessionEntity> collect){
   collect.stream().forEach(session->{
        //准备hash操作,绑定hash
        BoundHashOperations<String, Object, Object> operations =
                redisTemplate.boundHashOps(SECKILL_CACHE_PREFIX);
        session.getRelationSkus().forEach(seckillSkuVo->{
            //生成随机码
            String token = UUID.randomUUID().toString().replaceAll("-", "");
            String redisKey = seckillSkuVo.getPromotionSessionId().toString() + seckillSkuVo.getSkuId().toString();
            if(!operations.hasKey(redisKey)){
                //缓存商品信息redisTo
                SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();

                //1.查询sku的基本信息,调用远程服务
                int skuId = seckillSkuVo.getSkuId().intValue();
                R info = drugFeignService.info(skuId);
                int code = (int) info.get("code");
                if(code == 0){
                    String jsonString = JSON.toJSONString(info.get("drug"));
                    SkuInfoVo skuInfoVo = JSON.parseObject(jsonString, SkuInfoVo.class);
                    //设置商品信息
                    redisTo.setSkuInfo(skuInfoVo);
                }

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

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

                //4.设置商品的随机码
                redisTo.setRandomCode(token);

                //序列化json格式存入redis
                String seckillValue = JSON.toJSONString(redisTo);
                operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                //5、使用库存作为分布式Redisson信号量(限流)
                //使用库存作为分布式信号量
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
            };
        });
    });
}

4.查询参与秒杀的商品

@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    //1、确定当前时间属于哪个秒杀场次
    long time = new Date().getTime();

    try(Entry entry = SphU.entry("seckillSkus")){
        Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for (String key : keys) {
            //seckill:sessions:1624233600000_1624240800000
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            //获取存入Redis商品的开始时间
            long start = Long.parseLong(s[0]);
            //获取存入Redis商品的结束时间
            long end = Long.parseLong(s[1]);
            if(time>=start && time<=end){
                //2、获取这个秒杀场次所有的商品信息
                List<String> range = redisTemplate.opsForList().range(key,-100, 100);
                BoundHashOperations<String, String, Object> hashOps =
                        redisTemplate.boundHashOps(SECKILL_CACHE_PREFIX);
                List<Object> list = hashOps.multiGet(range);
                if(list != null && list.size()>0){
                    List<SeckillSkuRedisTo> collect = list.stream().map((item) -> {
                        SeckillSkuRedisTo redisTo = JSON.parseObject(item.toString(),
                                SeckillSkuRedisTo.class);
//                        redisTo.setRandomCode(null);
                        return redisTo;
                    }).collect(Collectors.toList());
                    return collect;
                }
                break;
            }
        }
    }catch (BlockException e){
        log.error("资源被限流,{}",e.getMessage());
    }

    return null;
}

public List<SeckillSkuRedisTo> blockHandler(BlockException e){
   log.error("getCurrentSeckillSkusResource被限流了...");
    return null;
}

5.开始秒杀

@Override
public String seckill(String killId, String key, Integer num) throws InterruptedException {

    //TODO 获取当前用户信息
    //1、获取当前秒杀商品的详细信息从Redis中获取
    BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CACHE_PREFIX);
    String skuInfoValue =  hashOps.get(killId);
    if(skuInfoValue.isEmpty()){
        return null;
    }

    //合法性校验
    SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
    Long startTime = redisTo.getStartTime();
    Long endTime = redisTo.getEndTime();
    long currentTime = new Date().getTime();
    //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
    if(currentTime>=startTime && currentTime<=endTime){
        //2、效验随机码和商品id
        String randomCode = redisTo.getRandomCode();
        String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
        if(randomCode.equals(key) && killId.equals(skuId)){
            //3、验证购物数量是否合理和库存量是否充足
            Integer seckillLimit = redisTo.getSeckillLimit();

            //获取信号量
            //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
            //SETNX 原子性处理
//                    String redisKey = user.getId() + "-" + skuId;
            String redisKey = "" + 1 + "-" + skuId;

            String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
            Integer count = Integer.valueOf(seckillCount);
            //判断信号量是否大于0,并且买的数量不能超过库存
            if(count>0 && num<=seckillLimit && count>num){                    //设置自动过期(活动结束时间-当前时间)
                long ttl = endTime - currentTime;
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, String.valueOf(num), ttl,
                        TimeUnit.MILLISECONDS);
                if(aBoolean){
                    //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                    //TODO 秒杀成功,快速下单
                    boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                    //保证Redis中还有商品库存
                    if(semaphoreCount){
                        //创建订单号和订单信息发送给MQ
                        // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                        String timeId = IdWorker.getTimeId();
                        rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order","用户1秒杀产品成功");

                        return timeId;
                    }
                }
            }
        }
    }
    return null;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值