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