秒杀令牌的原理
秒杀令牌的作用
秒杀令牌实现代码
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);
}