第九章 流量削峰技术

问题1:下单的请求可以通过脚本不停的刷造成黄牛还有对服务器的压力

可以在秒杀令牌颁发的过程中做限购 比如一个用户只能拿一个令牌等逻辑

问题2:秒杀下单逻辑和秒杀下单接口写在一起,强冗余。即使活动不开始,也可以作为普通商品下单。会对交易系统造成无关联负载

解决:引入秒杀令牌,将秒杀下单逻辑放到生成令牌这里,这样方便以后分开部署。

1.使用令牌来避免大量的访问来下单

秒杀令牌来管风控和验证,避免大流量的用户来进行下单操作

生成令牌一般比库存多一些,例如两倍

先调用/generatePromoToken, 生成promoToken,然后携带promoToken去下单/createorder

(1)生成秒杀令牌

public String generateSecondKillTocken(Integer itemId, Integer userId, Integer promoId, Integer amount) {
		//1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if(itemModel == null){
            return null;
        }
        
        UserModel userModel = userService.getUserByIdInCache(userId);
        if(userModel == null){
            return null;
        }
        if(amount <= 0 || amount > 99){
            return null;
        }

        //校验活动信息
        if(promoId != null){
            //(1)校验对应活动是否存在这个适用商品
            if(promoId.intValue() != itemModel.getPromoModel().getId()){
            	return null;
                //(2)校验活动是否正在进行中
            }else if(itemModel.getPromoModel().getStatus().intValue() != 2) {
            	return null;
            }
        }
        // 生成抢购token,并存入reids
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set("promo_token_"+promoId+"_"+itemId+"_"+userId, token);
        redisTemplate.expire("promo_token_"+promoId+"_"+itemId+"_"+userId, 5, TimeUnit.MINUTES);
        return token;
	}

(2)下单验证令牌

//封装下单请求
    @RequestMapping(value = "/createorder",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name="itemId")Integer itemId,
                                        @RequestParam(name="amount")Integer amount,
                                        @RequestParam(name="promoId",required = false)Integer promoId,
                                        @RequestParam(name="token",required = false)String token,
                                        @RequestParam(name="promoToken",required = false)String promoToken) throws BusinessException {

    	UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
    	// 秒杀令牌校验,与redis中的值比较
    	if(promoId!=null) {
    		if(promoToken!=null) {
    			String inRedisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_"+promoId+"_"+itemId+"_"+userModel.getId());
    			if(!promoToken.equals(inRedisPromoToken)) {
    				throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌验证失败");
    			}
    		} else {
    			throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌验证失败");
    		}
    	}
    	
        // 添加商品库存流水init状态
        String stockLogId = itemService.initStockLog(itemId, amount);
        
        //OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId,promoId,amount);
        // 事务型的消息驱动下单,同时根据回调状态来决定发送还是回滚消息
        boolean mqResult = mqProducer.transactionAsyncReduceStock(userModel.getId(),itemId,promoId,amount, stockLogId);
        if(!mqResult) {
        	throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下单失败");
        }
        return CommonReturnType.create(null);
    }

问题3:秒杀令牌的实现有缺陷,可以无限制的生成,这样如果有一亿用户过来,生成影响系统性能,而且一个令牌也不能抢到商品

解决:引入秒杀大闸,根据库存来颁发对应的数量的令牌,控制大闸流量

(1)在发布活动时,库存保存到redis中时,将大闸数量也保存到redis

public void publishPromo(Integer promoId) {
		。。。。。。。。。。。。。。。。。
		//将库存同步到redis中
		redisTemplate.opsForValue().getAndSet("promo_item_stock_"+promoDO.getItemId(), itemModel.getStock());
		//将秒杀大闸数量保存到redis
		redisTemplate.opsForValue().set("promo_door_count_"+promoId, itemModel.getStock().intValue()*5);
	}

(2)生成令牌前先校验秒杀大闸数量是否还有  

@Override
	public String generateSecondKillTocken(Integer itemId, Integer userId, Integer promoId, Integer amount) {
		// 校验库存是否售罄
        if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)) {
        	return null;
        }
		//1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
        。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
        // 获取秒杀大闸数量
        long result = redisTemplate.opsForValue().increment("promo_door_count_"+promoId, -1);
        if(result<0) {
        	return null;
        }
        // 生成抢购token,并存入reids
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set("promo_token_"+promoId+"_"+itemId+"_"+userId, token);
        redisTemplate.expire("promo_token_"+promoId+"_"+itemId+"_"+userId, 5, TimeUnit.MINUTES);
        return token;
	}

问题4:令牌对浪涌流量的涌入无法应对,比如库存本身就非常大。另外多库存,多商品的令牌限制能力弱

解决:引入队列泄洪,将任务提交给线程池,线程池中可执行线程沾满后会将任务放到等待队列中,这样做就等于是限制了用户并发的流量,使得其在线程池的等待队列中排队处理。然后future的使用是为了让前端用户在调用controller后可以同步的获得执行的结果

1排队有时比并发更快,如果出现锁的等待,线程会退出。CPU调度另一个线程,CPU耗损上下文切换

比如:redis是单线程的,但是很快。因为redis是内存操作,且单线程上没有线程切换开销

private ExecutorService executorService;
    
    @PostConstruct
    public void init() {
    	executorService = Executors.newFixedThreadPool(20);
    }

//封装下单请求
    @RequestMapping(value = "/createorder",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name="itemId")Integer itemId,
                                        @RequestParam(name="amount")Integer amount,
                                        @RequestParam(name="promoId",required = false)Integer promoId,
                                        @RequestParam(name="token",required = false)String token,
                                        @RequestParam(name="promoToken",required = false)String promoToken) throws BusinessException {
    	
		。。。。。。。。。。。。。。。。。。。。。。。。。。。。
    	// 同步调用线程池的submit方法
    	// 拥塞窗口为20的等待队列,用队列来泄洪,超过20的队列要等待
    	Future<Object> future = executorService.submit(new Callable<Object>() {

			@Override
			public Object call() throws Exception {
				// 添加商品库存流水init状态
		        String stockLogId = itemService.initStockLog(itemId, amount);
		        
		        //OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId,promoId,amount);
		        // 事务型的消息驱动下单,同时根据回调状态来决定发送还是回滚消息
		        boolean mqResult = mqProducer.transactionAsyncReduceStock(userModel.getId(),itemId,promoId,amount, stockLogId);
		        if(!mqResult) {
		        	throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下单失败");
		        }
				return null;
			}
    		
		});
    	
    	try {
    		// get方法获取执行结果,该方法会阻塞直到任务返回结果。
			future.get();
		} catch (InterruptedException | ExecutionException e) {
			throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
		}
        
        return CommonReturnType.create(null);
    }

  

猜你喜欢

转载自www.cnblogs.com/t96fxi/p/12099335.html