目次
回复内容:

如何处理下面的并发问题

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 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

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 and Python each have their own advantages, and the choice should be based on project requirements. 1.PHPは、シンプルな構文と高い実行効率を備えたWeb開発に適しています。 2。Pythonは、簡潔な構文とリッチライブラリを備えたデータサイエンスと機械学習に適しています。

Redisクラスターモードの構築方法 Redisクラスターモードの構築方法 Apr 10, 2025 pm 10:15 PM

Redisクラスターモードは、シャードを介してRedisインスタンスを複数のサーバーに展開し、スケーラビリティと可用性を向上させます。構造の手順は次のとおりです。異なるポートで奇妙なRedisインスタンスを作成します。 3つのセンチネルインスタンスを作成し、Redisインスタンスを監視し、フェールオーバーを監視します。 Sentinel構成ファイルを構成し、Redisインスタンス情報とフェールオーバー設定の監視を追加します。 Redisインスタンス構成ファイルを構成し、クラスターモードを有効にし、クラスター情報ファイルパスを指定します。各Redisインスタンスの情報を含むnodes.confファイルを作成します。クラスターを起動し、CREATEコマンドを実行してクラスターを作成し、レプリカの数を指定します。クラスターにログインしてクラスター情報コマンドを実行して、クラスターステータスを確認します。作る

PHP:それは死にかけていますか、それとも単に適応していますか? PHP:それは死にかけていますか、それとも単に適応していますか? Apr 11, 2025 am 12:13 AM

PHPは死にかけていませんが、常に適応して進化しています。 1)PHPは、1994年以来、新しいテクノロジーの傾向に適応するために複数のバージョンの反復を受けています。 2)現在、電子商取引、コンテンツ管理システム、その他の分野で広く使用されています。 3)PHP8は、パフォーマンスと近代化を改善するために、JITコンパイラおよびその他の機能を導入します。 4)Opcacheを使用してPSR-12標準に従って、パフォーマンスとコードの品質を最適化します。

phpmyadminを開く方法 phpmyadminを開く方法 Apr 10, 2025 pm 10:51 PM

次の手順でphpmyadminを開くことができます。1。ウェブサイトコントロールパネルにログインします。 2。phpmyadminアイコンを見つけてクリックします。 3。MySQL資格情報を入力します。 4.「ログイン」をクリックします。

Redisのソースコードを読み取る方法 Redisのソースコードを読み取る方法 Apr 10, 2025 pm 08:27 PM

Redisソースコードを理解する最良の方法は、段階的に進むことです。Redisの基本に精通してください。開始点として特定のモジュールまたは機能を選択します。モジュールまたは機能のエントリポイントから始めて、行ごとにコードを表示します。関数コールチェーンを介してコードを表示します。 Redisが使用する基礎となるデータ構造に精通してください。 Redisが使用するアルゴリズムを特定します。

phpmyAdminの脆弱性の概要 phpmyAdminの脆弱性の概要 Apr 10, 2025 pm 10:24 PM

PHPMyAdminセキュリティ防衛戦略の鍵は次のとおりです。1。PHPMyAdminの最新バージョンを使用し、PHPとMySQLを定期的に更新します。 2.アクセス権を厳密に制御し、.htaccessまたはWebサーバーアクセス制御を使用します。 3.強力なパスワードと2要素認証を有効にします。 4.データベースを定期的にバックアップします。 5.機密情報が公開されないように、構成ファイルを慎重に確認します。 6。Webアプリケーションファイアウォール(WAF)を使用します。 7.セキュリティ監査を実行します。 これらの測定は、不適切な構成、古いバージョン、または環境セキュリティのリスクにより、PHPMyAdminによって引き起こされるセキュリティリスクを効果的に削減し、データベースのセキュリティを確保することができます。

Redisコマンドの使用方法 Redisコマンドの使用方法 Apr 10, 2025 pm 08:45 PM

Redis指令を使用するには、次の手順が必要です。Redisクライアントを開きます。コマンド(動詞キー値)を入力します。必要なパラメーターを提供します(指示ごとに異なります)。 Enterを押してコマンドを実行します。 Redisは、操作の結果を示す応答を返します(通常はOKまたは-ERR)。

See all articles