1. 程式人生 > >執行緒池處理高併發請求

執行緒池處理高併發請求

背景

本系統(支付系統)會在每個月特定時間(如賬單日某個時間)接收上游系統發起的大量請求並進行處理,並在處理完成後返回結果給上游系統。而本系統接收到請求進行處理的過程是呼叫第三方(支付公司)進行處理並獲取結果。

系統原實現方案沒有采用任何控制請求併發數的措施,接收到上游系統的請求後,就傳送給支付渠道進行處理。這樣實際上就是來一個請求就啟動一個tomcat執行緒進行處理。

上游系統呼叫本系統,本系統呼叫第三方公司同步介面,獲得結果後本系統將結果同步返回給上游系統。
假設上游系統的併發數為n,則一開始本系統一秒鐘接收n個請求,並將這些請求發往第三方進行處理。假設第三方處理請求需要的時間為t,則對於上游系統來講,本系統的響應時間比t略大。由於整個呼叫鏈上第三方處理時間較長,最短1s,最長可達10s,所以一次完整請求的處理時間是比較長的。本系統處理上游系統請求的TPS = n/t(實際略大於t),由於併發數n不大同時響應時間t較大,所以tps不高。本系統和下游第三方基本沒什麼壓力。

此方案存在的問題:系統介面響應時間依賴於第三方介面響應時間,最長可能長達十幾秒。另外就是tps過低。實際上游系統併發量為16,響應時間平均為3s,則tps為5左右,支付系統平均每秒處理5個請求,一小時只能處理15000左右的請求數。

改進方案

對本系統進行改造,主要是把本系統的請求處理介面由同步介面改為非同步介面。

即同步介面不再等第三方處理完成之後才返回,而是在本系統接受到請求並進行內部處理,在傳送給第三方進行處理之前返回。然後再由非同步任務將請求傳送給第三方進行處理,將處理結果再通過回撥方式或者提供查詢介面提供給上游系統。

經此改造後,本系統對上游系統的響應時間由平均3s縮短到30ms。此種改進方案的本質是將上游系統的請求接受過來,然後根據下游的處理能力進行請求分發。那麼必須要有一種機制能將請求儲存起來,然後根據實際情況將請求取出來傳送給下游第三方進行處理。

考慮兩種方案,其中一種是訊息佇列,將請求放到訊息佇列中慢慢處理,但無法保證訊息不丟失。故採用第二種方案:將請求全部入庫,然後通過執行緒池來執行後續任務。

問題及解決辦法

1.大量請求沒有處理

改進後出現的第一個問題是大量請求在任務表中沒有得到處理。原因是:大量請求在一定時間內進入任務表,同時通過執行緒池將請求取出來執行。由於接受請求的介面響應時間極短(20ms-30ms),tps約為70。執行緒池核心執行緒數為40,四個伺服器例項加起來為160,而下游系統的響應時間大概為5-10s,tps為20左右。導致每秒約有50個請求得不到處理而進入佇列等待,80s之後四個伺服器例項的執行緒池佇列全部放滿,之後的請求就無法得到處理了。當上遊系統的請求全部接受完畢,主執行緒停止之後,因任務佇列滿而得不到處理的請求就在任務表中得不到處理。

解決辦法是將任務表中被拒絕的請求用一個狀態欄位來標記,然後通過補償任務撈出來再次執行。但是在此過程中又出現了第二個問題。

2.併發更新資料庫導致死鎖

補償任務處理邏輯是:從任務表中取出請求記錄,當該記錄狀態為被拒絕,且更新為已執行時更新成功,則將請求傳送給第三方進行處理。
這裡使用資料庫樂觀鎖來更新狀態,防止併發更新和重複執行請求。但是是將狀態作為樂觀鎖標識,更新語句以狀態為條件來更新。這種做法帶來了死鎖問題。原因是:mysql的innoDB行鎖是通過給索引項加鎖實現的。而索引分為主鍵索引和非主鍵索引,如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。如果兩個執行緒同時來更新一條記錄,一個鎖住了主鍵索引,在等待其他相關索引。另一個鎖定了非主鍵索引,在等待主鍵索引。這樣就會發生死鎖。解決辦法是加version欄位,更新時以主鍵id和version作為條件來更新。Version上無索引,更新時只會鎖定主鍵索引,就不會造成死鎖。

其實這裡也可以使用redis分散式鎖,但是需要設定合適的鎖過期時間。