第九章 流量削峰技术

摘要:
会对交易系统造成无关联负载解决:引入秒杀令牌,将秒杀下单逻辑放到生成令牌这里,这样方便以后分开部署。

问题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挂了,队列就没了

免责声明:文章转载自《第九章 流量削峰技术》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇使用jquery获取url以及jquery获取url参数的方法报错[Vue warn]:Invalid prop:custom validator check failed for prop "percentage"下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

记一次阿里云负载测试

环境: 服务器:主服务器A,备服务器B(主服务器配置和带宽都大于备服务器器) 业务:tomcat 端口:443,80 协议:https 域名:www.danny.com 证书:ssl证书,域名绑定了ssl证书并都配置安装在AB服务器中 要求:主要使用A服务器跑业务,B服务器只有在A服务器不可用时才接受访问流量 阿里云负载均衡后台转发服务器组选择: 1.后端...

xray与burp联动被动扫描

0X00xray建立监听 在实际测试过程中,除了被动扫描,也时常需要手工测试。这里使用 Burp 的原生功能与 xray 建立起一个多层代理,让流量从 Burp 转发到 xray 中。 首先 xray 建立起 webscan 的监听 .xray_windows_amd64.exe webscan --listen 127.0.0.1:7777 --html...

华为的快服务智慧平台是牛皮还是牛B?

华为快服务智慧平台是牛皮还是牛B?   来到快服务论坛专区的老铁们想必对快服务有一定的了解,那么作为华为快服务统一接入分发核心的华为快服务智慧平台是怎样的存在呢?想必带着眼睛阅读的小伙伴都已经看出来了,我都说了是“核心”(第一眼没看出来的朋友也别打我,我就是单纯想皮一下,嘻嘻)。   不过话说回来,相比于快服务而言,老铁们可能对于快服务智慧平台并不是非常...

vSAN 2节点集群设计与配置

对于分支结构或者小型企业,2节点vSAN性价比非常高,但是配置时需要额外注意见证节点的流量。 1、2节点vSAN架构设计图 2个数据节点不需要万兆交换机,可将万兆网口直接对接。 2、见证节点流量设计 2节点vSAN要求数据节点与见证节点vSAN Kernel网络必须能够通讯。 因该vSAN架构中2个数据节点的VSAN网络是直连,默认情况下无法和见证节点的...

js埋点(转载)

页面埋点的作用,其实就是用于流量分析。而流量的意思,包含了很多:页面浏览数(PV)、独立访问者数量(UV)、IP、页面停留时间、页面操作时间、页面访问次数、按钮点击次数、文件下载次数等。而流量分析又有什么用处: 1、提高网站的转化率 根据页面埋点可得到一些重要信息,它告诉你用户对网站的反应,以及如何提高网站流量、改进网站性能,了解用户访问网站的行为,为更好...

网络流(一)基础知识篇

传送门: 网络流(一)基础知识篇 网络流(二)最大流的增广路算法 网络流(三)最大流最小割定理 网络流(四)dinic算法 网络流(五)有上下限的最大流 网络流(六)最小费用最大流问题  转载自:https://blog.csdn.net/txl199106/article/details/64441994 网络流入门 基本概念(从书上摘抄,可以直接...