1. 程式人生 > >高併發重複請求的去重處理

高併發重複請求的去重處理

      最近碰到一個重複提交請求,並能在資料庫重複插入多條同樣資料的問題。因為需求涉及到的是給使用者發放購物卡,直接關係到的是金錢,所以是影響很大的一個問題,比如給一個使用者發放100元一次,結果被某些居心不良之人抓到包,直接複製一百個請求,然後在執行就相當於給使用者直接發放100元100次了,後果可想而知是非常嚴重的,所以必須的趕緊解決。

      問題還原:在頁面點選購物卡稽核通過按鈕,然後利用抓包工具抓到相應的請求,並複製請求N份,然後繼續執行,這時資料庫中會儲存為每個userid發放了N份購物卡。

      問題分析:對於這種問題,第一反應就是在資料庫中加入一個唯一索引,這樣就算重複請求N次,有唯一索引控制,結果也只會插入一條資料;並且購物卡發放也確實是有個購物卡批次號ID,這個ID針對每次發放來說都是一個唯一ID。但是在存入購物卡發放的record表中時,需求設計的是一個購物卡批次號中的一個userid可以對應多記錄,因為購物卡面值最大100,如果一次性發放1000的話那就要一次性插入十條記錄。所以加唯一索引的這條道不通。當然也考慮過加入一個欄位,然後把這個欄位設為唯一索引,但是現在系統已經迭代了多個版本,這裡加一個欄位,系統其它很多地方也需求跟著修改並需要重新測試,所以加欄位也暫不考慮。

                          既然這兩種方式都不通,那就只能針對具體業務流程去進行具體分析了。因為涉及程式碼安全性,這裡貼出主要業務流程的虛擬碼,這個業務邏輯主要在service層中進行處理的,service層的方法如下:

                          public void addcardservice(record r){

                                    //新增審批記錄,此處操作的是審批表

                                    addcardaudit();

                                    。。。

                                    //發放購物卡,此處操作的購物卡發放表record

                                    addcardrecord();

                                    。。。

                                    //修改購物卡狀態,此處操作的是card表

                                   updatecard();

                          }

      針對如上業務流程,因為record是允許插入多條重複記錄的,並且在購物卡record發放成功後,我們會去把card表的狀態改為審批通過,既state改為3.所以這裡想到的一個方法就是對card表加一個樂觀鎖,提前把card表的狀態改審批通過,既state改為3,如果改成功了我們就進行購物record的發放,因為加了樂觀鎖,所以會保證只能有一條記錄被更新為state=3,其餘的update全都返回false,所以也就無法進行購物卡的發放了,修改後的虛擬碼如下:

                           public void addcardservice(record r){

                                    //新增審批記錄,此處操作的是審批表

                                    addcardaudit();

                                    。。。

                                    //修改購物卡狀態,此處操作的是card表

                                   boolean flag = updatecard();

                                   (update card set state=3 where id=#id# and state!=3)

                                    。。。

                                    if(falg == true){

                                              //發放購物卡,此處操作的購物卡發放表record

                                              addcardrecord();

                                    }

                          }

      在修改card表處的update語句,我做了一點修改,既只有當狀態不為3時(既狀態為稽核不通過時)才能update成功,如果狀態為3了那麼這條語句就永遠也不能修改成功了,所以通過這種樂觀鎖機制,保證了update永遠只有一個請求會update成功,然後成功了的請求才會繼續發放購物卡,這樣就保證了即使有多個重複請求過來也不會有多條重複記錄產生。可能還會有個疑問,如果card表修改成功,在發放購物卡record表的操作時失敗了,那狀態就不一致了,其實這個也不用當心,我們整個業務邏輯都是在service層進行操作的,在service層我們有spring做了事務控制,所以即使操作record表時有了異常,也會整個一起回滾。

      做到這裡貌似整個方案都完美解決這個問題,並且不用加欄位也不用加唯一索引。但是問題並沒解決,如果在併發量不大的情況下,這個確實是解決了,但經過壓力測試,當併發量較大時,還是會出現兩條以上的重複記錄,雖然比之前的重複資料少了很多,但是重複資料還是存在。經分析,原因是當足夠多的併發量請求一起過來時,如果當兩個請求同時進入service層的方法,並同時開啟了事務,那麼這兩個事務看到的資料是一致,也就是兩個事務看到card表的state的值都不為3,並且在事務中,其中一個請求update成功後,必須在事務結束時才會提交update的資料,所以其他請求還是會看到state的值為3,並且在高併發情況下還是會產生多條重複資料的情況,雖然重複資料少了很多,但還是存在,所以問題並沒解決。碰到這種情況,我們可以在做進一步的處理,如下所示

                           public void addcardservice(record r){

                                    //此處增加行級鎖

                                    getcardlock();

                                    (select * from card where id=#id# for update)

                                    //新增審批記錄,此處操作的是審批表

                                    addcardaudit();

                                    。。。

                                    //修改購物卡狀態,此處操作的是card表

                                   boolean flag = updatecard();

                                   (update card set state=3 where id=#id# and state!=3)

                                    。。。

                                    if(falg == true){

                                              //發放購物卡,此處操作的購物卡發放表record

                                              addcardrecord();

                                    }

                          }

      以上處理的邏輯是在進入service層方法時,因為對於card的update操作,我們都是對於其中的一條資料進行update,所以我們可以在事務開啟時用for update的方式,增加一個行級鎖,這樣就算後面同時併發了再多了請求,但鎖只有一個,當第一個請求獲取到鎖時,其他同時併發過來的請求只能處於等待狀態。然後在用下面的樂觀鎖進行card表的update操作,這樣就可以從理論上避免了高併發下重複請求提交處理的問題。

      最後如果當有一萬的併發請求過來時,當第一個請求獲得鎖在處理業務時,可能其他9999個請求都出於阻塞狀態,這樣對於伺服器來說也是比較損耗資源的,對於這種情況,可以採取下面這種方式進行優化

                           private Vector<String> uniqeShopcardIds = new Vector<String>(1000);

                           if(!uniqeShopcardIds.contains(cardid)){
                                  uniqeShopcardIds.add(String.valueOf(cardid));
                                  if(uniqeShopcardIds.size()>500){
                                      uniqeShopcardIds.clear();
                                  }
                            }

      我們在進入service層方法的外層,controller的方法中加入以上程式碼,因為進入controller中的購物卡批次號是唯一的,雖然有重複請求,我們只需處理一次就足夠了,所以用上方法把購物卡批次號存入vector物件中進行重複判斷,其中uniqeShopcardIds為全域性變數,這樣可以過濾掉大部分的請求,從而減少service層中的併發量,也減輕了伺服器的壓力。

      到此關於這個高併發下重複請求處理的問題差不多就完全解決了。暫時先寫到這,記錄下這個問題的解決方法,留個紀念。微笑