SpringMVC整合rabbitmq:優化秒殺下單環節
前言
上一篇在springboot中基於自動配置集成了rabbitmq。那麼回到最初的話題中就是想在秒殺下單環節增加排隊機制,從而達到限流的目的。
優化秒殺下單流程
之前是在控制器裡拿到客戶端請求後直接入庫、減庫存。如果碰到羊毛黨其實這套機制是不行的。併發量高的時候,庫存數量也會不準確。那麼引入rabbitmq則在下單時讓使用者資訊產生一條訊息入隊。然後消費者處理下單(是否重複下單、下單失敗、庫存不夠)。客戶端接受到請求已入佇列(response引入state處理互動)後發起ajax輪詢請求,處理成功則跳轉下單成功頁或者結束本次互動。
1、下單(秒殺介面)
@RequestMapping(value="/{seckillId}/{md5}/execute",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId")Long seckillId, @PathVariable("md5")String md5, @CookieValue(value="phone",required=false)Long phone){ if(phone==null){ return new SeckillResult<SeckillExecution>(false,"手機號未註冊"); } SeckillResult<SeckillExecution> result=null; try{ SeckillExecution execution=seckillService.executeSeckill(seckillId,phone,md5); result=new SeckillResult<SeckillExecution>(true,execution); }catch(RepeatKillException e){ SeckillExecution execution=new SeckillExecution(seckillId,-1,"重複秒殺"); result=new SeckillResult<SeckillExecution>(true,execution); }catch(SeckillCloseException e){ SeckillExecution execution=new SeckillExecution(seckillId,0,"秒殺結束"); result=new SeckillResult<SeckillExecution>(true,execution); }catch (Exception e){ SeckillExecution execution=new SeckillExecution(seckillId,-2,"系統異常"); result=new SeckillResult<SeckillExecution>(true,execution); } return result; }
2、下單業務方法(Service) 這裡就要引入排隊
@Override public SeckillExecution executeSeckill(long seckillId, long phone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException { if (md5 == null || !md5.equals(getMd5(seckillId))) { throw new SeckillException("非法請求"); } Date now = new Date(); try { int insertCount = successKillDao.insertSuccessKilled(seckillId, phone); if (insertCount <= 0) { throw new RepeatKillException("重複秒殺"); } else { //請求入隊 MiaoshaUser miaoshaUser=new MiaoshaUser(); miaoshaUser.setPhone(phone); MiaoshaMessage miaoshaMessage=new MiaoshaMessage(); miaoshaMessage.setSeckillId(seckillId); miaoshaMessage.setMiaoshaUser(miaoshaUser); String miaosha=JSON.toJSONString(miaoshaMessage); amqpTemplate.convertAndSend(miaosha); return new SeckillExecution(seckillId,0,"請求入隊"); /*** * 直接入庫操作 int updateCount = seckillDao.reduceNumber(seckillId, now); if (updateCount <= 0) { throw new SeckillCloseException("秒殺已關閉"); } else { //秒殺成功,可以把秒殺詳情和商品詳情實體返回 SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(seckillId, phone); return new SeckillExecution(seckillId, 1, "秒殺成功", successKilled); } ***/ } } catch (SeckillCloseException e) { throw e; } catch (RepeatKillException e1) { throw e1; } catch (SeckillException e2) { logger.error(e2.getMessage(), e2); throw new SeckillException("Unkonwn error:" + e2.getMessage()); } }
3、下單結果介面
@RequestMapping(value="/{seckillId}/{md5}/result",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<SeckillExecution> result(@PathVariable("seckillId")Long seckillId, @PathVariable("md5")String md5, @CookieValue(value="phone",required=false)Long phone){ SuccessKilled successKilled = seckillService.queryByIdWithSeckill(seckillId, phone); SeckillExecution execution=null; if(successKilled.getSeckillId()>0){ execution=new SeckillExecution(seckillId, 1, "下單成功", successKilled); }else{ execution=new SeckillExecution(seckillId, -2, "下單失敗", successKilled); } return new SeckillResult<SeckillExecution>(true,execution); }
4、消費者(下單處理)
/** * 秒殺請求消費 **/ public class AmqpConsumer implements MessageListener { @Autowired SeckillDao seckillDao; @Autowired SuccessKillDao successKillDao; @Override public void onMessage(Message message) { Date now = new Date(); MiaoshaMessage miaosha = JSON.parseObject(message.getBody(), MiaoshaMessage.class); Long seckillId = miaosha.getSeckillId(); int updateCount = seckillDao.reduceNumber(seckillId, now); if (updateCount <= 0) { System.out.println("秒殺下單失敗"); } else { System.out.println("秒殺下單成功"); } } }
5、springmvc整合訊息佇列配置檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--spring整合rabbitmq--> <rabbit:connection-factory id="connectionFactory" host="192.168.80.128" port="5672" username="admin" password="admin" channel-cache-size="5" virtual-host="/" /> <rabbit:admin connection-factory="connectionFactory"/> <!--宣告佇列--> <rabbit:queue durable="true" auto-delete="false" exclusive="false" name="miaosha.queue"/> <!--交換器和佇列繫結--> <rabbit:direct-exchange name="miaosha.exchange"> <rabbit:bindings> <rabbit:binding queue="miaosha.queue" key="miaosha.tag.key"/> </rabbit:bindings> </rabbit:direct-exchange> <!--spring rabbitmqTemplate宣告--> <rabbit:template id="rabbitTemplate" exchange="miaosha.exchange" routing-key="miaosha.tag.key" connection-factory="connectionFactory" /> <!--訊息監聽--> <bean id="miaoshaConsumer" class="com.seckill.mq.AmqpConsumer"/> <rabbit:listener-container connection-factory="connectionFactory" acknowledge="auto"> <rabbit:listener ref="miaoshaConsumer" queues="miaosha.queue"/> </rabbit:listener-container> </beans>
6、客戶端秒殺下單、等待下單結果
/**秒殺結果**/ miaosha:function(seckillId,md5,node){ $.get('/seckill/'+seckillId+'/'+md5+'/result',{},function(result){ if(result && result["success"]){ var oData=result["data"]; if(oData["state"]===1){ node.html("<span class='label label-success'>下單成功</span>"); clearInterval(miaoshaTimer); }else{ console.log("還在排隊種..."); } } }) }, /**執行秒殺**/ seckill:function(seckillId,node){ //獲取秒殺地址、控制node節點顯示,執行秒殺 node.hide().html("<button id='killBtn' class='btn btn-primary btn-lg'>開始秒殺</button>") $.get('/seckill/'+seckillId+'/exposer',{},function(result){ if(result && result["success"]){ //在回撥函式中執行秒殺操作 var exposer=result["data"]; if(exposer["exposed"]){ //秒殺已開始 var md5=exposer["md5"]; var killUrl='/seckill/'+seckillId+'/'+md5+'/execute'; console.log(killUrl); $("#killBtn").one('click',function(){ //1、禁用秒殺按鈕 $(this).addClass('disabled'); //2、執行秒殺操作 $.post(killUrl,{},function(result){ if(result && result["success"]){ var killResult=result["data"]; var state=killResult["state"]; var stateInfo=killResult["stateInfo"]; node.html("<span class='label label-success'>"+stateInfo+"</span>"); if(state===0){ //已入隊,客戶端開始輪詢 miaoshaTimer=setInterval(function(){ seckill.miaosha(seckillId,md5,node); },3000); } } }) }); node.show(); }else{ //秒殺未開始, 防止瀏覽器和伺服器出現時間差,再次執行倒數計時 var now = exposer['now']; var start = exposer['start']; var end = exposer['end']; seckill.countdown(seckillId, now, start, end); } }else{ console.log('result:'+result); //沒有拿到秒殺地址 } }) }
好了,貼了這麼多程式碼,沒有示意圖怎麼能行?
總結
秒殺下單增加排隊機制來說對於完整的秒殺系統來說只是其中很少的一部分,這裡也只是學習rabbitmq的一個過程。對於秒殺系統來說流量主要是查詢多下單少。還需要引入redis,把庫存量、商品資訊能在秒殺開始前預處理。