赞
踩
秒杀令牌的原理
秒杀令牌的作用
秒杀令牌实现代码
OrderController.java
// 生成秒杀令牌 @PostMapping(value = "/generatetoken", consumes = {CONTENT_TYPE_FORMED}) public CommonReturnType generatetoken(@RequestParam("itemId") Integer itemId, @RequestParam("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); } // 下单 @PostMapping(value = "/createorder", consumes = {CONTENT_TYPE_FORMED}) public CommonReturnType createOrder(@RequestParam("itemId") Integer itemId, @RequestParam(value = "promoId", required = false) Integer promoId, @RequestParam("amount") Integer amount, @RequestParam(value = "promoToken", required = false) String promoToken) throws BusinessException { // 使用token的方法获取用户信息 String token = httpServletRequest.getParameterMap().get("token")[0]; if(StringUtils.isEmpty(token)){ throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单"); } UserModel userModel = (UserModel)redisTemplate.opsForValue().get(token); if(userModel == null){ throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单"); } // 校验秒杀令牌是否正确 if(promoId != null){ String inRedisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_userid_" + userModel.getId() + "_itemid_" + itemId); if(inRedisPromoToken == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败"); } if(!org.apache.commons.lang3.StringUtils.equals(inRedisPromoToken, promoToken)){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败"); } } // 判断库存是否已售罄,若对应的售罄key存在,则直接返回下单失败 if(redisTemplate.hasKey("promo_item_stock_invalid_" + itemId)){ throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } // 初始化库存流水(id、itemid、amount、status存入数据库流水表) String stockLogId = itemService.initStockLog(itemId, amount); // 完成对应的下单事务型消息机制 boolean orderState = mqProducer.transactionAsyncReduceStockAndAddSales(userModel.getId(), itemId, promoId, amount, stockLogId); // 下单失败 if(!orderState){ throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败"); } return CommonReturnType.create(null); }
PromoServiceImpl.java
@Override public String generateSecondKillToken(Integer promoId, Integer itemId, Integer userId) { // 校验是否有商品秒杀活动 PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId); PromoModel promoModel = convertFromDataObject(promoDO); if(promoModel == null){ return null; } // 校验秒杀活动是否开始 if(promoModel.getStartDate().isAfterNow()){ promoModel.setStatus(1); }else if(promoModel.getEndDate().isBeforeNow()){ promoModel.setStatus(3); }else{ promoModel.setStatus(2); } // 1表示秒杀未开始,2表示进行中,3表示已结束。如果秒杀活动不正在进行中,则不生成秒杀令牌 if(promoModel.getStatus() != 2){ return null; } // 校验商品信息是否存在 ItemModel itemModel = itemService.getItemByIdInCache(itemId); if(itemModel == null){ return null; } // 校验用户信息是否存在 UserModel userModel = userService.getUserByIdInCache(userId); if(userModel == null){ return null; } // 生成秒杀令牌并存入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; }
OrderServiceImpl.java
@Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount, String stockLogId) throws BusinessException { // 用户信息、秒杀活动信息、商品信息等放在生成令牌处校验 ItemModel itemModel = itemService.getItemByIdInCache(itemId); if(itemModel == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } if(amount <= 0 || amount > 99){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不正确"); } // 2.落单减库存 boolean result = itemService.decreaseStock(itemId, amount); if(!result){ throw new BusinessException((EmBusinessError.STOCK_NOT_ENOUGH)); } // 3.订单入库 OrderModel orderModel = new OrderModel(); orderModel.setItemId(itemId); orderModel.setUserId(userId); orderModel.setAmount(amount); if(promoId != null){ orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice()); }else{ orderModel.setItemPrice(itemModel.getPrice()); } orderModel.setPromoId(promoId); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount))); // 生成交易流水号(订单号) orderModel.setId(generateOrderNo()); OrderDO orderDO = convertFromOrderModel(orderModel); orderDOMapper.insertSelective(orderDO); // 4. 商品销量增加,先增加到缓存中,然后通过rocketmq事务消息机制发送消息 itemService.increaseSales(itemId, amount); // 设置库存流水状态为成功 StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId); if(stockLogDO == null){ throw new BusinessException(EmBusinessError.UNKNOWN_ERROR); } // status为2表示扣减库存成功 stockLogDO.setStatus(2); stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO); // 5. 返回前端 return orderModel; }
前端getitem.html下单界面ajax代码
// jQuery(document).ready()这个方法在dom载入就绪时对其进行操纵并调用执行它所绑定的函数。 jQuery(document).ready(function(){ $("#createorder").on("click", function () { var token = window.localStorage["token"]; if(token == null){ alert("没有登陆,不能下单"); window.location.href="login.html"; return false; } $.ajax({ type:"POST", contentType: "application/x-www-form-urlencoded", url:"http://" + g_host + "/order/generatetoken?token=" + token, data:{ "itemId":g_itemVO.id, "promoId":g_itemVO.promoId }, xhrFields:{withCredentials:true}, success:function (data) { if(data.status == "success"){ var promoToken = data.data; $.ajax({ type:"POST", contentType: "application/x-www-form-urlencoded", url:"http://" + g_host + "/order/createorder?token=" + token, data:{ "itemId":g_itemVO.id, "promoId":g_itemVO.promoId, "amount":1, "promoToken":promoToken }, xhrFields:{withCredentials:true}, success:function (data) { if(data.status == "success"){ alert("下单成功"); window.location.reload(); }else{ alert("下单失败,原因为"+data.data.errMsg); if(data.data.errCode == 20003){ window.location.href="login.html"; } } }, error:function (data) { alert("下单失败,原因为"+data.responseText); } }); }else{ alert("获取令牌失败,原因为"+data.data.errMsg); if(data.data.errCode == 20003){ window.location.href="login.html"; } } }, error:function (data) { alert("获取令牌失败,原因为"+data.responseText); } }); }); initView(); });
目前存在的问题
秒杀令牌只要活动一开始就可以无限制生成,影响系统性能.
比如有100件商品,十万用户抢,每个用户点一下就生成一个秒杀令牌,只有100件商品,生成海量的令牌只会影响系统性能.
秒杀大闸原理
秒杀大闸代码实现
PromoServiceImpl.java
// 发布促销活动
public void publishpromo(Integer promoId) {
// 通过活动id获取活动
PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
if(promoDO.getItemId() == null || promoDO.getItemId() == 0){
return;
}
ItemModel itemModel = itemService.getItemById(promoDO.getItemId());
// 将库存同步到redis中
redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock());
// 将销量同步到redis中
redisTemplate.opsForValue().set("promo_item_sales_" + itemModel.getId(), itemModel.getSales());
// 将秒杀大闸的限制数字设置到redis中,并设置大闸的限制数量为库存的5倍
redisTemplate.opsForValue().set("promo_door_count_" + promoId, itemModel.getStock() * 5);
}
PromoServiceImpl.java
@Override public String generateSecondKillToken(Integer promoId, Integer itemId, Integer userId){ // 判断库存是否已售罄,若对应的售罄key存在,则直接返回下单失败,之前在下订单方法中,现在前置到获取令牌方法中 if(redisTemplate.hasKey("promo_item_stock_invalid_" + itemId)){ return null; } // 校验是否有商品秒杀活动 PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId); PromoModel promoModel = convertFromDataObject(promoDO); if(promoModel == null){ return null; } // 校验秒杀活动是否开始 if(promoModel.getStartDate().isAfterNow()){ promoModel.setStatus(1); }else if(promoModel.getEndDate().isBeforeNow()){ promoModel.setStatus(3); }else{ promoModel.setStatus(2); } // 1表示秒杀未开始,2表示进行中,3表示已结束。如果秒杀活动不正在进行中,则不生成秒杀令牌 if(promoModel.getStatus() != 2){ return null; } // 校验商品信息是否存在 ItemModel itemModel = itemService.getItemByIdInCache(itemId); if(itemModel == null){ return null; } // 校验用户信息是否存在 UserModel userModel = userService.getUserByIdInCache(userId); if(userModel == null){ return null; } // 获取秒杀大闸的count数量 long result = redisTemplate.opsForValue().increment("promo_door_count_" + promoId, -1); if(result <= 0){ return null; } // 生成秒杀令牌并存入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.java
private ExecutorService executorService; @PostConstruct public void init(){ // newFixedThreadPool(): 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 // 开辟20个线程数的线程池,同一时间只能处理20个请求,其他的请求放在队列中等待,用来队列化泄洪 executorService = Executors.newFixedThreadPool(20); } @PostMapping(value = "/createorder", consumes = {CONTENT_TYPE_FORMED}) public CommonReturnType createOrder(@RequestParam("itemId") Integer itemId, @RequestParam(value = "promoId", required = false) Integer promoId, @RequestParam("amount") Integer amount, @RequestParam(value = "promoToken", required = false) String promoToken) throws BusinessException { // 使用token的方法获取用户信息 String token = httpServletRequest.getParameterMap().get("token")[0]; if(StringUtils.isEmpty(token)){ throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单"); } UserModel userModel = (UserModel)redisTemplate.opsForValue().get(token); if(userModel == null){ throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单"); } // 校验秒杀令牌是否正确 if(promoId != null){ String inRedisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_userid_" + userModel.getId() + "_itemid_" + itemId); if(inRedisPromoToken == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败"); } if(!org.apache.commons.lang3.StringUtils.equals(inRedisPromoToken, promoToken)){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败"); } } // 同步调用线程池的submit方法 // 当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。 // 即每一个初始化库存流水操作、rocketmq事务型消息、下订单操作放在一个线程中执行,一共20个线程,则可以同时有20个这一系列操作,其他的放在队列中 Future<Object> future = executorService.submit(new Callable<Object>() { @Override public Object call() throws Exception { // 初始化库存流水(id、itemid、amount、status存入数据库流水表) String stockLogId = itemService.initStockLog(itemId, amount); // 完成对应的下单事务型消息机制 boolean orderState = mqProducer.transactionAsyncReduceStockAndAddSales(userModel.getId(), itemId, promoId, amount, stockLogId); // 下单失败 if(!orderState){ throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败"); } return null; } }); try { // 返回null future.get(); } catch (InterruptedException | ExecutionException e) { throw new BusinessException(EmBusinessError.UNKNOWN_ERROR); } return CommonReturnType.create(null); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。