目錄
回复内容:
首頁 後端開發 php教程 如何处理下面的并发问题

如何处理下面的并发问题

Jun 06, 2016 pm 08:17 PM
mysql php redis

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 . 贴上相关代码。代码是我简化的结果,方便大家阅读

<code>$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();</code>
登入後複製
登入後複製

3. 贴上报错信息

失败方法1:
不合理的原因是:直接加上异常处理机制。当冲突时,并发的请求因为抛出异常,直接被捕获,程序继续运行,不做任何处理。结果就是:导致客户端请求失败,客户端需要重新发起请求。

<code>try{
    //代码放这里
} catch(Exception $e){
    if ($e->getCode() == 1062){
        //
    }
}</code>
登入後複製
登入後複製

失败方法2
尝试重试机制.当请求第一次失败时,会多次尝试请求。当数据库唯一索引冲突的时候,catch捕获该异常,然后重试。重试的条件必须是:捕获的异常是数据库插入冲突,返回1062异常code。

<code>for($i=0; $i code() == 1062){
        continue;
    }
    throw $e;
}</code>
登入後複製
登入後複製

失败方法3
通过redis来控制。因为冲突的根源就是于并发导致多个请求读取到了相同的seq导致的。所以考虑使用redis‘锁’的机制来实现。第一个获取到seq的请求会设置一个key,之后的请求都会在这个key的基础上进行获取。

<code>$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();</code>
登入後複製
登入後複製

失败方式4
另一种重试机制。上面的重试机制是靠MySQL数据库的插入冲突,现在的重试是通过Redis在Select语句层面实现。

<code>$device = "";
for($i=0;$i_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();
}</code>
登入後複製
登入後複製

失败方法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请求也同时在加锁,造成了循环等待锁释放的情况,产生死锁。

<code>//前一句使用select for update
//然后和insert包裹起来。多进程执行的话,会出现死锁。
//通过降低mysql的隔离水平确实可以实现,但是可能会产生不可重复读的问题.通俗的介绍就是:A请求在操作事务中两次读到的数据可能不一致。</code>
登入後複製
登入後複製

失败方法6
将两条sql放在一条sql里。 INSERT INTO table_2(device, seq) VALUES(?, (select seq + 1 from table_2) where device = ? )", array($device)

<code>//因为前一条sql是我简化版,其实他是一个分表的查询操作。
//这里的分表是一个router操作,所以不适合进行一条sql语句
//就是会依次查询我们这里分的几个表,最近的一个表查不到,查第二个</code>
登入後複製
登入後複製

方法7
修改数据表结构,将seq保存到独立的一个其他表中,每个device对应一个最大的seq。这样在select的时候就可以保证select for update 不会产生间隙锁,而是指在一条数据行上加锁。

  1. 贴上相关截图

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

大神来指点指点吧

<code>我实在是想不出什么好的办法了。将并发请求转化为串行,这个想法没有很好的实现。大神在哪里?</code>
登入後複製
登入後複製

回复内容:

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 . 贴上相关代码。代码是我简化的结果,方便大家阅读

<code>$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();</code>
登入後複製
登入後複製

3. 贴上报错信息

失败方法1:
不合理的原因是:直接加上异常处理机制。当冲突时,并发的请求因为抛出异常,直接被捕获,程序继续运行,不做任何处理。结果就是:导致客户端请求失败,客户端需要重新发起请求。

<code>try{
    //代码放这里
} catch(Exception $e){
    if ($e->getCode() == 1062){
        //
    }
}</code>
登入後複製
登入後複製

失败方法2
尝试重试机制.当请求第一次失败时,会多次尝试请求。当数据库唯一索引冲突的时候,catch捕获该异常,然后重试。重试的条件必须是:捕获的异常是数据库插入冲突,返回1062异常code。

<code>for($i=0; $i code() == 1062){
        continue;
    }
    throw $e;
}</code>
登入後複製
登入後複製

失败方法3
通过redis来控制。因为冲突的根源就是于并发导致多个请求读取到了相同的seq导致的。所以考虑使用redis‘锁’的机制来实现。第一个获取到seq的请求会设置一个key,之后的请求都会在这个key的基础上进行获取。

<code>$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();</code>
登入後複製
登入後複製

失败方式4
另一种重试机制。上面的重试机制是靠MySQL数据库的插入冲突,现在的重试是通过Redis在Select语句层面实现。

<code>$device = "";
for($i=0;$i_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();
}</code>
登入後複製
登入後複製

失败方法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请求也同时在加锁,造成了循环等待锁释放的情况,产生死锁。

<code>//前一句使用select for update
//然后和insert包裹起来。多进程执行的话,会出现死锁。
//通过降低mysql的隔离水平确实可以实现,但是可能会产生不可重复读的问题.通俗的介绍就是:A请求在操作事务中两次读到的数据可能不一致。</code>
登入後複製
登入後複製

失败方法6
将两条sql放在一条sql里。 INSERT INTO table_2(device, seq) VALUES(?, (select seq + 1 from table_2) where device = ? )", array($device)

<code>//因为前一条sql是我简化版,其实他是一个分表的查询操作。
//这里的分表是一个router操作,所以不适合进行一条sql语句
//就是会依次查询我们这里分的几个表,最近的一个表查不到,查第二个</code>
登入後複製
登入後複製

方法7
修改数据表结构,将seq保存到独立的一个其他表中,每个device对应一个最大的seq。这样在select的时候就可以保证select for update 不会产生间隙锁,而是指在一条数据行上加锁。

  1. 贴上相关截图

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

大神来指点指点吧

<code>我实在是想不出什么好的办法了。将并发请求转化为串行,这个想法没有很好的实现。大神在哪里?</code>
登入後複製
登入後複製

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命令即可。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PHP的未來:改編和創新 PHP的未來:改編和創新 Apr 11, 2025 am 12:01 AM

PHP的未來將通過適應新技術趨勢和引入創新特性來實現:1)適應云計算、容器化和微服務架構,支持Docker和Kubernetes;2)引入JIT編譯器和枚舉類型,提升性能和數據處理效率;3)持續優化性能和推廣最佳實踐。

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

phpmyadmin怎麼打開 phpmyadmin怎麼打開 Apr 10, 2025 pm 10:51 PM

可以通過以下步驟打開 phpMyAdmin:1. 登錄網站控制面板;2. 找到並點擊 phpMyAdmin 圖標;3. 輸入 MySQL 憑據;4. 點擊 "登錄"。

redis集群模式怎麼搭建 redis集群模式怎麼搭建 Apr 10, 2025 pm 10:15 PM

Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

php:死亡還是簡單地適應? php:死亡還是簡單地適應? Apr 11, 2025 am 12:13 AM

PHP不是在消亡,而是在不斷適應和進化。 1)PHP從1994年起經歷多次版本迭代,適應新技術趨勢。 2)目前廣泛應用於電子商務、內容管理系統等領域。 3)PHP8引入JIT編譯器等功能,提升性能和現代化。 4)使用OPcache和遵循PSR-12標準可優化性能和代碼質量。

redis怎麼讀取隊列 redis怎麼讀取隊列 Apr 10, 2025 pm 10:12 PM

要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

phpmyadmin連接mysql phpmyadmin連接mysql Apr 10, 2025 pm 10:57 PM

如何使用 phpMyAdmin 連接到 MySQL?訪問 phpMyAdmin 的 URL,通常為 http://localhost/phpmyadmin 或 http://[您的服務器 IP 地址]/phpmyadmin。輸入您的 MySQL 用戶名和密碼。選擇您要連接的數據庫。點擊 "連接" 按鈕以建立連接。

redis計數器怎麼實現 redis計數器怎麼實現 Apr 10, 2025 pm 10:21 PM

Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

See all articles