寫在前面
在實際工作中,很多小夥伴在開發定時任務時,會採取定時掃描資料表的方式實現。然而,這種方式存在著重大的缺陷:如果資料量大的話,頻繁的掃描資料表會對資料庫造成巨大的壓力;難以支撐大規模的分散式定時任務;難以支援精準的定時任務;大量浪費CPU的資源;掃描的資料大部分是不需要執行的任務。那麼,既然定時掃描資料表存在這麼多的弊端,那麼,有沒有一種方式來解決這些問題呢?今天,冰河就帶著他的開源專案mykit-delay來了!!開源地址:https://github.com/sunshinelyz/mykit-delay
在使用框架過程中如有任何問題,都可以新增冰河微信【sun_shine_lyz】進行交流。
文章已收錄到https://github.com/sunshinelyz/technology-binghe
專案簡述
Mykit體系中提供的簡單、穩定、可擴充套件的延遲訊息佇列框架,提供精準的定時任務和延遲佇列處理功能。
專案模組說明
- mykit-delay-common: mykit-delay 延遲訊息佇列框架通用工具模組,提供全域性通用的工具類
- mykit-delay-config: mykit-delay 延遲訊息佇列框架通用配置模組,提供全域性配置
- mykit-delay-queue: mykit-delay 延遲訊息佇列框架核心實現模組,目前所有主要的功能都在此模組實現
- mykit-delay-controller: mykit-delay 延遲訊息佇列框架Restful介面實現模組,對外提供Restful介面訪問,相容各種語言呼叫
- mykit-delay-core: mykit-delay 延遲訊息佇列框架的入口,整個框架的啟動程式在此模組實現
- mykit-delay-test: mykit-delay 延遲訊息佇列框架通用測試模組,主要提供Junit單元測試用例
需求背景
- 使用者下訂單後未支付,30分鐘後支付超時
- 在某個時間點通知使用者參加系統活動
- 業務執行失敗之後隔10分鐘重試一次
類似的場景比較多 簡單的處理方式就是使用定時任務 假如資料比較多的時候 有的資料可能延遲比較嚴重,而且越來越多的定時業務導致任務排程很繁瑣不好管理。
佇列設計
整體架構設計如下圖所示。
開發前需要考慮的問題
- 及時性 消費端能按時收到
- 同一時間訊息的消費權重
- 可靠性 訊息不能出現沒有被消費掉的情況
- 可恢復 假如有其他情況 導致訊息系統不可用了 至少能保證資料可以恢復
- 可撤回 因為是延遲訊息 沒有到執行時間的訊息支援可以取消消費
- 高可用 多例項 這裡指HA/主備模式並不是多例項同時一起工作
- 消費端如何消費
當然初步選用redis作為資料快取的主要原因是因為redis自身支援zset的資料結構(score 延遲時間毫秒) 這樣就少了排序的煩惱而且效能還很高,正好我們的需求就是按時間維度去判定執行的順序 同時也支援map list資料結構。
簡單定義一個訊息資料結構
private String topic;/***topic**/
private String id;/***自動生成 全域性惟一 snowflake**/
private String bizKey;
private long delay;/***延時毫秒數**/
private int priority;//優先順序
private long ttl;/**消費端消費的ttl**/
private String body;/***訊息體**/
private long createTime=System.currentTimeMillis();
private int status= Status.WaitPut.ordinal();
執行原理:
- 用Map來儲存後設資料。id作為key,整個訊息結構序列化(json/…)之後作為value,放入元訊息池中。
- 將id放入其中(有N個)一個zset有序列表中,以createTime+delay+priority作為score。修改狀態為正在延遲中
- 使用timer實時監控zset有序列表中top 10的資料 。 如果資料score<=當前時間毫秒就取出來,根據topic重新放入一個新的可消費列表(list)中,在zset中刪除已經取出來的資料,並修改狀態為待消費
- 客戶端獲取資料只需要從可消費佇列中獲取就可以了。並且狀態必須為待消費 執行時間需要<=當前時間的 如果不滿足 重新放入zset列表中,修改狀態為正在延遲。如果滿足修改狀態為已消費。或者直接刪除後設資料。
客戶端
因為涉及到不同程式語言的問題,所以當前預設支援http訪問方式。
- 新增延時訊息新增成功之後返回消費唯一ID POST /push {…..訊息體}
- 刪除延時訊息 需要傳遞訊息ID GET /delete?id=
- 恢復延時訊息 GET /reStore?expire=true|false expire是否恢復已過期未執行的訊息。
- 恢復單個延時訊息 需要傳遞訊息ID GET /reStore/id
- 獲取訊息 需要長連線 GET /get/topic
用nginx暴露服務,配置為輪詢 在新增延遲訊息的時候就可以流量平均分配。
目前系統中客戶端並沒有採用HTTP長連線的方式來消費訊息,而是採用MQ的方式來消費資料這樣客戶端就可以不用關心延遲訊息佇列。只需要在傳送MQ的時候攔截一下 如果是延遲訊息就用延遲訊息系統處理。
訊息可恢復
實現恢復的原理 正常情況下一般都是記錄日誌,比如mysql的binlog等。
這裡我們直接採用mysql資料庫作為記錄日誌。
目前建立以下2張表:
- 訊息表 欄位包括整個訊息體
- 訊息流轉表 欄位包括訊息ID、變更狀態、變更時間、zset掃描執行緒Name、host/ip
定義zset掃描執行緒Name是為了更清楚的看到訊息被分發到具體哪個zset中。前提是zset的key和監控zset的執行緒名稱要有點關係 這裡也可以是zset key。
支援訊息恢復
假如redis伺服器當機了,重啟之後發現資料也沒有了。所以這個恢復是很有必要的,只需要從表1也就是訊息表中把訊息狀態不等於已消費的資料全部重新分發到延遲佇列中去,然後同步一下狀態就可以了。
當然恢復單個任務也可以這麼幹。
資料表設計
這裡,我就直接給出建立資料表的SQL語句。
DROP TABLE IF EXISTS `mykit_delay_queue_job`;
CREATE TABLE `mykit_delay_queue_job` (
`id` varchar(128) NOT NULL,
`bizkey` varchar(128) DEFAULT NULL,
`topic` varchar(128) DEFAULT NULL,
`subtopic` varchar(250) DEFAULT NULL,
`delay` bigint(20) DEFAULT NULL,
`create_time` bigint(20) DEFAULT NULL,
`body` text,
`status` int(11) DEFAULT NULL,
`ttl` int(11) DEFAULT NULL,
`update_time` datetime(3) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mykit_delay_queue_job_ID_STATUS` (`id`,`status`),
KEY `mykit_delay_queue_job_STATUS` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for mykit_delay_queue_job_log
-- ----------------------------
DROP TABLE IF EXISTS `mykit_delay_queue_job_log`;
CREATE TABLE `mykit_delay_queue_job_log` (
`id` varchar(128) NOT NULL,
`status` int(11) DEFAULT NULL,
`thread` varchar(60) DEFAULT NULL,
`update_time` datetime(3) DEFAULT NULL,
`host` varchar(128) DEFAULT NULL,
KEY `mykit_delay_queue_job_LOG_ID_STATUS` (`id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
關於高可用
分散式協調還是選用zookeeper。
如果有多個例項最多同時只能有1個例項工作 這樣就避免了分散式競爭鎖帶來的壞處,當然如果業務需要多個例項同時工作也是支援的,也就是一個訊息最多隻能有1個例項處理,可以選用zookeeper或者redis就能實現分散式鎖了。
最終做了一下測試多例項同時執行,可能因為會涉及到鎖的問題效能有所下降,反而單機效果很好。所以比較推薦基於docker的主備部署模式。
執行模式
- 支援 master,slave (HA)需要配置
mykit.delay.registry.serverList
zk叢集地址列表 - 支援 cluster 會涉及到分散式鎖競爭 效果不是很明顯 分散式鎖採用
redis
的setNx
實現 - StandAlone
目前,經過測試,推薦使用master slave的模式,後期會優化Cluster模式
如何接入
為了提供一個統一的精準定時任務和延時佇列框架,mykit-delay提供了HTTP Rest介面供其他業務系統呼叫,介面使用簡單方便,只需要簡單的呼叫介面,傳遞相應的引數即可。
訊息體
以JSON資料格式引數 目前只提供了http
協議
- body 業務訊息體
- delay 延時毫秒 距
createTime
的間隔毫秒數 - id 任務ID 系統自動生成 任務建立成功返回
- status 狀態 預設不填寫
- topic 標題
- subtopic 保留欄位
- ttl 保留欄位
- createTime 建立任務時間 非必填 系統預設
新增任務
/push
POST application/json
{"body":"{hello world}","delay":10000,"id":"20","status":0,"topic":"ces","subtopic":"",ttl":12}
刪除任務
刪除任務 需要記錄一個JobId
/delete?jobId=xxx
GET
恢復單個任務
用於任務錯亂 腦裂情況 根據日誌恢復任務
/reStoreJob?JobId=xxx
GET
恢復所有未完成的任務
根據日誌恢復任務
/reStore?expire=true
GET
引數expire
表示是否需要恢復已過期還未執行的資料
清空佇列資料
根據日誌中未完成的資料清空佇列中全部資料。清空之後 會刪除快取中的所有任務
/clearAll
GET
客戶端獲取佇列方式
目前預設實現了RocketMQ
與ActiveMQ
的推送方式。依賴MQ的方式來實現延時框架與具體業務系統的耦合。
訊息體中訊息與RocketMQ
和 ActiveMQ
訊息欄位對應關係
mykit-delay | RocketMQ | ActiveMQ | 備註 | |
---|---|---|---|---|
topic | topic | topic | 點對點傳送佇列名稱或者主題名稱 | |
subtopic | subtopic | subtopic | 點對點傳送佇列子名稱或者主題子名稱 | |
body | 訊息內容 | 訊息內容 | 訊息內容 |
關於系統配置
延遲框架與具體執行業務系統的互動方式通過延遲框架配置實現,具體配置檔案位置為mykit-delay-config專案下的resources/properties/starter.properties
檔案中。
測試
需要配置好資料庫地址和Redis的地址 如果不是單機模式 也需要配置好Zookeeper
執行mykit-delay-test模組下的測試類io.mykit.delay.test.PushTest
新增任務到佇列中
啟動mykit-delay-test模組下的io.mykit.delay.TestDelayQueue
消費前面新增資料 為了方便查詢效果 預設的消費方式是consoleCQ
控制檯輸出
擴充套件
支援zset佇列個數可配置 避免大資料帶來高延遲的問題。
近期規劃
- 分割槽(buck)支援動態設定
- redis與資料庫資料一致性的問題 (
重要
) - 實現自己的推拉機制
- 支援可切換實現方式 目前只是依賴Redis實現,後續待優化
- 支援Web控制檯管理佇列
- 實現訊息消費
TTL
機制
如果這款開源框架對你有幫助,請小夥伴們開啟github連結:https://github.com/sunshinelyz/mykit-delay ,給個Star,讓更多的小夥伴看到,減輕工作中繁瑣的掃描資料表的定時任務開發。也希望能夠有越來越多的小夥伴參與這個開源專案,我們一起養肥它!!
好了,不早了,今天就到這兒吧,我是冰河,我們下期見!!
重磅福利
微信搜一搜【冰河技術】微信公眾號,關注這個有深度的程式設計師,每天閱讀超硬核技術乾貨,公眾號內回覆【PDF】有我準備的一線大廠面試資料和我原創的超硬核PDF技術文件,以及我為大家精心準備的多套簡歷模板(不斷更新中),希望大家都能找到心儀的工作,學習是一條時而鬱鬱寡歡,時而開懷大笑的路,加油。如果你通過努力成功進入到了心儀的公司,一定不要懈怠放鬆,職場成長和新技術學習一樣,不進則退。如果有幸我們江湖再見!
另外,我開源的各個PDF,後續我都會持續更新和維護,感謝大家長期以來對冰河的支援!!