如何處理下面的並發問題

來源:互聯網
上載者:User
1. 描述你的問題

工作中存在如下表:
表table_2,其中存在 unique key(device, seq)唯一索引。 其中seq欄位是我們程式中維護的一個自增序列。
業務的需求就是:每次推送一條訊息,會根據device擷取表中最大的seq。
select seq from table_2 where device = ? order by seq desc
然後將擷取的 seq+1 插入到table_2中作為當前訊息的記錄
insert into table_2 (device, seq) values(?, ?)

2 . 貼上相關代碼。代碼是我簡化的結果,方便大家閱讀

$device = "x-x";$this->_db->startTrans();//從table_2表中讀取該device最大的seq,//然後將該seq+1之後,重新將該device和seq插入的資料表中。//現在的問題是當並發的時候,並發的select語句擷取到了相同的seq,//所以insert的時候衝突發生了。$result = $this->_db->getRow(    “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device)$Seq = $result['seq'] + 1;$this->_db->execute(    "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq));$this->_db->commit();

3. 貼上報錯資訊

失敗方法1:
不合理的原因是:直接加上異常處理機制。當衝突時,並發的請求因為拋出異常,直接被捕獲,程式繼續運行,不做任何處理。結果就是:導致用戶端請求失敗,用戶端需要重新發起請求。

try{    //代碼放這裡} catch(Exception $e){    if ($e->getCode() == 1062){        //    }}

失敗方法2
嘗試重試機制.當請求第一次失敗時,會多次嘗試請求。當資料庫唯一索引衝突的時候,catch捕獲該異常,然後重試。重試的條件必須是:捕獲的異常是資料庫插入衝突,返回1062異常code。

for($i=0; $i < 10; $i++)try{    //代碼放這裡} catch(DBException $e){    if($e->code() == 1062){        continue;    }    throw $e;}

失敗方法3
通過redis來控制。因為衝突的根源就是於並發導致多個請求讀取到了相同的seq導致的。所以考慮使用redis‘鎖’的機制來實現。第一個擷取到seq的請求會設定一個key,之後的請求都會在這個key的基礎上進行擷取。

$device = "";$result = $this->_db->getRow(    “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device)$Seq = $result['seq'] + 1;//因為並發其實就是因為上面的sql語句查到了相同的seq,所以這裡//就只擷取第一條執行的seq,之後的全部通過對該redis加1操作。使用setnx和incr是為了保證原子操作。//這個處理方式仍然存在問題,比如在索引值到期的時刻存在並發,這個時候$Seq就可能從1開始。$lock = $redis->setnx($device, $Seq)$redis->expire($device, 2);if (!$lock){    $Seq = $redis->incr($device);}$this->_db->execute(    "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq));$this->_db->commit();

失敗方式4
另一種重試機制。上面的重試機制是靠MySQL資料庫的插入衝突,現在的重試是通過Redis在Select語句層面實現。

$device = "";for($i=0;$i<6; $i++){    if($i!=0){        usleep(10000);    } else {        $result = $this->_db->getRow(            “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device        )        $Seq = $result['seq'] + 1;        $lock = $redis->setnx($device . $Seq, rand());        if ($lock){             $success = true;             $redis->expire($device . $Seq, 2);             break;        }    }    if (!$success) {            throw new Exception();    }    $this->_db->execute(        "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq)    );    $this->_db->commit();}

失敗方法5
使用事務。使用MySQL機制的事務來實現。首先在Select語句中加上意向獨佔鎖定IX, 修改之後的SQL為 “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1 FOR UPDATE”, 後面的語句不變。

官方文檔是這樣介紹的:兩個意向排他說並沒有衝突,所以並發請求A和並發請求B同時會擷取IX,insert操作需要一個X鎖。

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

我的猜測:A請求在擷取X鎖的時候,B請求也在擷取X鎖。導致A請求在他遍曆的行上加鎖,B請求也同時在加鎖,造成了迴圈等待鎖釋放的情況,產生死結。

//前一句使用select for update//然後和insert包裹起來。多進程執行的話,會出現死結。//通過降低mysql的隔離水平確實可以實現,但是可能會產生不可重複讀取的問題.通俗的介紹就是:A請求在操作事務中兩次讀到的資料可能不一致。

失敗方法6
將兩條sql放在一條sql裡。 INSERT INTO table_2(device, seq) VALUES(?, (select seq + 1 from table_2) where device = ? )", array($device)

//因為前一條sql是我簡化版,其實他是一個分表的查詢操作。//這裡的分表是一個router操作,所以不適合進行一條sql語句//就是會依次查詢我們這裡分的幾個表,最近的一個表查不到,查第二個

方法7
修改資料表結構,將seq儲存到獨立的一個其他表中,每個device對應一個最大的seq。這樣在select的時候就可以保證select for update 不會產生間隙鎖,而是指在一條資料行上加鎖。

  1. 貼上相關

圖片就如上所示。大部分的代碼都是自己手工敲的,所以有些地方大家不用深究,瞭解清楚意思就可以。
希望大家可以有建設性建議

大神來指點指點吧

我實在是想不出什麼好的辦法了。將並發請求轉化為串列,這個想法沒有很好的實現。大神在哪裡?

回複內容:

1. 描述你的問題

工作中存在如下表:
表table_2,其中存在 unique key(device, seq)唯一索引。 其中seq欄位是我們程式中維護的一個自增序列。
業務的需求就是:每次推送一條訊息,會根據device擷取表中最大的seq。
select seq from table_2 where device = ? order by seq desc
然後將擷取的 seq+1 插入到table_2中作為當前訊息的記錄
insert into table_2 (device, seq) values(?, ?)

2 . 貼上相關代碼。代碼是我簡化的結果,方便大家閱讀

$device = "x-x";$this->_db->startTrans();//從table_2表中讀取該device最大的seq,//然後將該seq+1之後,重新將該device和seq插入的資料表中。//現在的問題是當並發的時候,並發的select語句擷取到了相同的seq,//所以insert的時候衝突發生了。$result = $this->_db->getRow(    “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device)$Seq = $result['seq'] + 1;$this->_db->execute(    "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq));$this->_db->commit();

3. 貼上報錯資訊

失敗方法1:
不合理的原因是:直接加上異常處理機制。當衝突時,並發的請求因為拋出異常,直接被捕獲,程式繼續運行,不做任何處理。結果就是:導致用戶端請求失敗,用戶端需要重新發起請求。

try{    //代碼放這裡} catch(Exception $e){    if ($e->getCode() == 1062){        //    }}

失敗方法2
嘗試重試機制.當請求第一次失敗時,會多次嘗試請求。當資料庫唯一索引衝突的時候,catch捕獲該異常,然後重試。重試的條件必須是:捕獲的異常是資料庫插入衝突,返回1062異常code。

for($i=0; $i < 10; $i++)try{    //代碼放這裡} catch(DBException $e){    if($e->code() == 1062){        continue;    }    throw $e;}

失敗方法3
通過redis來控制。因為衝突的根源就是於並發導致多個請求讀取到了相同的seq導致的。所以考慮使用redis‘鎖’的機制來實現。第一個擷取到seq的請求會設定一個key,之後的請求都會在這個key的基礎上進行擷取。

$device = "";$result = $this->_db->getRow(    “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device)$Seq = $result['seq'] + 1;//因為並發其實就是因為上面的sql語句查到了相同的seq,所以這裡//就只擷取第一條執行的seq,之後的全部通過對該redis加1操作。使用setnx和incr是為了保證原子操作。//這個處理方式仍然存在問題,比如在索引值到期的時刻存在並發,這個時候$Seq就可能從1開始。$lock = $redis->setnx($device, $Seq)$redis->expire($device, 2);if (!$lock){    $Seq = $redis->incr($device);}$this->_db->execute(    "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq));$this->_db->commit();

失敗方式4
另一種重試機制。上面的重試機制是靠MySQL資料庫的插入衝突,現在的重試是通過Redis在Select語句層面實現。

$device = "";for($i=0;$i<6; $i++){    if($i!=0){        usleep(10000);    } else {        $result = $this->_db->getRow(            “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1”, $device        )        $Seq = $result['seq'] + 1;        $lock = $redis->setnx($device . $Seq, rand());        if ($lock){             $success = true;             $redis->expire($device . $Seq, 2);             break;        }    }    if (!$success) {            throw new Exception();    }    $this->_db->execute(        "INSERT INTO table_2(device, seq) VALUES(?, ?)", array($device, $Seq)    );    $this->_db->commit();}

失敗方法5
使用事務。使用MySQL機制的事務來實現。首先在Select語句中加上意向獨佔鎖定IX, 修改之後的SQL為 “SELECT * FROM table_2 WHERE device = ? ORDER BY seq DESC LIMIT 1 FOR UPDATE”, 後面的語句不變。

官方文檔是這樣介紹的:兩個意向排他說並沒有衝突,所以並發請求A和並發請求B同時會擷取IX,insert操作需要一個X鎖。

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

我的猜測:A請求在擷取X鎖的時候,B請求也在擷取X鎖。導致A請求在他遍曆的行上加鎖,B請求也同時在加鎖,造成了迴圈等待鎖釋放的情況,產生死結。

//前一句使用select for update//然後和insert包裹起來。多進程執行的話,會出現死結。//通過降低mysql的隔離水平確實可以實現,但是可能會產生不可重複讀取的問題.通俗的介紹就是:A請求在操作事務中兩次讀到的資料可能不一致。

失敗方法6
將兩條sql放在一條sql裡。 INSERT INTO table_2(device, seq) VALUES(?, (select seq + 1 from table_2) where device = ? )", array($device)

//因為前一條sql是我簡化版,其實他是一個分表的查詢操作。//這裡的分表是一個router操作,所以不適合進行一條sql語句//就是會依次查詢我們這裡分的幾個表,最近的一個表查不到,查第二個

方法7
修改資料表結構,將seq儲存到獨立的一個其他表中,每個device對應一個最大的seq。這樣在select的時候就可以保證select for update 不會產生間隙鎖,而是指在一條資料行上加鎖。

  1. 貼上相關

圖片就如上所示。大部分的代碼都是自己手工敲的,所以有些地方大家不用深究,瞭解清楚意思就可以。
希望大家可以有建設性建議

大神來指點指點吧

我實在是想不出什麼好的辦法了。將並發請求轉化為串列,這個想法沒有很好的實現。大神在哪裡?

insert into table_2 select max(seq)+1,device from table_2 WHERE device = ?;

直接使用redis的incr來產生自增id,incr是原子操作,不會有並發問題。

你的問題歸納一下就是:如何?同一個device下seq自增長問題。
解決方案:使用redis的hash儲存device與seq的關係,當需要為指定device生產一個唯一seq時使用HINCRBY命令即可。

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.