一份针对于新手的多线程实践--进阶篇
前言
在上文《一份針對于新手的多線程實踐》留下了一個問題:
這只是多線程其中的一個用法,相信看到這里的朋友應(yīng)該多它的理解更進一步了。
再給大家留個閱后練習(xí),場景也是類似的:
在 Redis 或者其他存儲介質(zhì)中存放有上千萬的手機號碼數(shù)據(jù),每個號碼都是唯一的,需要在最快的時間內(nèi)把這些號碼全部都遍歷一遍。
有想法感興趣的朋友歡迎在文末留言參與討論??。
網(wǎng)友們的方案
我在公眾號以及其他一些平臺收到了大家的回復(fù),果然是眾人拾柴火焰高啊。
感謝每一位參與的朋友。
其實看了大家的方案大多都想到了數(shù)據(jù)肯定要分段,因為大量的數(shù)據(jù)肯定沒法一次性 load 到內(nèi)存。
但怎么加載就要考慮清楚了,有些人說放在數(shù)據(jù)庫中通過分頁的方式進行加載,然后將每頁的數(shù)據(jù)丟到一個線程里去做遍歷。
其實想法挺不錯的,但有個問題就是:
這樣肯定會導(dǎo)致有一個主線程去遍歷所有的號碼,即便是分頁查詢的那也得全部查詢一遍,效率還是很低。
即便是分頁加載號碼用多線程,那就會涉及到鎖的問題,怎么保證每個線程讀取的數(shù)據(jù)是互不沖突的。
但如果存儲換成 Redis 的 String 結(jié)構(gòu)這樣就更行不通了。
遍歷數(shù)據(jù)方案
有沒有一種利用多線程加載效率高,并且線程之間互相不需要競爭鎖的方案呢?
下面來看看這個方案:
首先在存儲這千萬號碼的時候我們把它的號段單獨提出來并冗余存儲一次。
比如有個號碼是 18523981123 那么就還需要存儲一個號段:1852398。
這樣當(dāng)我們有以下這些號碼時:
18523981123 18523981124 18523981125 13123874321 13123874322 13123874323
我們就還會維護一個號段數(shù)據(jù)為:
1852398 1312387
這樣我想大家應(yīng)該明白下一步應(yīng)當(dāng)怎么做了吧。
在需要遍歷時:
- 通過主線程先把所有的號段加載到內(nèi)存,即便是千萬的號碼號段也頂多幾千條數(shù)據(jù)。
- 遍歷這個號段,將每個號段提交到一個 task 線程中。
- 由這個線程通過號段再去查詢真正的號碼進行遍歷。
- 最后所有的號段都提交完畢再等待所有的線程執(zhí)行完畢即可遍歷所有的號碼。
這樣做的根本原因其實是避免了線程之間加鎖,通過號段可以讓每個線程只取自己那一部分?jǐn)?shù)據(jù)。
可能會有人說,如果號碼持續(xù)增多導(dǎo)致號段的數(shù)據(jù)也達到了上萬甚至幾十萬這怎么辦呢?
那其實也是同樣的思路,可以再把號段進行拆分。
比如之前是 1852398 的號段,那我繼續(xù)拆分為 1852 。
這樣只需要在之前的基礎(chǔ)上再啟動一個線程去查詢子號段即可,有點 fork/join 的味道。
這樣的思路其實也和 JDK1.7 中的 ConcurrentHashMap 類似,定位一個真正的數(shù)據(jù)需要兩次定位。
分布式方案
上面的方案也是由局限性的,畢竟說到底還是一個單機應(yīng)用。沒法擴展;處理的數(shù)據(jù)始終是有上限。
這個上限就和服務(wù)器的配置以及線程數(shù)這些相關(guān),說的高大上一點其實就是垂直擴展增加單機的處理性能。
因此隨著數(shù)據(jù)量的提升我們肯定得需要通過水平擴展的方式才能達到最好的性能,這就是分布式的方案。
假設(shè)我現(xiàn)在有上億的數(shù)據(jù)需要遍歷,但我當(dāng)前的服務(wù)器配置只能支撐一個應(yīng)用啟動 N 個線程 5 分鐘跑5000W 的數(shù)據(jù)。
于是我水平擴展,在三臺服務(wù)器上啟動了三個獨立的進程。假設(shè)一個應(yīng)用能跑 5000W ,那么理論上來說三個應(yīng)用就可以跑1.5億的數(shù)據(jù)了。
但這個的前提還是和上文一樣:每個應(yīng)用只能處理自己的數(shù)據(jù),不能出現(xiàn)加鎖的情況(這樣會大大的降低性能)。
所以我們得對剛才的號段進行分組。
先通過一張圖來直觀的表示這個邏輯:
假設(shè)現(xiàn)在我有 9 個號段,那么我就得按照圖中的方式把數(shù)據(jù)隔離開來。
第一個數(shù)據(jù)給應(yīng)用0,第二個數(shù)據(jù)給應(yīng)用1,第三個數(shù)據(jù)給應(yīng)用2。后面的數(shù)據(jù)以此類推(就是一個簡單的取模運算)。
這樣就可以將號段均勻的分配給不同的應(yīng)用來進行處理,然后每個應(yīng)用再按照上文提到的將分配給自己的號段丟到線程池中由具體的線程去查詢、遍歷即可。
分布式帶來的問題
這樣看似沒啥問題,但一旦引入了分布式之后就不可避免的會出現(xiàn) CAP 的取舍,這里不做過多討論,感興趣的朋友可以自行搜索。
首先要解決的一個問題就是:
這三個應(yīng)用怎么知道它自己應(yīng)該取哪些號段的數(shù)據(jù)呢?比如 0 號應(yīng)用就取 0 3 6(這個相當(dāng)于號段的下標(biāo)),難道在配置文件里配置嘛?
那如果數(shù)據(jù)量又增大了,對應(yīng)的機器數(shù)也增加到了 5 臺,那自然 0 號應(yīng)用就不是取 0 3 6 了(取模之后數(shù)據(jù)會變)。
所以我們得需要一個統(tǒng)一的調(diào)度來分配各個應(yīng)用他們應(yīng)當(dāng)取哪些號段,這也就是數(shù)據(jù)分片。
假設(shè)我這里有一個統(tǒng)一的分配中心,他知道現(xiàn)在有多少個應(yīng)用來處理數(shù)據(jù)。還是假設(shè)上文的三個應(yīng)用吧。
在真正開始遍歷數(shù)據(jù)的時候,這個分配中心就會去告訴這三個應(yīng)用:
你們要開始工作了啊,0 號應(yīng)用你的工作內(nèi)容是 0 3 6,1 號應(yīng)用你的工作內(nèi)容是 1 4 7,2 號應(yīng)用你的工作內(nèi)容是 2 5 8。
這樣各個應(yīng)用就知道他們所應(yīng)當(dāng)處理的數(shù)據(jù)了。
當(dāng)我們新增了一個應(yīng)用來處理數(shù)據(jù)時也很簡單,同樣這個分配中心知道現(xiàn)在有多少臺應(yīng)用會工作。
他會再拿著現(xiàn)有的號段對 4(3+1臺應(yīng)用) 進行取模然后對數(shù)據(jù)進行重新分配,這樣就可以再次保證數(shù)據(jù)分配均勻了。
只是分配中心如何知道有多少應(yīng)用呢,其實也簡單,只要中心和應(yīng)用之間通信就可以了。比如啟動的時候調(diào)用分配中心的接口即可。
上面提到的這個分配中心其實就是一個常見的定時任務(wù)的分布式調(diào)度中心,由它來統(tǒng)一發(fā)起調(diào)度,當(dāng)然分片只是它其中的一個功能而已(關(guān)于調(diào)度中心之后有興趣再細說)。
總結(jié)
本次探討了多線程的更多應(yīng)用方式,如要是如何高效的運行。最主要的一點其實就是盡量的避免加鎖。
同時對分布式水平擴展談了一些處理建議,本次也是難得的一行代碼都沒貼,大家感興趣的話在后面更新相關(guān)代碼。
也歡迎大家留言討論。?
你的點贊與轉(zhuǎn)發(fā)是最大的支持。
轉(zhuǎn)載于:https://www.cnblogs.com/crossoverJie/p/9880830.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的一份针对于新手的多线程实践--进阶篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正则实现二代身份证号码验证详解
- 下一篇: volatile 关键字解析