數據庫分表後,併發環境下,生成全局id生成的幾種方式

最近一個項目因爲數據量變大,須要進行數據分表。數據存儲在淘寶的tddl上。分表後,原先的自增id就不能使用了。tddl對java支持很好,分表後無需考慮全局id的問題。可是這個項目使用的是php進行開發,必須本身生成全局id。如下列出幾種分表方案,僅當拋磚引玉。php

1:使用CAS(compare and swap)

其實這裏並非嚴格的CAS,而是使用了比較交換原子操做的思想。
生成思路以下:每次生成全局id時,先從sequence表中獲取當前的全局最大id。而後在獲取的全局id上作加1操做。把加1後的值更新到數據庫。更新時是關鍵。
如加1後的值爲203,表名是users,數據表結構以下:java

CREATE TABLE `SEQUENCE` (
    `name` varchar(30) NOT NULL COMMENT '分表的表名',
    `gid` bigint(20) NOT NULL COMMENT '最大全局id',
    PRIMARY KEY (`name`)
) E

那麼更新語句是。
update sequence set gid = 203 where name = 'users' and gid < 203;
sql語句的 and gid < 203 是爲了保證併發環境下gid的值只增不減。
若是update語句的影響記錄條數爲0說明,已經有其餘進程提早生成了203這個值,並寫入了數據庫。須要重複以上步驟重新生成。
代碼實現以下:redis

//$name 表名
function next_id_db($name){
    //獲取數據庫全局sequence對象
    $seq_dao = Wk_Sequence_Dao_Sequence::getInstance();
    $threshold = 100; //最大嘗試次數
    for($i = 0; $i < $threshold; $i++){
        $last_id = $seq_dao->get_seq_id($name);//從數據庫獲取全局id
        $id = $last_id +1;
        $ret = $seq_dao->set_seq_id($name, $id);
        if($ret){
            return $id;
            break;
        }
    }
    return false;
}

2:使用全局鎖

在進行併發編程時,通常都會使用鎖機制。其實,全局id的生成也是解決併發問題。
生成思路以下:
在使用redis的setnx方法和memcace的add方法時,若是指定的key已經存在,則返回false。利用這個特性,實現全局鎖。
每次生成全局id前,先檢測指定的key是否存在。
若是不存在則使用redis的incr方法或者memcache的increment進行加1操做。這兩個方法的返回值是加1後的值。
若是存在,則程序進入循環等待狀態。循環過程當中不斷檢測key是否還存在,若是key不存在就執行上面的操做。
代碼以下:sql

//使用redis實現
//$name 爲 邏輯表名
function next_id_redis($name){
    $redis = Wk_Redis_Util::getRedis();//獲取redis對象
    $seq_dao = Wk_Sequence_Dao_Sequence::getInstance();//獲取存儲全局id數據表對象
    if(!is_object($redis)){
        throw new Exception("fail to create redis object");
    }
    $max_times = 10; //最大執行次數 避免redis不可用的時候 進入死循環
    while(1){
        $i++;
        //檢測key是否存在,至關於檢測鎖是否存在
        $ret = $redis->setnx("sequence_{$name}_flag",time());
        if($ret){
            break;
        }
        if($i > $max_times){
            break;
        }
        $time = $redis->get("sequence_{$name}_flag");
        if(is_numeric($time) && time() - $time > 1){//若是循環等待時間大於1秒,則再也不等待。
            break;
        }
    }
    $id = $redis->incr("sequence_{$name}");
    //若是操做失敗,則從sequence表中獲取全局id並加載到redis
    if (intval($id) === 1 or $id === false) {
        $last_id = $seq_dao->get_seq_id($name);//從數據庫獲取全局id
        if(!is_numeric($last_id)){
            throw new Exception("fail to get id from db");
        }
        $ret = $redis->set("sequence_{$name}",$last_id);
        if($ret == false){
            throw new Exception("fail to set redis key [ sequence_{$name} ]");
        }
        $id = $redis->incr("sequence_{$name}");
        if(!is_numeric($id)){
            throw new Exception("fail to incr redis key [ sequence_{$name} ]");
        }
    }
    $seq_dao->set_seq_id($name, $id);//把生成的全局id寫入數據表sequence
    $redis->delete("sequence_{$name}_flag");//刪除key,至關於釋放鎖
    $db = null;
    return $id;
}

3:redis和db結合

使用redis直接操做內存,可能性能會好些。可是若是redis死掉後,如何處理呢?把以上兩種方案結合,提供更好的穩定性。
代碼以下:數據庫

function next_id($name){
    try{
        return $this->next_id_redis($name);
    }
    catch(Exception $e){
        return $this->next_id_db($name);
    }
}

另外對於全局id的生成,Flicker和Twitter也都公佈了本身的方案。感興趣的人,能夠了解下。編程

http://my.oschina.net/u/142836/blog/174465併發

相關文章
相關標籤/搜索