赞
踩
秒杀大家都不陌生,而且是电商项目必备的一个技能点。
但是真正的秒杀服务是非常复杂的,秒杀具有瞬间高并发的特点,所以解决瞬间高并发的问题,就可以解决秒杀的问题。
今天就将秒杀系统完整的实现分解开,一起研究一下吧。
(有问题还请指正)
秒杀服务是有很大风险的,一不小心就会造成服务宕机或者一瞬间占用大量服务器资源,所以秒杀服务必须独立部署,而且秒杀服务只做秒杀功能。
防止恶意攻击,防止有人模拟秒杀请求造成服务器更大的压力;
防止链接暴露,防止工作人员提前秒杀商品。
秒杀系统读多写少,我们可以先将库存总数预热,存入redis中,使用信号量来控制秒杀请求的数量。
使用nginx做好动静分离,保证静态资源直接能够请求到,避免占用后端资源。(现在基本都是前后端分离项目,此处可忽略)
识别非法攻击的请求进行拦截,可以从网关层拦截,判断用户是否登录。
使用各种手段,将流量分担到更大宽度的时间点。比如验证码、加入购物车,多加几步操作。
前端限流+后端限流。
限制每秒钟只能点击一次;限制总量;
后端快速失败、降级运行、熔断机制防止雪崩。
秒杀成功的所有商品,放入消息队列中,然后消费端慢慢创建订单等等逻辑。
使用sentinel进行限流
详解sentinel:分布式系统的流量防卫兵
使用rockerMQ或者rabbitMQ进行削峰。
使用定时任务,提前将商品信息、商品随机码(防止恶意攻击)、商品库存等信息存入redis。
伪代码:
/** * 缓存秒杀活动所关联的商品信息 */ private void saveProductInfo(List<Product> products) { products.stream().forEach(products-> { //准备hash操作,绑定hash BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); products.getRelationSkus().stream().forEach(seckillSkuVo -> { //生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(); if (!operations.hasKey(redisKey)) { // 防止重复添加 //缓存我们商品信息 SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); //1、先查询商品的基本信息,调用远程服务 R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.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、设置商品的随机码(防止恶意攻击) 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()); } }); }); }
/** * 商品进行秒杀(秒杀开始) * @param killId * @param key * @param num * @return */ @GetMapping(value = "/kill") public String seckill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num, Model model) { String orderSn = null; try { //1、判断是否登录 orderSn = seckillService.kill(killId,key,num); model.addAttribute("orderSn",orderSn); } catch (Exception e) { e.printStackTrace(); } return "success"; }
/** * 当前商品进行秒杀(秒杀开始) * @param killId * @param key * @param num * @return */ @Override public String kill(String killId, String key, Integer num) throws InterruptedException { long s1 = System.currentTimeMillis(); //获取当前用户的信息 MemberResponseVo user = LoginUserInterceptor.loginUser.get(); //1、获取当前秒杀商品的详细信息从Redis中获取 BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); String skuInfoValue = hashOps.get(killId); if (StringUtils.isEmpty(skuInfoValue)) { return null; } //(合法性效验) SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class); Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); long currentTime = System.currentTimeMillis(); //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性) 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(); //获取信号量 String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode); Integer count = Integer.valueOf(seckillCount); //判断信号量是否大于0,并且买的数量不能超过库存 if (count > 0 && num <= seckillLimit && count > num ) { //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId //SETNX 原子性处理 String redisKey = user.getId() + "-" + skuId; //设置自动过期(活动结束时间-当前时间) Long ttl = endTime - currentTime; Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), 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(); SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setMemberId(user.getId()); orderTo.setNum(num); orderTo.setPromotionSessionId(redisTo.getPromotionSessionId()); orderTo.setSkuId(redisTo.getSkuId()); orderTo.setSeckillPrice(redisTo.getSeckillPrice()); rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo); long s2 = System.currentTimeMillis(); log.info("耗时..." + (s2 - s1)); return timeId; } } } } } long s3 = System.currentTimeMillis(); log.info("耗时..." + (s3 - s1)); return null; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。