聚焦Java性能优化 打造亿级流量秒杀系统【学习笔记】08_流量削峰技术
本章目标
- 掌握秒杀令牌的原理和使用方式
- 掌握秒杀大闸的原理和使用方式
- 掌握队列泄洪的原理和使用方式
- 秒杀下单接口会被脚本不停的刷
- 秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高
- 秒杀验证逻辑复杂,对交易系统产生无关联负载
- 秒杀接口需要依靠令牌才能进入
- 秒杀的令牌由秒杀活动模块负责生成
- 秒杀活动模块对秒杀令牌生成全权处理,逻辑收口
- 秒杀下单前需要先获得秒杀令牌
PromoService接口上实现generateSecondKillToken秒杀令牌生成函数
//生成秒杀用的令牌
String generateSecondKillToken(Integer promoId,Integer itemId,Integer userId);
PromoServiceImpl
public String generateSecondKillToken(Integer promoId,Integer itemId,Integer userId) {PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
//promoDo(dataObject) -> PromoModel
PromoModel promoModel = convertFromDataObject(promoDO);
if(promoModel == null) {
return null;
}
//判断当前时间是否秒杀活动即将开始或正在进行
DateTime now = new DateTime();
if(promoModel.getStartDate().isAfterNow()) {
promoModel.setStatus(1);
}else if(promoModel.getEndDate().isBeforeNow()) {
promoModel.setStatus(3);
}else {
promoModel.setStatus(2);
}
//判断活动是否正在进行
if(promoModel.getStatus().intValue()!=2){
return null;
}//判断item信息是否存在
ItemModel itemModel = itemService.getItemByIdInCache(itemId);
if(itemModel == null) {
return null;
}
//判断用户信息是否存在
UserModel userModel = userService.getUserByIdInCache(userId);
if(userModel == null) {
return null;
}
//生成token并且存入redis设置5分组有效期
String token = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,token);
redisTemplate.expire("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,5, TimeUnit.MINUTES);
return token;
}
OrderController
//生成秒杀令牌
@RequestMapping(value = "https://www.it610.com/generatetoken",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType generatetoken(@RequestParam(name="itemId")Integer itemId,
@RequestParam(name="promoId")Integer promoId) throws BusinessException {
//根据token获取用户信息
String token = httpServletRequest.getParameterMap().get("token")[0];
if(StringUtils.isEmpty(token)){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
}
//获取用户的登陆信息
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if(userModel == null){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
}
//获取秒杀访问令牌
String promoToken = promoService.generateSecondKillToken(promoId,itemId,userModel.getId());
if(promoToken == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"生成令牌失败");
}
//返回对应的结果
return CommonReturnType.create(promoToken);
}
**抛缺陷:**秒杀令牌只要活动一开始就无限制生成,影响系统性能
9-4 秒杀大闸原理及实现
- 依靠秒杀令牌的授权原理定制化发牌逻辑,做大闸功能
- 根据秒杀商品初始化库存颁发对应数量令牌,控制大闸流量
- 用户风控策略前置到秒杀令牌发放中
- 库存售罄判断前置到秒杀令牌发放中
//将大闸限制数字设置到redis内
redisTemplate.opsForValue().set("promo_door_count_"+promoId,itemModel.getStock().intValue()*5);
抛缺陷:
- 浪涌流量涌入后系统无法应对
- 多库存,多商品等令牌限制能力弱
- 排队有些时候比并发更高效(例如redis单线程模型,innodb mutex key等)
- 依靠排队去限制并发流量
- 依靠排队和下游拥塞窗口程度调整队列释放流量大小
- 支付宝银行网关队列举例
队列泄洪代码实现
OrderController
private ExecutorService executorService;
@PostConstruct
public void init(){
//定义一个只有20个可工作线程的线程池
executorService = Executors.newFixedThreadPool(20);
}
//同步调用线程池的submit方法
//拥塞窗口为20的等待队列,用来队列化泄洪
Future
9-7 本地或分布式
- 本地:将队列维护在本地内存中
- 分布式:将队列设置到redis内
本地队列的好处就是完全维护在内存当中的,因此其对应的没有网络请求的消耗,只要JVM不挂,应用是存活的,那本地队列的功能就不会失效。因此企业级开发应用还是推荐使用本地队列,本地队列的性能以及高可用性对应的应用性和广泛性。可以使用外部的分布式集中队列,当外部集中队列不可用时或者请求时间超时,可以采用降级的策略,切回本地的内存队列。
—————————————————————————————————
【本课程已整理完毕】
【聚焦Java性能优化 打造亿级流量秒杀系统【学习笔记】08_流量削峰技术】01_电商秒杀商品回顾
02_云端部署
03_分布式扩展
04_查询性能优化技术之多级缓存
05_查询性能优化技术之页面静态化
06_交易性能优化技术之缓存库存
07_交易性能优化技术之事务型消息
08_流量削峰技术
09_防刷限流技术
10_课程总结
—————————————————————————————————
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 事件代理
- Java|Java OpenCV图像处理之SIFT角点检测详解
- java中如何实现重建二叉树
- 数组常用方法一
- 【Hadoop踩雷】Mac下安装Hadoop3以及Java版本问题
- Java|Java基础——数组
- RxJava|RxJava 在Android项目中的使用(一)
- java之static、static|java之static、static final、final的区别与应用
- Java基础-高级特性-枚举实现状态机