前言
在上文《一份針對於新手的多線程實踐》留下了一個問題:
這只是多線程其中的一個用法,相信看到這裏的朋友應該多它的理解更進一步了。
再給大家留個閱後練習,場景也是類似的:
在 Redis 或者其他存儲介質中存放有上千萬的手機號碼數據,每個號碼都是唯一的,需要在最快的時間內把這些號碼全部都遍歷一遍。
有想法感興趣的朋友歡迎在文末留言參與討論🤔🤨。
網友們的方案
我在公衆號以及其他一些平臺收到了大家的回覆,果然是衆人拾柴火焰高
啊。
感謝每一位參與的朋友。
其實看了大家的方案大多都想到了數據肯定要分段,因爲大量的數據肯定沒法一次性 load
到內存。
但怎麼加載就要考慮清楚了,有些人說放在數據庫中通過分頁的方式進行加載,然後將每頁的數據丟到一個線程裏去做遍歷。
其實想法挺不錯的,但有個問題就是:
這樣肯定會導致有一個主線程去遍歷所有的號碼,即便是分頁查詢的那也得全部查詢一遍,效率還是很低。
即便是分頁加載號碼用多線程,那就會涉及到鎖的問題,怎麼保證每個線程讀取的數據是互不衝突的。
但如果存儲換成 Redis
的 String
結構這樣就更行不通了。
遍歷數據方案
有沒有一種利用多線程加載效率高,並且線程之間互相不需要競爭鎖的方案呢?
下面來看看這個方案:
首先在存儲這千萬號碼的時候我們把它的號段單獨提出來並冗餘存儲一次。
比如有個號碼是 18523981123
那麼就還需要存儲一個號段:1852398
。
這樣當我們有以下這些號碼時:
18523981123 18523981124 18523981125 13123874321 13123874322 13123874323
我們就還會維護一個號段數據爲:
1852398 1312387
這樣我想大家應該明白下一步應當怎麼做了吧。
在需要遍歷時:
- 通過主線程先把所有的號段加載到內存,即便是千萬的號碼號段也頂多幾千條數據。
- 遍歷這個號段,將每個號段提交到一個
task
線程中。 - 由這個線程通過號段再去查詢真正的號碼進行遍歷。
- 最後所有的號段都提交完畢再等待所有的線程執行完畢即可遍歷所有的號碼。
這樣做的根本原因其實是避免了線程之間加鎖,通過號段可以讓每個線程只取自己那一部分數據。
可能會有人說,如果號碼持續增多導致號段的數據也達到了上萬甚至幾十萬這怎麼辦呢?
那其實也是同樣的思路,可以再把號段進行拆分。
比如之前是 1852398
的號段,那我繼續拆分爲 1852
。
這樣只需要在之前的基礎上再啓動一個線程去查詢子號段即可,有點 fork/join
的味道。
這樣的思路其實也和 JDK1.7 中的 ConcurrentHashMap 類似,定位一個真正的數據需要兩次定位。
分佈式方案
上面的方案也是由侷限性的,畢竟說到底還是一個單機應用。沒法擴展;處理的數據始終是有上限。
這個上限就和服務器的配置以及線程數這些相關,說的高大上一點其實就是垂直擴展
增加單機的處理性能。
因此隨着數據量的提升我們肯定得需要通過水平擴展
的方式才能達到最好的性能,這就是分佈式的方案。
假設我現在有上億的數據需要遍歷,但我當前的服務器配置只能支撐一個應用啓動 N 個線程 5 分鐘跑5000W
的數據。
於是我水平擴展,在三臺服務器上啓動了三個獨立的進程。假設一個應用能跑 5000W ,那麼理論上來說三個應用就可以跑1.5億的數據了。
但這個的前提還是和上文一樣:每個應用只能處理自己的數據,不能出現加鎖的情況(這樣會大大的降低性能)。
所以我們得對剛纔的號段進行分組。
先通過一張圖來直觀的表示這個邏輯:
假設現在我有 9 個號段,那麼我就得按照圖中的方式把數據隔離開來。
第一個數據給應用0,第二個數據給應用1,第三個數據給應用2。後面的數據以此類推(就是一個簡單的取模運算)。
這樣就可以將號段均勻的分配給不同的應用來進行處理,然後每個應用再按照上文提到的將分配給自己的號段丟到線程池中由具體的線程去查詢、遍歷即可。
分佈式帶來的問題
這樣看似沒啥問題,但一旦引入了分佈式之後就不可避免的會出現 CAP
的取捨,這裏不做過多討論,感興趣的朋友可以自行搜索。
首先要解決的一個問題就是:
這三個應用怎麼知道它自己應該取哪些號段的數據呢?比如 0 號應用就取 0 3 6
(這個相當於號段的下標),難道在配置文件裏配置嘛?
那如果數據量又增大了,對應的機器數也增加到了 5 臺,那自然 0 號應用就不是取 0 3 6
了(取模之後數據會變)。
所以我們得需要一個統一的調度來分配各個應用他們應當取哪些號段,這也就是數據分片。
假設我這裏有一個統一的分配中心,他知道現在有多少個應用來處理數據。還是假設上文的三個應用吧。
在真正開始遍歷數據的時候,這個分配中心就會去告訴這三個應用:
你們要開始工作了啊,0 號應用你的工作內容是 0 3 6,1 號應用你的工作內容是 1 4 7,2 號應用你的工作內容是 2 5 8。
這樣各個應用就知道他們所應當處理的數據了。
當我們新增了一個應用來處理數據時也很簡單,同樣這個分配中心知道現在有多少臺應用會工作。
他會再拿着現有的號段對 4(3+1臺應用) 進行取模然後對數據進行重新分配,這樣就可以再次保證數據分配均勻了。
只是分配中心如何知道有多少應用呢,其實也簡單,只要中心和應用之間通信就可以了。比如啓動的時候調用分配中心的接口即可。
上面提到的這個分配中心其實就是一個常見的定時任務的分佈式調度中心,由它來統一發起調度,當然分片只是它其中的一個功能而已(關於調度中心之後有興趣再細說)。
總結
本次探討了多線程的更多應用方式,如要是如何高效的運行。最主要的一點其實就是儘量的避免加鎖。
同時對分佈式水平擴展談了一些處理建議,本次也是難得的一行代碼都沒貼,大家感興趣的話在後面更新相關代碼。
也歡迎大家留言討論。😄
你的點贊與轉發是最大的支持。