SpringBoot秒殺系統實戰21-高併發秒殺系統接口優化 RabbitMQ異步下單...

文章目錄

【秒殺系統的接口優化之異步下單】

問題:

針對秒殺的業務場景,在大併發下,僅僅依靠頁面緩存、對象緩存或者頁面靜態化等還是遠遠不夠。數據庫壓力還是很大,所以需要異步下單,如果業務執行時間比較長,那麼異步是最好的解決辦法,但會帶來一些額外的程序上的複雜性。

思路:

  1. 系統初始化,把商品庫存數量stock加載到Redis上面來。
  2. 後端收到秒殺請求,Redis預減庫存,如果庫存已經到達臨界值的時候,就不需要繼續請求下去,直接返回失敗,即後面的大量請求無需給系統帶來壓力。
  3. 判斷這個秒殺訂單形成沒有,判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品,判斷是否重複秒殺。
  4. 庫存充足,且無重複秒殺,將秒殺請求封裝後消息入隊,同時給前端返回一個code (0),即代表返回排隊中。(返回的並不是失敗或者成功,此時還不能判斷)
  5. 前端接收到數據後,顯示排隊中,並根據商品id輪詢請求服務器(考慮200ms輪詢一次)。
  6. 後端RabbitMQ監聽秒殺MIAOSHA_QUEUE的這名字的通道,如果有消息過來,獲取到傳入的信息,執行真正的秒殺之前,要判斷數據庫的庫存,判斷是否重複秒殺,然後執行秒殺事務(秒殺事務是一個原子操作:庫存減1,下訂單,寫入秒殺訂單)。
  7. 此時,前端根據商品id輪詢請求接口MiaoshaResult,查看是否生成了商品訂單,如果請求返回-1代表秒殺失敗,返回0代表排隊中,返回>0代表商品id說明秒殺成功。

返回結果說明:

前端會根據後端返回的值來判斷是秒殺結果。

-1 :庫存不足秒殺失敗
=0 :排隊中,繼續輪詢
>0 :返回的是商品id ,說明秒殺成功

1.後端接收秒殺請求的接口doMiaosha。

@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaosha(Model model,MiaoshaUser user,
        @RequestParam(value="goodsId",defaultValue="0") long goodsId,
        @PathVariable("path")String path) {
    model.addAttribute("user", user);
    //1.如果用戶爲空,則返回至登錄頁面
    if(user==null){
        return Result.error(CodeMsg.SESSION_ERROR);
    }   
    //2.預減少庫存,減少redis裏面的庫存
    long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
    //3.判斷減少數量1之後的stock,區別於查數據庫時候的stock<=0
    if(stock<0) {
        return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
    }
    //4.判斷這個秒殺訂單形成沒有,判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
    MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
    if (order != null) {// 查詢到了已經有秒殺訂單,代表重複下單
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    //5.正常請求,入隊,發送一個秒殺message到隊列裏面去,入隊之後客戶端應該進行輪詢。
    MiaoshaMessage mms=new MiaoshaMessage();
    mms.setUser(user);
    mms.setGoodsId(goodsId);
    mQSender.sendMiaoshaMessage(mms);
    //返回0代表排隊中
    return Result.success(0);
}

//MiaoshaMessage 消息的封裝  MiaoshaMessage Bean
public class MiaoshaMessage {

private MiaoshaUser user;
private long goodsId;
public MiaoshaUser getUser() {
    return user;
}
public void setUser(MiaoshaUser user) {
    this.user = user;
}
public long getGoodsId() {
    return goodsId;
}
public void setGoodsId(long goodsId) {
    this.goodsId = goodsId;
}
}

注意:消息隊列這裏,消息只能傳字符串,MiaoshaMessage 這裏是個Bean對象,是先用beanToString方法,將轉換爲String,放入隊列,使用AmqpTemplate傳輸。

@Autowired
RedisService redisService;
@Autowired
AmqpTemplate amqpTemplate;
public void sendMiaoshaMessage(MiaoshaMessage mmessage) {
    // 將對象轉換爲字符串
    String msg = RedisService.beanToString(mmessage);
    log.info("send message:" + msg);
    // 第一個參數隊列的名字,第二個參數發出的信息
    amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
/**
 * 將Bean對象轉換爲字符串類型
 * @param <T>
 */
public static <T> String beanToString(T value) {
    //如果是null
    if(value==null) return null;
    //如果不是null
    Class<?> clazz=value.getClass();
    if(clazz==int.class||clazz==Integer.class) {
        return ""+value;
    }else if(clazz==String.class) {
        return ""+value;
    }else if(clazz==long.class||clazz==Long.class) {
        return ""+value;
    }else {
        return JSON.toJSONString(value);
    }       
}

2.監控該消息隊列,一旦有消息進入,從該消息中獲取對象進行秒殺操作

@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)//指明監聽的是哪一個queue
	public void receiveMiaosha(String message) {
		log.info("receiveMiaosha message:"+message);
		//通過string類型的message還原成bean,拿到了秒殺信息之後。開始業務邏輯秒殺,
		MiaoshaMessage mm=RedisService.stringToBean(message, MiaoshaMessage.class);
		MiaoshaUser user=mm.getUser();
		long goodsId=mm.getGoodsId();
		GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);
		int  stockcount=goodsvo.getStockCount();		
		//1.判斷庫存不足
		if(stockcount<=0) {
			return;
		}
		//2.判斷這個秒殺訂單形成沒有,判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
		if (order != null) {// 重複下單
			return;
		}
		//原子操作:1.庫存減1,2.下訂單,3.寫入秒殺訂單--->是一個事務
		miaoshaService.miaosha(user,goodsvo);		
	}
	@Transactional
	public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsvo) {
		//1.減少庫存,即更新庫存
		boolean success=goodsService.reduceStock1(goodsvo);//考慮減少庫存失敗的時,不進行寫入訂單
		if(success) {
			//2.下訂單,其中有兩個訂單: order_info   miaosha_order
			OrderInfo orderinfo=orderService.createOrder_Cache(user, goodsvo);
			return orderinfo;	
		}else {//減少庫存失敗,做一個標記,代表商品已經秒殺完了。
			setGoodsOver(goodsvo.getId());
			return null;
		}
	}
	
	//寫入緩存
	private void setGoodsOver(Long goodsId) {
		redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true);
	}
	//查看緩存中是否有該key
	private boolean getGoodsOver(Long goodsId) {
		return redisService.exitsKey(MiaoshaKey.isGoodsOver, ""+goodsId);
	}

注意:秒殺操作是一個事務,使用@Transactional註解來標識,如果減少庫存失敗,則回滾。

3.前端根據商品id輪詢請求接口MiaoshaResult,查看是否生成了商品訂單,後端處理秒殺邏輯,並向前端返回請求結果。

/**
 * 客戶端做一個輪詢,查看是否成功與失敗,失敗了則不用繼續輪詢。
 * 秒殺成功,返回訂單的Id。
 * 庫存不足直接返回-1。
 * 排隊中則返回0。
 * 查看是否生成秒殺訂單。
 */
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public Result<Long> doMiaoshaResult(Model model, MiaoshaUser user,
        @RequestParam(value = "goodsId", defaultValue = "0") long goodsId) {
    long result=miaoshaService.getMiaoshaResult(user.getId(),goodsId);
    System.out.println("輪詢 result:"+result);
    return Result.success(result);
}

public long getMiaoshaResult(Long userId, long goodsId) {
    //先去緩存裏面取得
    MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndGoodsId(userId, goodsId);
    //秒殺成功
    if(order!=null) {       
        return order.getOrderId();
    }
    else {
        //查看商品是否賣完了
        boolean isOver=getGoodsOver(goodsId);
        if(isOver) {//商品賣完了
            return -1;
        }else {     //商品沒有賣完
            return 0;
        }
    }
}

注意:然後輪詢訪問 doMiaoshaResult這個接口,從數據庫中拿訂單,如果有。返回商品id,說明秒殺成功,通過從redis中拿到isOver標記來判斷失敗還是在請求,商品賣完了返回-1,商品沒有賣完返回0,繼續請求,前端拿到返回的數據,通過判斷,進行顯示,成功就跳轉訂單頁面。

前端輪詢業務代碼:

function doMiaosha(path) {
		alert(path);
		alert("秒殺!");
		$.ajax({
			url : "/miaosha/" + path + "/do_miaosha",
			type : "POST",
			data : {
				goodsId : $("#goodsId").val()				
			},
			success : function(data) {
				if (data.code == 0) {
					//秒殺成功,跳轉詳情頁面
					//window.location.href="order_detail.htm?orderId="+data.data.id;	
					//輪詢
					getMiaoshaResult($("#goodsId").val());
				} else {
					layer.msg(data.msg);
				}
			},
			error : function() {
				layer.msg("請求有誤!");
			}
		});
	}
	//做輪詢
	function getMiaoshaResult(goodsId) {
		$.ajax({
			url : "/miaosha/result",
			type : "GET",
			data : {
				goodsId : $("#goodsId").val()
			},
			success : function(data) {
				if (data.code == 0) {
					var result = data.data;
					if (result < 0) {
						layer.msg("抱歉,秒殺失敗!");
					} else if (result == 0) {
						//繼續輪詢
						setTimeout(function() {
							getMiaoshaResult(goodsId);
						}, 200);//200ms之後繼續輪詢
						layer.msg(data.msg);
					} else {
						layer.confirm("恭喜你,秒殺成功!查看訂單?", {
							btn : [ "確定", "取消" ]
						}, function() {
							//秒殺成功,跳轉詳情頁面
							window.location.href = "order_detail.htm?orderId="
									+ result;
						}, function() {
							layer.closeAll();
						});
					}
				} else {
					layer.msg(data.msg);
				}
			},
			error : function() {
				layer.msg("請求有誤!");
			}
		});
	}

注意setTimeout的用法
setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。
1000 毫秒= 1 秒。
如果你只想重複執行可以使用 setInterval() 方法。
使用 clearTimeout() 方法來阻止函數的執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章