一次分佈式任務冪等處理的探索

當設計一個分佈式任務系統的時候,往往會遇到任務冪等的處理,比如同一個任務一天只能執行一次,或者同一個任務實例只能執行一次,否則會造成數據混亂;而這時需要怎麼做呢,有幾種做法呢?

分析

假設任務中有以下僞代碼

//業務處理, 獲取結果數據
data = business.do();
//將業務數據插入至數據庫
//開啓事務
Transaction.start()
dao.insertOrUpdate(data)
//提交事務
Transaction.commit()

假設以上任務一天只能執行一次的話,那如果由於重試或者消息等機制,導致被觸發多次,那麼只能數據庫中就會存在髒數據,甚至造成一定的損失,所以要進行冪等處理,即讓其一天只能執行一次

這時可能想到是不是可以藉助中間件如Redis進行處理呢?於是僞代碼修改如下:

//判斷是否執行過
flag = redisClient.get('20200101')
if (flag == null) {
    //業務處理, 獲取結果數據
    data = business.do();
    //將業務數據插入至數據庫
    //開啓事務
    Transaction.start()
    dao.insertOrUpdate(data)
    //提交事務
    Transaction.commit()
    redisClient.set('20200101', '1')
}

這樣是否就可以了呢?顯然是不行的,因爲在分佈式場景下,可能有兩個以上節點併發執行,當它們併發執行到“flag = redisClient.get('20200101')”,得到的flag可能都是null,這時就仍會出現多次執行業務邏輯的問題。

試想如果是單機多線程,我們會選擇如何處理呢?立刻想到了鎖進行同步處理,那在分佈式環境中,則顯然可以使用分佈式鎖。

分佈式鎖方案

僞代碼修改如下:

distributedLock.lock()
//判斷是否執行過
flag = redisClient.get('20200101')
if (flag == null) {
    //業務處理, 獲取結果數據
    data = business.do();
    //將業務數據插入至數據庫
    //開啓事務
    Transaction.start()
    dao.insertOrUpdate(data)
    //提交事務
    Transaction.commit()
}
distributedLock.release()

這樣貌似就解決了我們的問題,至於分佈式鎖可以使用Zookeeper等CP模型的中間件進行實現或者數據庫悲觀鎖等,但是不是給我們帶來了新的問題呢,比如系統變得複雜了,增加了中間件的維護成本,而且分佈式事務會使我們的應用性能大幅度下降。

那有沒有更加簡單的方式呢?讓我們簡單的利用數據庫事務的特性

本地事務表方式

首先我們在業務的數據庫中創建一張表,比如叫job_record,其中有兩個字段job_name, 和date,且二者爲聯合主鍵。

此時僞代碼修改如下:

//判斷是否執行過
flag = jobRecordDao.select('testJob', '20200101')
if (flag == null) {
    //業務處理, 獲取結果數據
    data = business.do();
    //將業務數據插入至數據庫
    //開啓事務
    Transaction.start()
    jobRecordDao.insert('testJob', '20200101')
    dao.insertOrUpdate(data)
    //提交事務
    Transaction.commit()
}

此時的flag判斷只是在非高併發場景下,減少對數據庫的嘗試提交事務以及業務邏輯處理操作,而當出現併發,flag判斷不起作用的情況下,insert job_record會觸發數據庫的主鍵重複檢測,拋出異常,而job_record的插入操作和業務數據的提交又在一個數據庫事務裏,則會發生回滾操作,這樣就保證了同一個job即使在高併發場景下一天仍然只能執行一次的目的;好處也顯而易見,沒有增加任何中間件,也沒有使性能出現嚴重的下降,大部分場景只是增加了一次select查詢而已。

至此完成了這次分佈式任務的冪等探索,成果可人!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章