當設計一個分佈式任務系統的時候,往往會遇到任務冪等的處理,比如同一個任務一天只能執行一次,或者同一個任務實例只能執行一次,否則會造成數據混亂;而這時需要怎麼做呢,有幾種做法呢?
分析
假設任務中有以下僞代碼
//業務處理, 獲取結果數據
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查詢而已。
至此完成了這次分佈式任務的冪等探索,成果可人!