[Soul 源碼之旅] 1.6 Soul數(shù)據(jù)同步 (長輪詢)

1.6.1 背景

我們先來聊一下常用的幾種即時通訊技術包括 輪詢,長輪詢,和 Websocket 三種。

1.6.1.1 輪詢

輪詢是客戶端每隔一段時間向服務端發(fā)送請求,服務端不管是否又數(shù)據(jù)更新都會直接返回所有數(shù)據(jù),這種方式實現(xiàn)起來很簡單,而且服務端無需做任何改造就可以滿足要求,但是會占用比較多的內存和帶寬,無法承受大量客戶端。

1.6.1.2 長輪詢

長輪詢是當服務器收到客戶端發(fā)來的請求后,服務器端不會直接進行響應,而是先將這個請求掛起,然后判斷服務器端數(shù)據(jù)是否有更新。如果有更新,則進行響應,如果一直沒有數(shù)據(jù),則到達一定的時間限制(服務器端設置)才返回。相比輪詢,長輪詢主要優(yōu)點是可以減少帶寬,但是服務端需進行一定的改造。

1.6.1.2 websocket

websocket 是一種比較新的網(wǎng)絡協(xié)議,它是全雙工的,在第一次客戶端向服務端發(fā)送Http請求建立鏈接后,客戶端和服務端就處于平等狀態(tài),可以相互發(fā)送數(shù)據(jù),websocket 相比上面兩種協(xié)議,無論是對服務器的CPU和內存資源的消耗,還是服務的響應及時性都要更好,但是需要服務器做的變動比較多,還需要又支持 websocket 的客戶端。

1.6.2 soul Bootstrap

由于是客戶端發(fā)起請求,我們先來看一下Bootstrap 客戶端。我們還是根據(jù) 引入的 starter 追蹤到 HttpSyncDataConfiguration 這個配置類,HttpSyncDataConfiguration 主要是初始化類 httpSyncDataService 這個bean。


HttpSyncDataConfiguration

httpSyncDataService 的初始化主要做了三件事,首先是初始化了所有的插件的 subscriber 訂閱者,初始化 httpClinet 使用的是 OkHttp3ClientHttpRequestFactory 作為一個 Restemplate ,后面可以直接通過操作 Restemplate 進行輪詢,最終調用start 方法。


image.png

start 方法先判斷當前狀態(tài)是否處于運行狀態(tài),保證每次只有一個客戶端在輪詢。
image.png

接著調用 /configs/fetch 接口先拿回全量的數(shù)據(jù),然后更新。


image.png

這里有個有趣的地方,我們看 DataRefreshFactory 的 excite 方法,這里主要是通過stream 的 foreach 方法進行遍歷,但是這里使用了一個 boolean 數(shù)組,但是只有一個元素,為什么呢,因為 lamb 表達式要求里面的所有元素都是 final 的,final 就意味著對這個元素的操作是線程安全的。但是假如是一個 final 的 boolean 那不是無法修改狀態(tài)了嗎,所以這里使用了一個看起來很奇怪的 boolean 數(shù)組。


image.png

接著 bootsrapt 啟動一個 ThreadPoolExecutor 線程池 ,主要是執(zhí)行定時刷新任務,這個線程池為每個 soul admin 服務端建立一個定時任務 HttpLongPollingTask ,所以我們知道 soul admin 和 soul bootstrap 是可以使用一個集群提供高可用保證的。
在定時任務中主要是先判斷目前是處于運行中,是則進行長輪詢,假如輪詢失敗則重試,重試超過三次就睡眠 5 min。


image.png

doLongPolling 首先為每個需要輪詢的數(shù)據(jù)的MD5值和最后修改時間用逗號拼接作為請求參數(shù),請求服務端。/configs/listener 接口,然后將拿到的數(shù)據(jù)更新到內存中。
doLongPolling

image.png

1.6.3 soul admin
我們接著看 /configs/listener 接口 , 這里也是調用 doLongPolling 方法。


doLongPolling

它先比較請求的MD5值和當前的配置是否相等和最后修改時間和當前的配置是否相同,假如不同,則返回該配置組,假如都相同,這里會先取一把鎖,然后從數(shù)據(jù)庫里面更新最新的數(shù)據(jù)到內存中,這里主要是為了多個 soul admin 假如其中一個更新了,其他的也能同步更新,很多人會想問,這里數(shù)據(jù)是使用 concurrentHasmap 進行存儲,為什么還需要加鎖,這里作者解釋,主要是因為存在多個 soul web 的話,可能存在同時向數(shù)據(jù)庫請求,數(shù)據(jù)庫的瞬時壓力增大,這種情況很好理解,就像我們設置 redis 的 key 一樣,不能同時設置同個過期時間,否則很容易造成緩存穿透和甚至緩存雪崩。
image.png

這里拿到所有已經(jīng)更新的組信息返回。
compareChangedGroup

這里假如有數(shù)據(jù)更新則立即返回數(shù)據(jù)。假如沒有數(shù)據(jù)則調用 request.startAsync() 方法,這個是什么意思呢,它的作用是HTTP請求不再綁定到HTTP線程,這使我們以后可以使用更少的線程來處理它,我們拿到 asynccontext 后發(fā)給我們的線程池,這里處理該 http 請求的線程就完成了此次請求,后續(xù)需要發(fā)送數(shù)據(jù)給客戶端只需要線程池進行通過操作 asynccontext 實現(xiàn)。
longpoll

最終這里通過線程池的schedule方法,設置延時時長,到預定時間后查看緩存數(shù)據(jù)是否有更新,有則返回給客戶端。
image.png

這里調用 asyncContext.complete() 整個調用才算結束。
sendResponse

1.6.3 總結

這一期最精彩的莫過于對request.startAsync()對運用,這里假如直接阻塞當前線程,我們知道 tomcat 處理http 請求對線程是有限的,直接阻塞假如 soul bootstrap 很多會耗盡請求線程池。這里直接放入 一個調度線程池, 這個線程池只有一個線程,當很多請求進來,他會先將它放入 BlockQueue 中,然后一個個處理,減少了線程池的開銷。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容