先來說說定時任務在集群中需要解決的問題:
1、如果集群中每台機器都啟動定時任務,容易造成資料重複處理的問題
2、如果採用定時任務開關的方式,只一台機器的開關on,其他機器的開關off,可以避免資料重複處理的問題,但是存在單點故障的問題。
①指定執行定時任務的機器
在多台機器中選擇一台執行定時任務,每次執行的時候回判斷當前機器和指定的機器是否一致,一致才會執行。
這種方法可以避免執行多次的情況,但是最明顯的缺點就是單點故障問題,當這台指定的機器掛了以後,任務就不會執行了。
②通過動態的載入配置檔案來實現
例如:新建多個timmer的配置檔案,對任務進行分配。針對不同的機器載入不同的配置檔案,即不同的機器執行不同的任務。
這種方法缺陷就是要有至少兩種不同的配置檔案,那麼維護起來就很麻煩。
③將定時任務單獨執行
將程式中牽扯到的定時任務,從主程式中剝離出來單獨執行。
這種方法缺陷就是增加了開發和維護的成本,如果不是大型專案的話,一般不建議這麼做。
④監聽程式
對程式進行監聽,監聽是否有重複執行的定時器任務,有的話則不執行相關的業務邏輯。
⑤從資料庫中讀取定時任務
由於都是連線同乙個資料庫,給資料庫裡的定時任務打上相應的標記,來區別有無重複執行定時任務。
create
table timed_task (
type
int(3)
notnull
,status
tinyint(1
)not
null
,exec_timestamp bigint(20
)not
null
,interval_time int(11
)not
null
,primary
key(
type))
;
這個表是用來表示當前查詢的任務是否可以執行,如果專案中有很多定時任務,通過type欄位可以將多個不同型別的定時器進行區分,從而實現表的共用。
type # 將多個不同型別的定時器進行區分
status # 狀態,是否可以執行定時任務,0為可執行,1為不可執行
exec_timestamp # 上一次定時任務的執行時間
interval_time # 時間閾值,以秒為單位
這裡主要說一下字段interval_time
,主要用來檢測執行定時任務的節點有無出現故障或宕機和防止短時間內定時任務被重複執行的情況。
可以設立乙個檢查機制,當執行節點出現故障或宕機的時候沒有及時把字段status重新設定為0,讓這個定時任務重新處於可執行狀態的話,那麼可以根據欄位exec_timestamp和字段interval_time和當前時間進行比較。如果當前時間是大於欄位exec_timestamp和字段interval_time之和的,而字段status始終為1的話,說明出現了故障或宕機,那麼就可以去檢查程式**出現了問題。
定時任務在集群中的執行,因為每台機器可能存在乙個幾秒的時間差,所以有可能出現
定時任務已經被某台機器處理過了,處理時間只用了幾毫秒,所以欄位status被重置為0,讓這個定時任務重新處於可執行的狀態,但是因為幾秒鐘的偏差,這時某台機器才到了定時任務的指定定時時間,發現資料庫中這個定時任務的字段status為0處於可執行的狀態,那麼會又重新執行了一次這個定時任務,出現資料重複處理的情況。所以這個字段interval_time
也可以用來防止重複執行的問題,當前時間如果小於欄位exec_timestamp和字段interval_time之和的話,並且欄位status為0,說明短期內這個定時任務是已經被執行過一次的,那麼就不允許再次執行了。
⑥使用redis的過期機制和分布式鎖
借用redis的分布式鎖來解決資料重複處理的問題,為了防止鎖被長久鎖定,或者防止客戶端崩掉了沒有刪掉鎖,可以用expire加入過期時間。
我個人是使用第六種方案,就是使用redis的分布式鎖來解決定時任務的多機處理問題的。下面會有簡單的實現**,但由於篇幅所限,所以只能將**放在乙個檔案下,但這在開發中是不合理的結構,此處只供參考。
)// 分布式鎖的結構體
type redislock struct
var cron *cron.cron // 用到第三方庫提供的定時任務,很方便就解決了定時任務的難題, 避免重複造輪子
var client = redis.
newclient
(&redis.options
)func
startcronjob()
cron.
start()
}func
cronjob()
haslock, err := r.
grablock
(time.
now().
unix()
,101
)// 搶鎖
if err !=
nilif
!haslock
defer r.
releaselock()
// 搶到鎖的才有資格釋放鎖
fmt.
println
("hello world, "
, time.
now())
}// 搶鎖
func
(r *redislock)
grablock
(datestamp int64
, tp int
)(haslock bool
, err error
)return
}// 釋放鎖
func
(r *redislock)
releaselock()
result, err := client.
eval
(delscript, keys, r.value)
.result()
if err !=
nil|| result ==0}
func
main()
首先借助於redis的setnx
命令來操作,setnx
本身針對key賦值的時候會判斷redis中是否存在這個key,如果有返回-1, 如果沒有的話,會直接set
鍵值。那setnx
命令跟set
命令有啥區別?setnx
是原子操作,而set
不能保證原子性。
超時時間是為了防止如果某個搶到鎖的客戶端在還沒釋放鎖之前redis就宕機了,那麼這個鎖就永遠不會被釋放,其他客戶端就搶不到鎖,結果就是死鎖。
那麼為什麼執行eval()
方法可以確保原子性,源於redis的特性,下面是官網對eval
命令的解釋:在eval
命令執行lua
指令碼的時候,lua
指令碼將被當成乙個命令去執行,並且直到eval
命令執行完成,redis才會執行其他命令。
最常見的釋放鎖的方式就是直接使用del()
方法刪除鎖,這種不先判斷鎖的擁有者而直接釋放鎖的方式,會導致乙個客戶端釋放了另乙個客戶端的鎖。舉個例子:a客戶端拿到了鎖,被某個操作阻塞了很長時間,然後這個鎖過了超時時間後被redis自動釋放了,然後這個a客戶端還以為這個鎖還是自己的,就去釋放鎖了,可實際上鎖已經被b客戶端拿到了,這樣就導致a客戶端釋放了b客戶端的鎖。所以單純的用del
指令有可能造成乙個客戶端刪除了其他客戶端的鎖。
所以,為了防止把別人的鎖釋放了,釋放鎖之前必須檢查一下,當前的value是不是自己設定進去的value,如果不是,就說明鎖不是自己的了,不能釋放。
定時任務的處理
對於定時任務,一般由時間戳timers 或者死迴圈while true 操作,兩者都能達到,指定間隔時間內執行去執行任務,這裡不做效率的比較,只說明一下適合的場景。先拿while來說,執行完成任務後,設定好執行緒等待時間即可,它有一優勢,即如果此次任務未執行完,則不會進入下一次,也就是說是可控的,不...
WEB耗時任務的處理方案
最近有乙個需求,從網頁上傳乙個文字包到後台處理,處理時長可能在幾分鐘到幾十分鐘。原來的方案就是直接接收乙個ajax請求處理資料,然後返回。遇到的問題是 經過十幾分鐘的處理後,後台返回結果到前端,前端收不到該結果了。我想應該是http連線超時了,除非長連線,沒有哪個請求可以這樣無限制地等待。於是著手改...
vue定時任務引發的問題
vue中定義了乙個定時任務來定時重新整理渲染前台資料 created function 5000 每隔5秒重新整理一次 由於後台介面請求第三方應用超時,無法及時返回資料,導致前台http請求一直處於pending狀態,並且不斷建立新的http請求。由於谷歌瀏覽器最多同時支援6個ajax請求,超過6個...