最近要作一個批跑服務, 基本邏輯就是定時掃描數據庫的記錄, 有知足條件的就進行處理(一條記錄表明一個任務,如下任務與記錄含義相同). 要求支持多機部署批跑服務.php
要實現多機部署, 只要保證每一個批跑服務實例每次只獲取一條記錄, 處理完再獲取下一條便可. 其中最種要的是避免不一樣的實例獲取到同一條記錄,即所謂搶任務.mysql
先看錶結構設計:sql
create database if not exists ae; create table ae.task ( id int primary key, status int); -- status爲0說明任務可處理,其它不可處理
以上是簡化的表結構,但足以說明本文試圖說明的問題.數據庫
要避免搶任務, oracle的作法, 直接session
update ae.task set status=1 where status=0 and rownum = 1 returning id
便可.併發
mysql的要囉嗦一點:oracle
select id from ae.task where status=0; -- 獲得ID update ae.task where id = ${id} and status=0;
這兩個sql,第一個sql用於獲取符合條件的任務, 第二個sql用戶將任務鎖定. 在併發的場景下, 有可能不一樣的批跑實例的第一個SQL會返回相同的記錄, 但第二個sql只有一個會更新成功, 經過判斷affected rows
便可知道哪一個鎖定成功. 鎖定成功的繼續處理本任務, 鎖定失敗的繼續處理其它任務.post
管理後臺提交了一個任務後, 兩個批跑實例剛好同時啓動, 進入搶任務環節. 結果發現異常, 其中一個實例成功搶到任務, 但另外一個實例則掛死了:this
搶到任務的實例:設計
2015-11-23 19:42:01|INFO|exec_task.php|40||get one task: 11 ... 2015-11-23 19:42:01|INFO|exec_task.php|107||line_count: 9 2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8346 2015-11-23 19:42:01|INFO|exec_task.php|264||[0] pid: 8346, start: 0, stop: 0 2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8347 2015-11-23 19:42:01|INFO|exec_task.php|264||[1] pid: 8347, start: 1, stop: 1 2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8348 2015-11-23 19:42:01|INFO|exec_task.php|264||[2] pid: 8348, start: 2, stop: 2 2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8349 ...
沒有搶到任務的實例:
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task 2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
能夠看到沒有搶到任務的實例進入了死循環.
按照咱們以前的設計, 若是第二條SQL鎖定任務的時候失敗了, 獲取下一個任務. 應當不會死循環. 死循環的緣由是由於沒有搶到任務的實例, 在執行第一個SQL的時候, 一直返回了相同的記錄(id=11,實際上當時也只有一條記錄)
請注意, 搶到任務的實例搶到任務後, 會把狀態更新並提交, 按說搶不到任務的實例會看到此狀態更新,並致使第一條sql查不到數據,而後 正常退出.
而事實上搶不到任務的實例看不到此變化, 說明事務隔離級別(Transaction Isolation Level)不是"READ COMMITED", 而是其它. 經確認, 級別是"REPEATABLE-READ"
mysql> select @@TX_ISOLATION; +-----------------+ | @@TX_ISOLATION | +-----------------+ | REPEATABLE-READ |
"REPEATABLE-READ" 看到的數據是事務啓動時的樣子,因此看不到搶到任務的實例對任務狀態的修改. 進而致使死循環.
請注意執行第一個SQL查詢知足條件的任務是在一個事務內進行的. 此事務其實是業務的須要, 除了獲取到任務,還須要獲取其它資源,若是獲取不到其它 資源, 則rollback任務,以便下次處理.
ORACLE相應的事務隔離級別是"Serializable Isolation Level", 如上描述的這個場景, 在ORACLE下的反應是搶不到任務的實例在試圖更新任務狀態的 時候,會返回一個"ORA-08177: Cannot serialize access for this transaction"錯誤, 程序也能夠正常退出. 詳見<<Oracle Database Concepts 11g Release 2 (11.2) E40540-04>>
第9章"Overview of Oracle Database Transaction Isolation Levels"
mysql在"REPEATABLE-READ"的事務隔離級別上的表現是不能讓人滿意的. 查詢到的數據是事務啓動時的樣子,但更新的時候看到的數據又是其它事務提交 後的結果,而且update也沒有錯誤提示.
而"SERIALIZABLE"更糟糕, 若是同時開了兩個session, 乾脆直接鎖表了, 誰了更新不了. 這就勢必形成另外一個問題, 既然你們都更新不了,那就rollback事務, 重試唄. 可是重試也是頗有可能你們再同時開了事務,又鎖死了, 一直死循環. 爲了解決這種狀況,可能的作法是, 各自等待一個隨機時間再重試,讓隨機打破這個僵局. 不知道是否有其它辦法,歡迎指教.
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
不斷查詢知足條件的任務不要放到一個事務裏. 發現"affected rows"爲0,更新不到數據時, 事務rollback,從新啓動事務. 即在循環裏不斷開啓事務而不是在事務裏不斷循環.
還有一個辦法是開事務而後select for update
, 可是這種方法會致使鎖表, 必須等待其它事務提交後才能返回. 當初我進行設計的時候,是計劃使用select for update的方式的, 可是最終沒有使用, 如今回想, 多是沒有開事務, 結果兩個實例都查詢到了相同的記錄, 因此被我否認了. 可是看我另外一個文章 <<mysql搶單>>
又彷佛多是因爲鎖表而棄用了, 緣由已經不可考了.
但從本個需求來講, 彷佛使用select for update
來讓把表鎖住會更簡單.
你覺得搶到任務的實例就能夠高枕無憂了嗎, 錯了! 等他高高興興處理完任務, 要把任務狀態置爲成功時, 發現這個任務竟然被沒有搶到任務的實例給鎖了, 自已只能獲得一個鎖超時的錯誤
2015-11-23 19:42:52|ERR|function.inc.php|113||SQL fail: Lock wait timeout exceeded; try restarting transaction
請期待下一個問題分析.
今天回來確認了一下, 實際上ORACEL的update task set status = 1 where status = 0 and rownum = 1 returning taskid 這個SQL也會把表鎖住.
因此能夠用@flygogo 在30樓提出的方法模擬oracle 的returning
SET @update_id := 0; update ae.task set status=1, id = (SELECT @update_id := id) where status=0 limit 1; SELECT @update_id;
而postgresql的update彷佛沒有limit 1之類的限定只更新一條的寫法?
同時ORACLE和postgresql的select for update 也都會鎖表.
因此就本文所討論的範圍來講, 彷佛不能說是兩個凡是. 叉!
差點被繞暈了. 其實本文所指出的mysql在"REPEATABLE-READ"事務隔離級別下的表現是奇怪的,不直觀的,這點值得注意. 明明select出來的數據是可更新, 而更新時候又沒有成功, 會讓人很是疑惑. 而爲oracel在"Serializable"級別下發現數據已經被更新了以後,拋出"ORA-08177"的作法才更直觀更合適.
本文另外一個意義是分享了一種不鎖表實現隊列的方法