一個基於mysql構建的隊列表


一般你們都會使用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 筆記
索引

相關文章
相關標籤/搜索