一般你們都會使用redis做爲應用的任務隊列表,redis的List結構,在一段進行任務的插入,在另外一端進行任務的提取。
任務的插入mysql
$redis->lPush("key:task:list",$task);
任務的提取redis
$tasks = $redis->RPop("key:task:list",0,-1);
但是你們想,如何使用mysql來實現一個隊列表呢?
映入你們腦海的一個典型的模式是一個表包含多種類型的記錄:未處理記錄,已處理記錄,正在處理記錄等。一個或者多個消費者線程在表中查詢未處理的記錄,而後聲稱正在處理這個任務,處理完成以後,再講記錄更新爲已處理狀態。
這個典型的模式,存在兩個問題;1:隨着隊列表愈來愈大,查找未處理記錄的速度會愈來愈慢。2:頻繁的加鎖會讓多個消費者線程增長競爭。
首先咱們來建立一個表sql
create table unsent_emails{ id int not null primary key auto_increment, status enum("unsent","claimed","sent"), owner int unsigned not null default 0, ts timestamp, key (owner,status,ts) };
該表的列owner用來存儲當前正在處理這個記錄的鏈接id,由函數 CONNECTION_ID()返回的鏈接id或者線程id。若是這個記錄當前被沒有被處理,則該值爲0
咱們在 owner status ts上面作了索引的處理,因此查找未處理的記錄會很快。
經過咱們會採用 select for update的方式來標記待處理的記錄,方法以下數據庫
begin; select id from unsent_emails where owner = 0 and status = 'unsent' limit 10 for update; -- result 10,20,33 update unsent_emails set status = 'claimed',owner = CONNECTION_ID() where id in (10,20,33); commit;
select的時候,使用了兩個索引,應該會很快。問題出在select 和 update兩個查詢之間的間隙,這裏的加鎖會讓其餘相同的查詢所有阻塞。
若是咱們採用update then select的方式,那麼效果就會更加高效,代碼以下函數
set autocommit=1; commit; update unsent_emails set statue = 'claimed',owner = CONNECTION_ID() where owner = 0 and status = 'unsent' limit 10; set autocommit=0; select id from unsent_emails where owner = CONNECTION_ID() and status = 'claimed';
根本無需使用select去查找哪些記錄尚未處理。客戶端協議會告訴你更新了幾條記錄,就能夠知道此次須要處理多少條記錄。
這樣是否是解決了上面的第二個問題,select for update的模式的加鎖會增長多個消費隊列的競爭問題。
其實全部的select for update 均可以替換爲 update then select模式。性能
問題尚未結束,還有一種狀況須要處理,就是好比正在處理任務的進程異常退出了,那麼對應的進程正在處理的任務也就變爲殭屍任務了。如何避免這種狀況的發生呢?線程
因此咱們仍是須要一個新的定時器或者線程來定時檢測而且update,將那些殭屍任務的記錄更新到原始狀態,就能夠了。
殭屍任務的定義必須符合兩點,1:任務被擱置了好久,好比十分鐘,而一般一個任務只須要10秒就能夠處理完;2:任務的owner(線程id或者鏈接id)已經不存在,只須要執行show processlist就能夠獲取當前正在工做的線程id了。代碼以下code
update unsent_emails set owner = 0,status = 'unsent' where owner not in (10,20,33,44) and status = 'claimed' and ts < current_timestamp - interval 10 minute;
一個基於mysql構建的隊列表就完成了。
固然,最好的辦法就是將任務隊列從數據庫中遷移出來。redis真是一個很好的隊列容器,固然也可使用ssdb(基於leveldb,內存佔用更少)。
讀 高性能mysql 筆記索引