1.6.1 背景
我們先來聊一下常用的幾種即時通訊技術(shù)包括 輪詢,長輪詢他匪,和 Websocket 三種割疾。
1.6.1.1 輪詢
輪詢是客戶端每隔一段時間向服務(wù)端發(fā)送請求侧纯,服務(wù)端不管是否又數(shù)據(jù)更新都會直接返回所有數(shù)據(jù),這種方式實現(xiàn)起來很簡單碍讨,而且服務(wù)端無需做任何改造就可以滿足要求治力,但是會占用比較多的內(nèi)存和帶寬,無法承受大量客戶端勃黍。
1.6.1.2 長輪詢
長輪詢是當服務(wù)器收到客戶端發(fā)來的請求后,服務(wù)器端不會直接進行響應(yīng)宵统,而是先將這個請求掛起,然后判斷服務(wù)器端數(shù)據(jù)是否有更新覆获。如果有更新马澈,則進行響應(yīng)瓢省,如果一直沒有數(shù)據(jù),則到達一定的時間限制(服務(wù)器端設(shè)置)才返回箭券。相比輪詢净捅,長輪詢主要優(yōu)點是可以減少帶寬,但是服務(wù)端需進行一定的改造辩块。
1.6.1.2 websocket
websocket 是一種比較新的網(wǎng)絡(luò)協(xié)議蛔六,它是全雙工的,在第一次客戶端向服務(wù)端發(fā)送Http請求建立鏈接后废亭,客戶端和服務(wù)端就處于平等狀態(tài)国章,可以相互發(fā)送數(shù)據(jù),websocket 相比上面兩種協(xié)議豆村,無論是對服務(wù)器的CPU和內(nèi)存資源的消耗液兽,還是服務(wù)的響應(yīng)及時性都要更好,但是需要服務(wù)器做的變動比較多掌动,還需要又支持 websocket 的客戶端四啰。
1.6.2 soul Bootstrap
由于是客戶端發(fā)起請求,我們先來看一下Bootstrap 客戶端粗恢。我們還是根據(jù) 引入的 starter 追蹤到 HttpSyncDataConfiguration 這個配置類柑晒,HttpSyncDataConfiguration 主要是初始化類 httpSyncDataService 這個bean。
httpSyncDataService 的初始化主要做了三件事眷射,首先是初始化了所有的插件的 subscriber 訂閱者匙赞,初始化 httpClinet 使用的是 OkHttp3ClientHttpRequestFactory 作為一個 Restemplate ,后面可以直接通過操作 Restemplate 進行輪詢妖碉,最終調(diào)用start 方法涌庭。
start 方法先判斷當前狀態(tài)是否處于運行狀態(tài),保證每次只有一個客戶端在輪詢欧宜。
接著調(diào)用 /configs/fetch 接口先拿回全量的數(shù)據(jù)坐榆,然后更新。
這里有個有趣的地方冗茸,我們看 DataRefreshFactory 的 excite 方法猛拴,這里主要是通過stream 的 foreach 方法進行遍歷,但是這里使用了一個 boolean 數(shù)組蚀狰,但是只有一個元素愉昆,為什么呢,因為 lamb 表達式要求里面的所有元素都是 final 的麻蹋,final 就意味著對這個元素的操作是線程安全的跛溉。但是假如是一個 final 的 boolean 那不是無法修改狀態(tài)了嗎,所以這里使用了一個看起來很奇怪的 boolean 數(shù)組。
接著 bootsrapt 啟動一個 ThreadPoolExecutor 線程池 芳室,主要是執(zhí)行定時刷新任務(wù)专肪,這個線程池為每個 soul admin 服務(wù)端建立一個定時任務(wù) HttpLongPollingTask ,所以我們知道 soul admin 和 soul bootstrap 是可以使用一個集群提供高可用保證的堪侯。
在定時任務(wù)中主要是先判斷目前是處于運行中嚎尤,是則進行長輪詢,假如輪詢失敗則重試伍宦,重試超過三次就睡眠 5 min芽死。
doLongPolling 首先為每個需要輪詢的數(shù)據(jù)的MD5值和最后修改時間用逗號拼接作為請求參數(shù),請求服務(wù)端次洼。/configs/listener 接口关贵,然后將拿到的數(shù)據(jù)更新到內(nèi)存中。
1.6.3 soul admin
我們接著看 /configs/listener 接口 卖毁, 這里也是調(diào)用 doLongPolling 方法揖曾。
它先比較請求的MD5值和當前的配置是否相等和最后修改時間和當前的配置是否相同,假如不同亥啦,則返回該配置組炭剪,假如都相同,這里會先取一把鎖翔脱,然后從數(shù)據(jù)庫里面更新最新的數(shù)據(jù)到內(nèi)存中念祭,這里主要是為了多個 soul admin 假如其中一個更新了,其他的也能同步更新碍侦,很多人會想問,這里數(shù)據(jù)是使用 concurrentHasmap 進行存儲隶糕,為什么還需要加鎖瓷产,這里作者解釋,主要是因為存在多個 soul web 的話枚驻,可能存在同時向數(shù)據(jù)庫請求濒旦,數(shù)據(jù)庫的瞬時壓力增大,這種情況很好理解再登,就像我們設(shè)置 redis 的 key 一樣尔邓,不能同時設(shè)置同個過期時間,否則很容易造成緩存穿透和甚至緩存雪崩锉矢。
這里拿到所有已經(jīng)更新的組信息返回梯嗽。
這里假如有數(shù)據(jù)更新則立即返回數(shù)據(jù)。假如沒有數(shù)據(jù)則調(diào)用 request.startAsync() 方法沽损,這個是什么意思呢灯节,它的作用是HTTP請求不再綁定到HTTP線程,這使我們以后可以使用更少的線程來處理它,我們拿到 asynccontext 后發(fā)給我們的線程池炎疆,這里處理該 http 請求的線程就完成了此次請求卡骂,后續(xù)需要發(fā)送數(shù)據(jù)給客戶端只需要線程池進行通過操作 asynccontext 實現(xiàn)。
最終這里通過線程池的schedule方法形入,設(shè)置延時時長全跨,到預定時間后查看緩存數(shù)據(jù)是否有更新,有則返回給客戶端亿遂。
這里調(diào)用 asyncContext.complete() 整個調(diào)用才算結(jié)束浓若。
1.6.3 總結(jié)
這一期最精彩的莫過于對request.startAsync()對運用,這里假如直接阻塞當前線程崩掘,我們知道 tomcat 處理http 請求對線程是有限的七嫌,直接阻塞假如 soul bootstrap 很多會耗盡請求線程池。這里直接放入 一個調(diào)度線程池苞慢, 這個線程池只有一個線程诵原,當很多請求進來,他會先將它放入 BlockQueue 中挽放,然后一個個處理绍赛,減少了線程池的開銷。