又被夺命连环问了!从一道关于定时任务的面试题说起。
你好呀,我是歪歪。
定時(shí)任務(wù),大家在開發(fā)的過程中肯定都是接觸過的。
歪師傅面試的時(shí)候關(guān)于定時(shí)任務(wù)一般都會(huì)問這樣的一個(gè)問題:在實(shí)際開發(fā)的過程中,你們是如何避免定時(shí)任務(wù)重復(fù)執(zhí)行的呢?
什么意思呢?
我給你上個(gè)圖你就明白了。
假設(shè)我們有個(gè)訂單服務(wù)的微服務(wù),它部署在兩臺(tái)機(jī)器上:
這是一個(gè)再正常不過的部署方案了吧。
現(xiàn)在有一個(gè)需求來了:要從數(shù)據(jù)庫里面獲取前一日狀態(tài)為成功的訂單,然后把這些訂單一筆筆的調(diào)用其他服務(wù)的接口,通知給他們。
寫代碼的時(shí)候很簡單,基于 Quartz 框架,咔嚓幾下就能搞出一個(gè)定時(shí)任務(wù)來,偽代碼如下:
//每天10點(diǎn)觸發(fā)一次
@Scheduled(cron?=?"0?0?10?*?*??")
public?void?sendOrder()?{
????//查詢前一日狀態(tài)為成功的訂單
????List<Order>?orderList?=?selectSuccessOrder();
????for?(Order?order?:?orderList)?{
????????//發(fā)送訂單到數(shù)據(jù)分析服務(wù)
????????sendOrder(order);
????}
}
測(cè)試的時(shí)候也非常的正常,看不出任何毛病。
但是一上生產(chǎn)就完?duì)僮恿耍瑸槭裁茨兀?/p>
因?yàn)闇y(cè)試環(huán)境一般來說就只部署一臺(tái)服務(wù)器,但是生產(chǎn)環(huán)境是多臺(tái)呀:
每天 10 點(diǎn)一到,兩臺(tái)機(jī)器都跑起來了...
同樣的邏輯跑了兩次,一下就瓜起了澀,這肯定不是我們想要的結(jié)果。
問:這個(gè)情況你怎么處理?
在實(shí)際開發(fā)的過程中,我理解大家理論上都會(huì)遇到這個(gè)問題的。
歪師傅當(dāng)年還是一個(gè)小萌新,第一次遇到這個(gè)問題的時(shí)候,是怎么考慮的呢?
摳了摳腦袋,想到一個(gè)自己覺得非常靠譜的解決方案。
各個(gè)微服務(wù)提供接口,接口內(nèi)部實(shí)現(xiàn)定時(shí)任務(wù)的業(yè)務(wù)邏輯。然后抽離出一個(gè)專門的定時(shí)任務(wù)微服務(wù),在這個(gè)服務(wù)中開發(fā)定時(shí)任務(wù),來調(diào)用對(duì)應(yīng)的接口:
由于“定時(shí)任務(wù)微服務(wù)”只部署在一臺(tái)服務(wù)器上,所以當(dāng)定時(shí)任務(wù)的時(shí)間一到,只會(huì)發(fā)起一次 RPC 調(diào)用,具體調(diào)用哪一臺(tái)服務(wù),由 RPC 的負(fù)載均衡來決定。
從而規(guī)避了前面提到的“觸發(fā)兩次”的問題。
當(dāng)時(shí)我還覺得微服務(wù)的思想還是真是厲害,這樣一抽離之后,業(yè)務(wù)代碼和定時(shí)邏輯徹底分離開來,橫向擴(kuò)展也不需要考慮“多次觸發(fā)”的問題:
但是,問題隨著就來了:定時(shí)任務(wù)服務(wù)只部署了一臺(tái),它有單點(diǎn)風(fēng)險(xiǎn)啊,它掛了,所有的定時(shí)任務(wù)不就都掛了嗎?
我知道在有的公司,實(shí)際情況就是這樣的,知道服務(wù)有單點(diǎn)風(fēng)險(xiǎn),但是評(píng)估下來,覺得是可以接受的,大不了就是做好服務(wù)監(jiān)控,出了問題就趕緊重啟一波服務(wù)。
所以遇到這個(gè)問題的解決方案就是:不管它。
但是,朋友,面試的時(shí)候你能這樣回答嗎,你是去調(diào)侃面試官的嗎?
所以,該怎么辦?
單點(diǎn)問題,很好解決,針對(duì)“定時(shí)任務(wù)服務(wù)”多部署一臺(tái)服務(wù)器就行了:
但是,調(diào)用關(guān)系怎么辦呢?
時(shí)間一到,咔的一下,兩臺(tái)“定時(shí)任務(wù)服務(wù)”都跑起來了,都對(duì)下游發(fā)起了 RPC 調(diào)用,這不又出現(xiàn)了前面這樣“調(diào)用兩次”的問題嗎:
開始套娃了,你說怎么辦?
這個(gè)時(shí)候歪師傅又摳了摳腦袋,又想到一個(gè)自己覺得非常靠譜的解決方案。
關(guān)于這個(gè)問題,我先換個(gè)殼問你:如果有多個(gè)請(qǐng)求過來,但是我們同一時(shí)間只想讓一個(gè)請(qǐng)求正常執(zhí)行,請(qǐng)問你怎么辦?
一般來說我們都會(huì)想到加鎖嘛。
單機(jī)的話,什么 synchronized,ReentrantLock 這些玩意就使勁兒往上懟。
多臺(tái)服務(wù)就上分布式鎖嘛,Redis、Zookeeper 拿出來秀一秀嘛,對(duì)不對(duì)?
比如,如果我們用 Redis,怎么做?
在發(fā)起 RPC 調(diào)用之前先從 Redis 里面拿鎖,多臺(tái)機(jī)器,誰拿到了,誰就可以執(zhí)行:
//每天10點(diǎn)觸發(fā)一次
@Scheduled(cron?=?"0?0?10?*?*??")
public?void?sendOrder()?{
????//獲取redis鎖
????if(SET?key?value?expireTime?nx){
????????//拿到鎖的才能調(diào)用訂單服務(wù)發(fā)送成功訂單邏輯的接口
????????callOrderRPC();???
????}
}
這樣即使某一臺(tái)服務(wù)器上的服務(wù)掛了,另外一臺(tái)也能確保定時(shí)任務(wù)按時(shí)觸發(fā),并表示非常開心:很好,沒人和我搶鎖了。
或者說基于 zookeeper 來做。
比如我們定時(shí)任務(wù)的服務(wù)啟動(dòng)的時(shí)候,以服務(wù)名稱維度向 zk 申請(qǐng)一個(gè)臨時(shí)節(jié)點(diǎn)。
誰申請(qǐng)成功了,誰就算加鎖成功了,雖然到點(diǎn)之后每個(gè)定時(shí)任務(wù)都會(huì)按時(shí)觸發(fā),但是和 Redis 同理,只有拿到鎖的實(shí)例才能執(zhí)行定時(shí)任務(wù)。
沒有拿到鎖的怎么辦呢?
監(jiān)聽這個(gè)臨時(shí)節(jié)點(diǎn),處于隨時(shí)待命狀態(tài)。如果當(dāng)前持有鎖的服務(wù)掛了,那么臨時(shí)節(jié)點(diǎn)也就沒了,相當(dāng)于鎖就釋放了,就可以上手搶鎖了。
搶到鎖,就可以執(zhí)行定時(shí)任務(wù),這樣也能保證高可用。
如果是面試,針對(duì)“避免定時(shí)任務(wù)重復(fù)執(zhí)行”能回答到分布式鎖這里,我認(rèn)為就可以了。
但是,朋友,這可是面試,面試一般是出連招的。
如果我突然畫風(fēng)一轉(zhuǎn),順勢(shì)提出下一個(gè)問題:
用分布式鎖,可以通過只讓一臺(tái)機(jī)器運(yùn)行的方式解決重復(fù)運(yùn)行的問題。現(xiàn)在我換個(gè)場(chǎng)景,問問題,如果我昨日成功的訂單數(shù)據(jù)量比較多,假設(shè)有 100w 筆吧,如果只在一臺(tái)機(jī)器上跑,即使開啟多線程,也需要很長的時(shí)間,而且是一臺(tái)機(jī)器忙的不行,不太機(jī)器在旁邊閑的不行。如果我想要充分把機(jī)器利用起來,讓兩臺(tái)機(jī)器都來處理這 100w 筆訂單,各自處理 50w 條,時(shí)間不就縮短了嗎?
就像是這樣:
請(qǐng)問,閣下又該如何應(yīng)對(duì)?
ElasticJob
好了,前面鋪墊了這么多,終于要引出 ElasticJob 這個(gè)玩意了。
這是官方文檔的地址:
https://shardingsphere.apache.org/elasticjob/current/cn/overview/
其中有一個(gè)章節(jié)叫做“彈性調(diào)度”:
彈性調(diào)度是 ElasticJob 最重要的功能,也是這款產(chǎn)品名稱的由來。 它是一款能夠讓任務(wù)通過分片進(jìn)行水平擴(kuò)展的任務(wù)處理系統(tǒng)。
從關(guān)于“分片”的描述中,我們知道也許能在這里找到問題的答案。
雖然答案就在眼前,但是別猴急。按照歪師傅的風(fēng)格,還是得先上個(gè) Demo 作為引子,給你抽絲剝個(gè)繭。
這里順便吐槽一句官方文檔:
你這個(gè)“快速入門”寫的是什么玩意,根本就不能用好吧?
quick start 不能拿來即用,對(duì)于本白嫖黨來說,是很難受的,好嗎。
害得我還得自己摸索一下,還好整體并不復(fù)雜,你按照歪師傅給你提供的“快速入門”,五分鐘足夠搭個(gè) Demo 了。
首先,新建一個(gè) Spring Boot 項(xiàng)目,在 pom 文件中加入相關(guān)引用:
<dependency>
????<groupId>org.apache.shardingsphere.elasticjob</groupId>
????<artifactId>elasticjob-lite-spring-boot-starter</artifactId>
????<version>3.0.1</version>
</dependency>
然后實(shí)現(xiàn) SimpleJob 接口,自定義一個(gè)定時(shí)任務(wù):
package?com.example.elasticjobtest;
@Slf4j
@Component
public?class?SpringBootJob?implements?SimpleJob?{
????@Override
????public?void?execute(ShardingContext?shardingContext)?{
????????log.info("SpringBootJob作業(yè),分片總數(shù)是【{}】,當(dāng)前分片是【{}】,分片參數(shù)是【{}】",
????????????????shardingContext.getShardingTotalCount(),
????????????????shardingContext.getShardingItem(),
????????????????shardingContext.getShardingParameter());
????}
}
接著在 application.yml 里面添加配置:
elasticjob:
??#?注冊(cè)中心配置
??regcenter:
????serverlists:?127.0.0.1:2181
????#?ZooKeeper?的命名空間
????namespace:?why-elastic-job
??#?作業(yè)配置
??jobs:
????springJob:?#?job的名稱
??????elasticJobClass:?com.example.elasticjobtest.SpringBootJob
??????cron:?0/5?*?*?*?*??
??????shardingTotalCount:?2
??????shardingItemParameters:?0=Beijing,1=Shanghai
就這幾行代碼,Demo 就算搭完了。
你自己說,這整個(gè)流程是不是五分鐘夠夠的了?
在把服務(wù)啟動(dòng)起來之前,針對(duì) application.yml 的配置,我先多 BB 幾句。
里面這兩個(gè)玩意是什么東西呢:
可以參考官方文檔中的描述:
https://shardingsphere.apache.org/elasticjob/current/cn/user-manual/configuration/
shardingTotalCount 叫做作業(yè)分片總數(shù),這個(gè)概念非常重要,理解了這個(gè)概念,就理解了 ElasticJob 的核心理念,先按下不表。
shardingItemParameters 叫做個(gè)性化分片參數(shù),我這里寫的是 0=Beijing,1=Shanghai,看起來很奇怪對(duì)不對(duì),怎么突然冒出了北京和上海呢?
因?yàn)檫@也是官方文檔中的案例:
這只是實(shí)例而已,當(dāng)你理解了這個(gè)概念的用途之后,就可以按照自己的需求進(jìn)行“個(gè)性化”配置。
Demo 跑起來
這個(gè)是 ElasticJob 的架構(gòu)示意圖:
可以看到它選擇了 Zookeeper 做為自己的注冊(cè)中心,所以在啟動(dòng) Demo 之前,需要你把你本地的 Zookeeper 啟動(dòng)起來。
然后把 Demo 運(yùn)行起來,觀察日志輸出:
你會(huì)發(fā)現(xiàn)每隔 5s 就會(huì)輸出這樣的日志:
2023-12-16?16:31:45.020?SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【0】,分片參數(shù)是【Beijing】
2023-12-16?16:31:45.020?SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【1】,分片參數(shù)是【Shanghai】
怎么樣,看到日志輸出之后是不是稍微品出了點(diǎn)淡淡的味道,就是那種雖然不知道怎么回事,但是總感覺馬上就摸到門道的感覺。
保持住這種感覺,歪師傅馬上就讓你摸到門把手了。
為了模擬多個(gè)服務(wù)部署的情況,所以我們需要再多啟動(dòng)一個(gè)服務(wù)。
在 Idea 里面點(diǎn)擊這個(gè):
然后把“Allow multiple instances(運(yùn)行多實(shí)例運(yùn)行)”勾選上:
修改一下服務(wù)端口,避免端口沖突:
接著再次啟動(dòng) Demo,觀察一下日志:
標(biāo)號(hào)為 ① 的地方是僅一臺(tái)服務(wù)器運(yùn)行的情況,兩個(gè)分片都在這一個(gè)服務(wù)器上運(yùn)行。
標(biāo)號(hào)為 ② 和 ③ 的地方是兩臺(tái)服務(wù)器都運(yùn)行起來的情況,同樣的代碼、同樣的配置,跑在不同的端口而已。
一臺(tái)的日志輸出是這樣的:
SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【1】,分片參數(shù)是【Shanghai】
SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【1】,分片參數(shù)是【Shanghai】
另外一臺(tái)的日志輸出是這樣的:
SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【0】,分片參數(shù)是【Beijing】
SpringBootJob作業(yè),分片總數(shù)是【2】,當(dāng)前分片是【0】,分片參數(shù)是【Beijing】
可以看到,每隔五秒鐘兩臺(tái)服務(wù)器都同時(shí)觸發(fā)了定時(shí)任務(wù),但是一臺(tái)拿到的參數(shù)是 Shanghai,一臺(tái)拿到的參數(shù)是 Beijing。
這個(gè)時(shí)候我們?cè)倩厝タ疵嬖嚬俚倪@個(gè)問題:
假設(shè)有 100w 筆吧,如果只在一臺(tái)機(jī)器上跑,即使開啟多線程,也需要很長的時(shí)間,而且是一臺(tái)機(jī)器忙的不行,不太機(jī)器在旁邊閑的不行。如果我想要充分把機(jī)器利用起來,讓兩臺(tái)機(jī)器都來處理這 100w 筆訂單,各自處理 50w 條,時(shí)間不就縮短了嗎?
然后我再給你上個(gè)圖:
每個(gè)機(jī)器上運(yùn)行的代碼是一樣的,但是通過 ElasticJob 能讓每個(gè)機(jī)器在運(yùn)行定時(shí)任務(wù)的時(shí)候,拿到不一樣的參數(shù)。
基于這個(gè)不一樣的參數(shù),我們就能搞很多事情了嘛。
比如 100w 數(shù)據(jù),分為兩組,一組 50w 條。假設(shè) ID 是連續(xù)自增的,是不是可以這樣判斷奇偶數(shù):
偶數(shù):id % 2 == 0
奇數(shù):id % 2 == 1
在這個(gè)表達(dá)式里面,每個(gè)數(shù)據(jù)的 id 是確定的,而這個(gè)“2”,你看它像不像是我們的“分片數(shù)”?至于這個(gè)“0”和“1”,是不是可以通過我們的“個(gè)性化分片參數(shù)”傳遞進(jìn)來?
id % 分片數(shù) == 個(gè)性化分片參數(shù)
比如我們寫個(gè)這樣的代碼:
然后把作業(yè)配置改成這樣的:
然后啟動(dòng)兩個(gè)服務(wù),我們觀察一下日志輸出:
一臺(tái)機(jī)器處理的是 “1,3,5,7,9”,一臺(tái)機(jī)器處理的是“0,2,4,6,8”
剛剛面試官的問題是啥來著?
兩臺(tái)機(jī)器處理 100w 筆訂單,各自處理 50w 條?
這不就實(shí)現(xiàn)了嗎?
再給你看一個(gè)神奇的東西,假設(shè)我在運(yùn)行時(shí)把 shardingTotalCount 修改為 3,即分片數(shù)變成 3,對(duì)應(yīng)的自定義參數(shù)也進(jìn)行對(duì)應(yīng)的修改,會(huì)發(fā)生什么事情呢?
按照我們之前的這個(gè)邏輯:
id % 分片數(shù) == 個(gè)性化分片參數(shù)
0 到 9 這十個(gè)數(shù)字分別對(duì) 3 取模,那么就會(huì)分成下面這三組:
第一組:0,3,6,9 第二組:1,4,7 第三組:2,5,8
這個(gè)沒有任何毛病,對(duì)不對(duì)?
然后還需要特別注意的是,我說的是“在運(yùn)行時(shí)”修改。
怎么修改?
很簡單,ElasticJob 其實(shí)提供了對(duì)應(yīng)的管理后臺(tái)頁面可以進(jìn)行參數(shù)修改,但是我這里偷個(gè)懶,難得去部署對(duì)應(yīng)的管理后臺(tái),,準(zhǔn)備換個(gè)簡單的思路。
因?yàn)榍懊嬲f了,ElasticJob 使用的是 zk 做為自己的注冊(cè)中心,我直接用工具連接上 zk,然后修改 zk 節(jié)點(diǎn)就行了。
我是怎么知道修改 zk 的哪個(gè)節(jié)點(diǎn)的呢?
別著急,等下就講,歪師傅先帶你看效果。
我這里用的工具是 ZooInspector,修改之后直接點(diǎn)擊保存:
然后,朋友們,注意了,看日志輸出
為了讓你看的更加清楚,我把關(guān)鍵日志單獨(dú)拿出來:
第一臺(tái)機(jī)器上的日志是這樣的:
分片總數(shù)是【3】,當(dāng)前分片是【1】,分片參數(shù)是【1】,處理的數(shù)據(jù) date=【1,4,7,】
第二臺(tái)機(jī)器上的日志是這樣的:
分片總數(shù)是【3】,當(dāng)前分片是【0】,分片參數(shù)是【0】,處理的數(shù)據(jù) date=【0,3,6,9,】
分片總數(shù)是【3】,當(dāng)前分片是【2】,分片參數(shù)是【2】,處理的數(shù)據(jù) date=【2,5,8,】
和我們前面推理的結(jié)果一模一樣。
好,到這里就可以解答我的一個(gè)“按下不表”了。
首先,shardingTotalCount 叫做作業(yè)分片總數(shù),在我前面的例子中,作業(yè)分片總數(shù)一共是 3 片:
第一組(第一片):0,3,6,9 第二組(第二片):1,4,7 第三組(第三片):2,5,8
分成三片之后,Elasticjob 怎么知道每一片應(yīng)該處理哪些數(shù)據(jù)呢?
它不知道,它也不用知道。它只需要告訴每一臺(tái)服務(wù)器:“來,哥們,給你一個(gè)號(hào)你拿著。你們這波一共有多少多少個(gè)人,你是第幾片。”
就完事了。
因?yàn)椤白蛉粘晒Φ挠唵巍边@個(gè)總的要處理的數(shù)據(jù)是不變的,所有每一臺(tái)服務(wù)器知道一共要把這批數(shù)據(jù)分成幾片,自己是第幾片后,通過代碼就能拿到對(duì)應(yīng)的該處理的數(shù)據(jù)。
然后你再去看官方描述中關(guān)于“分片項(xiàng)”你大概就能知道這到底是個(gè)啥玩意了:
有的哥們比較猛,一次拿到兩個(gè)號(hào),也沒關(guān)系,就是多處理一份數(shù)據(jù)嘛。這種情況就適用于兩臺(tái)機(jī)器的性能不一致的情況。
但是我用這個(gè)案例并不是為了引出“性能不一致”這種極少數(shù)的情況,而是為了這個(gè)...
當(dāng)我再啟動(dòng)一個(gè)新的服務(wù)器,當(dāng)?shù)谌_(tái)服務(wù)器加入之后,我們啥也沒干,它自己就開始處理任務(wù)了。
3 個(gè)分片,一臺(tái)服務(wù)器處理一個(gè)分片的數(shù)據(jù)。
能自動(dòng)加入,就能自動(dòng)退出,所以假設(shè)我把一臺(tái)服務(wù)給關(guān)閉了:
從日志可以看出來,數(shù)據(jù)并沒有丟。
第一臺(tái)機(jī)器把本來該在下線的這臺(tái)服務(wù)器上處理的數(shù)據(jù)給接管了:
分片總數(shù)是【3】,當(dāng)前分片是【2】,分片參數(shù)是【2】,處理的數(shù)據(jù) date=【2,5,8,】
分片總數(shù)是【3】,當(dāng)前分片是【0】,分片參數(shù)是【0】,處理的數(shù)據(jù) date=【0,3,6,9,】
好了,到這里,基本功能就算演示完成,可以適當(dāng)?shù)捻懫鹨恍┱坡暳恕?/p>
啥原理啊?
其實(shí)關(guān)于原理,官方文檔上也按照步驟進(jìn)行了比較詳細(xì)的說明:
https://shardingsphere.apache.org/elasticjob/current/cn/features/elastic/
如果你不了解 zk 的大致工作原理、節(jié)點(diǎn)特性、監(jiān)聽機(jī)制啥的,后面肯定會(huì)看得比較懵逼。
所以需要先去補(bǔ)一下這方面的信息,對(duì)于這部分的描述和源碼的解讀有很大幫助。
如果你能大致理解 zk 的工作原理,那么整體讀下來其實(shí)沒有什么特別難以理解的地方,如果要深入理解每一個(gè)步驟的話,那肯定要讀一下源碼的。
步驟都有了,去找對(duì)應(yīng)的源碼,不就是按圖索驥,手拿把掐的事情嗎。
在閱讀源碼之前,還有一個(gè)非常重要的東西要鋪墊一下,前面也說了:基于 zk 做的注冊(cè)中心。
所以你必須要了解“注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu)”是怎么樣的,每個(gè)節(jié)點(diǎn)是干啥的,才能理解代碼里面操作 zk 節(jié)點(diǎn)的時(shí)候,到底是什么含義。
關(guān)于注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu),文檔上也有介紹:
我覺得這個(gè)還是非常重要的,所以我多啰嗦幾句,主要給你看看實(shí)際的數(shù)據(jù)是怎么樣的。
還是以我本地啟動(dòng)三個(gè)服務(wù)為例。
啟動(dòng)起來之后,看 zk 上注冊(cè)了這些節(jié)點(diǎn):
其中“why-elastic-job”和“springJob”分別是我們寫在 application.yml 里面的 ZooKeeper 的命名空間和 Job 名稱:
config 節(jié)點(diǎn)
config 節(jié)點(diǎn)里面是作業(yè)配置信息,以 YAML 格式存儲(chǔ):
可以看到節(jié)點(diǎn)里面實(shí)際的值比我們配置的多,因?yàn)橛泻芏嗄J(rèn)項(xiàng)。每個(gè)默認(rèn)項(xiàng)是干啥的,就自己去研究吧。
前面我說的“運(yùn)行時(shí)修改”,就修改的是這個(gè)地方信息。
我為什么知道改這里?
還不是官網(wǎng)告訴我的。
instances 節(jié)點(diǎn)
該節(jié)點(diǎn)是作業(yè)運(yùn)行實(shí)例信息,子節(jié)點(diǎn)是當(dāng)前作業(yè)運(yùn)行實(shí)例的主鍵。
作業(yè)運(yùn)行實(shí)例主鍵由作業(yè)運(yùn)行服務(wù)器的 IP 地址和 PID 構(gòu)成。
作業(yè)運(yùn)行實(shí)例主鍵均為臨時(shí)節(jié)點(diǎn),當(dāng)作業(yè)實(shí)例上線時(shí)注冊(cè),下線時(shí)自動(dòng)清理。注冊(cè)中心可以監(jiān)控這些節(jié)點(diǎn)的變化,來協(xié)調(diào)分布式作業(yè)的分片以及高可用。
具體到我們這個(gè)案例中,是這樣的:
instances 下面有三個(gè)子節(jié)點(diǎn),代表有三個(gè)微服務(wù)。
假設(shè)我停止運(yùn)行一個(gè)服務(wù),由于是 zk 的臨時(shí)節(jié)點(diǎn),這個(gè)地方就會(huì)變成 2 個(gè):
sharding 節(jié)點(diǎn)
作業(yè)分片信息,子節(jié)點(diǎn)是分片項(xiàng)序號(hào),從零開始,至分片總數(shù)減一。比如我們這里就是 0 到 2:
分片項(xiàng)序號(hào)的子節(jié)點(diǎn)存儲(chǔ)詳細(xì)信息,每個(gè)分片項(xiàng)下的子節(jié)點(diǎn)用于控制和記錄分片運(yùn)行狀態(tài):
sharding-0-instance:192.168.2.16@-@4964 sharding-1-instance:192.168.2.16@-@2224 sharding-2-instance:192.168.2.16@-@4964
可以看到 0,2 分片是運(yùn)行在同一個(gè) instance 上的,這一點(diǎn)和日志是匹配的:
sharding 下除了 instance 節(jié)點(diǎn)外,可能還有其他的節(jié)點(diǎn),詳細(xì)信息說明如下:
servers 節(jié)點(diǎn)
作業(yè)服務(wù)器信息,子節(jié)點(diǎn)是作業(yè)服務(wù)器的 IP 地址。
可在 IP 地址節(jié)點(diǎn)寫入 DISABLED 表示該服務(wù)器禁用。
在新的云原生架構(gòu)下,servers 節(jié)點(diǎn)大幅弱化,僅包含控制服務(wù)器是否可以禁用這一功能。
為了更加純粹的實(shí)現(xiàn)作業(yè)核心,servers 功能未來可能刪除,控制服務(wù)器是否禁用的能力應(yīng)該下放至自動(dòng)化部署系統(tǒng)。
leader 節(jié)點(diǎn)
作業(yè)服務(wù)器主節(jié)點(diǎn)信息,下面有三個(gè)子節(jié)點(diǎn):
election:用于主節(jié)點(diǎn)選舉 sharding:用于分片 failover:用于失效轉(zhuǎn)移處理
除了節(jié)點(diǎn)介紹外,在官網(wǎng)描述上有這樣的一句話:
換句話說就是,如果你想了解作業(yè),那這個(gè)節(jié)點(diǎn)是很重要的。看源碼的時(shí)候,需要特別關(guān)注對(duì)于 leader 節(jié)點(diǎn)下的操作。
在我們的案例中,instance 里面的信息是這樣的:
表示這個(gè)節(jié)點(diǎn)是主節(jié)點(diǎn)。
源碼
知道了 zk 上每個(gè)節(jié)點(diǎn)的用處,看源碼的時(shí)候比著看就行了。
源碼比較多,歪師傅這里只能帶著你做個(gè)非常簡單的導(dǎo)讀。
首先,因?yàn)楹芏噙壿嫸际腔?zk 節(jié)點(diǎn)在來做的,所以最重要的是各種各樣的 zk 節(jié)點(diǎn)監(jiān)聽器,ElasticJob 在啟動(dòng)時(shí),會(huì)執(zhí)行這個(gè)方法,開啟監(jiān)聽器:
org.apache.shardingsphere.elasticjob.kernel.internal.listener.ListenerManager#startAllListeners
比如前面說的這個(gè)節(jié)點(diǎn):
如果這個(gè)節(jié)點(diǎn)存在,則說明需要重新分片,對(duì)應(yīng)的監(jiān)聽器是這個(gè):
shardingListenerManager.start();
那么什么時(shí)候會(huì)觸發(fā)“重新分片”呢?
如果分片總數(shù)變化,或作業(yè)服務(wù)器節(jié)點(diǎn)上下線或啟用/禁用,以及主節(jié)點(diǎn)選舉,會(huì)觸發(fā)設(shè)置重分片標(biāo)記 作業(yè)在下次執(zhí)行時(shí)使用主節(jié)點(diǎn)重新分片,且中間不會(huì)被打斷作業(yè)執(zhí)行時(shí)不會(huì)觸發(fā)分片
所以在 shardingListenerManager 監(jiān)聽器里面我們可以看到這兩個(gè)邏輯:
滿足條件之后,就會(huì)執(zhí)行設(shè)置重新分片標(biāo)識(shí)的代碼:
shardingService.setReshardingFlag();
該方法里面,創(chuàng)建了一個(gè)新的節(jié)點(diǎn):
這個(gè)節(jié)點(diǎn),就是它:
再比如,看看這個(gè)方法:
org.apache.shardingsphere.elasticjob.lite.internal.sharding.ShardingService#shardingIfNecessary
這個(gè)方法是做對(duì)作業(yè)進(jìn)行分片邏輯的。
對(duì)作業(yè)進(jìn)行分片,首先我們要知道當(dāng)前有哪些實(shí)例在運(yùn)行,對(duì)不對(duì)?
那怎么才能知道呢?
instances 節(jié)點(diǎn)請(qǐng)求出戰(zhàn):
shardingIfNecessary 方法的第一行邏輯就是讀取 instances 節(jié)點(diǎn)下的數(shù)據(jù):
獲取到節(jié)點(diǎn)之后,是不是就可以分片了?
理論上是這樣的,但是別著急,你看源碼里面還有這樣一個(gè)判斷:
isLeaderUntilBlock,看方法名稱也知道了,看看 Leader 節(jié)點(diǎn)是不是到位了,如果沒到位,需要等一下 Leader 選舉結(jié)束。
怎么判斷 Leader 節(jié)點(diǎn)是不是到位了?
前面文檔中說了,就是看這個(gè)節(jié)點(diǎn)是否存在:
對(duì)應(yīng)到源碼就是這樣的:
所以這就是我前面說的,你看源碼的時(shí)候得結(jié)合 zk 節(jié)點(diǎn)的用途一起看,知道節(jié)點(diǎn)的用途就能理解源碼里面操作節(jié)點(diǎn)的目的是什么。
然后,在這里多說一句。
shardingIfNecessary 這個(gè)方法是讀取配置,處理分片邏輯的。
但是這個(gè)方法在每一個(gè)實(shí)例中都會(huì)運(yùn)行,豈不是每個(gè)實(shí)例都會(huì)執(zhí)行一次分片邏輯?
這樣處理的話,由于多個(gè)地方執(zhí)行分片邏輯,就需要考慮沖突和一致性的問題,導(dǎo)致邏輯非常的復(fù)雜。
雖然這個(gè)方法每個(gè)實(shí)例都會(huì)執(zhí)行,但是其實(shí)只需要有一個(gè)實(shí)例執(zhí)行分片邏輯就行了。
那么哪個(gè)節(jié)點(diǎn)來執(zhí)行呢?
你肯定也猜到了,當(dāng)然是主節(jié)點(diǎn)來干這個(gè)事兒嘛。如果當(dāng)前節(jié)點(diǎn)不是主節(jié)點(diǎn) return 就完事了:
怎么看當(dāng)前節(jié)點(diǎn)是否是主節(jié)點(diǎn)呢?
前面已經(jīng)出現(xiàn)多次了,zk 里面記錄著的:
如果當(dāng)然節(jié)點(diǎn)是主節(jié)點(diǎn),就接著往下執(zhí)行,就是“作業(yè)分片策略”了:
目前官方提供了三個(gè)不同的分片策略:
對(duì)應(yīng)的實(shí)現(xiàn)類是這樣的:
邏輯都非常簡單,上手 Debug 兩次就能摸清楚。
建議直接把項(xiàng)目拉下來,然后從測(cè)試用例入手。
好了,源碼導(dǎo)讀就到這里了。
我覺得我已經(jīng)算是告訴你關(guān)于 ElasticJob 源碼閱讀的方式和注意點(diǎn),如果你掌握到了,留言區(qū)留言“清晰”二字,支持一波。
如果你還是云里霧里的,沒事,是我的問題。大膽的說出來:什么玩意?看求不懂。呸,垃圾作者。
如果你是第一次接觸到 ElasticJob,那么讀到這里的時(shí)候,你的內(nèi)心關(guān)于 ElasticJob 應(yīng)該還有很多疑問以及不清楚的細(xì)節(jié)。
很好,帶著你的問題,去翻源碼吧。
源碼之下無秘密。
下面這個(gè)環(huán)節(jié)叫做[荒腔走板],技術(shù)文章后面我偶爾會(huì)記錄、分享點(diǎn)生活相關(guān)的事情,和技術(shù)毫無關(guān)系。我知道看起來很突兀,但是我喜歡,因?yàn)檫@是一個(gè)普通博主的生活氣息。
荒腔走板
這周終于是把《長安三萬里》看了,之前一直想看,但是又被三個(gè)小時(shí)的時(shí)長勸退。
我個(gè)人覺得確實(shí)是值得豆瓣高分的。
看完之后,包括看的過程中,我老是想起之前在網(wǎng)上看到的一段話,關(guān)于“一顆子彈”和“教育閉環(huán)”的。
“一顆子彈”是指在《我與地壇》看到的一段書評(píng),其內(nèi)容是:一個(gè)人十三四歲的夏天,在路上撿到一支真槍,因?yàn)槟晟贌o知,天不怕地不怕,他扣下扳機(jī)。沒有人死,也沒有人受傷,他認(rèn)為自己開了空槍。后來他三十歲或者更老,走在路上聽到背后隱隱約約的風(fēng)聲。他停下來回過身去,子彈正中眉心。
“教育閉環(huán)”是指教育具有長期性和滯后性,起初你只能理解表層的道理,直到多年后的某個(gè)瞬間,你才能真正領(lǐng)悟到書上知識(shí)的真諦,此時(shí)教育的任務(wù)才算真正完成。
我小時(shí)候讀到“兩岸猿聲啼不住,輕舟已過萬重山”的時(shí)候,重點(diǎn)總是在“兩岸猿聲”上,想象著猿猴的叫聲是什么樣的,那是一番怎樣有趣的畫面。
后來,甚至可以說是今年,這個(gè)電影上映之后,我才明白當(dāng)年讀書的時(shí)候我忽略的“輕舟已過萬重山”背后才是有更加蜿蜒曲折、激動(dòng)人心的故事。
這句詩就是當(dāng)年的那一顆子彈,命中了馬上三十歲的我,至此,教育才算完成了閉環(huán)。
今年,讓我產(chǎn)生同樣感受的,還有當(dāng)年完全忽略的這句話:孔乙己是站著喝酒而穿長衫的唯一的人穿的雖然是長衫,可是又臟又破似平十多年沒有補(bǔ),也沒有洗。他對(duì)人說話,總是滿口之乎者也叫人半懂不懂的。
此外,電影中多次提到“長安”,雖然我們學(xué)的是同樣的課本,讀的是一樣的詩,但是每個(gè)人對(duì)與“長安”的認(rèn)知和理解是不一樣的。
現(xiàn)在提到長安,我腦海中出現(xiàn)的第一個(gè)畫面永遠(yuǎn)是當(dāng)年看《河西走廊》紀(jì)錄片的時(shí)候那一個(gè)畫面。
第一集《使者》,張騫出使西域,被匈奴*九年后同隨從堂邑父出逃,繼續(xù)西行。
靠強(qiáng)大意志力穿越塔克拉瑪干沙漠和帕米爾高原,到達(dá)西域。回程再次被俘,數(shù)年后帶匈奴妻子和堂邑父又一次出逃?xùn)|歸。
十三年后,終于再次望到長安城,張騫匍匐在地,長跪不起。
西北望長安,可憐無數(shù)山。
這一跪,看的我眼淚婆娑。
總結(jié)
以上是生活随笔為你收集整理的又被夺命连环问了!从一道关于定时任务的面试题说起。的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iPhone微信上的小视频怎么储存在电脑
- 下一篇: java信息管理系统总结_java实现科