• 第九章 流量削峰技术


    问题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);
        }
    

     

    问题五:队列保存本地还是分布式好

    本地将队列维护在内存中,性能高,但是不能负载均衡,每个机器有20个线程不能平均使用

    分布式:将队列保存在redis中,有网络响应时间,速度慢。单点的若redis挂了,队列就没了

  • 相关阅读:
    redis下载安装及php配置redis
    php--小数点问题
    php--0与空的判断
    php--判断是否是手机端
    php--ip的处理
    mysql--sql_mode报错整理
    mysql-建表、添加字段、修改字段、添加索引SQL语句写法
    Python-多任务复制文件夹
    Python学习笔记(十一)——赋值、深拷贝与浅拷贝
    Python学习笔记(十)—JSON格式的处理
  • 原文地址:https://www.cnblogs.com/t96fxi/p/12099335.html
Copyright © 2020-2023  润新知