如何处理下面的并发问题
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 不会产生间隙锁,而是指在一条数据行上加锁。
贴上相关截图
图片就如上所示。大部分的代码都是自己手工敲的,所以有些地方大家不用深究,了解清楚意思就可以。
希望大家可以有建设性建议
大神来指点指点吧
<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 不会产生间隙锁,而是指在一条数据行上加锁。
贴上相关截图
图片就如上所示。大部分的代码都是自己手工敲的,所以有些地方大家不用深究,了解清楚意思就可以。
希望大家可以有建设性建议
大神来指点指点吧
<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命令即可。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

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

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

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

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック









PHPの将来は、新しいテクノロジーの傾向に適応し、革新的な機能を導入することで達成されます。1)クラウドコンピューティング、コンテナ化、マイクロサービスアーキテクチャに適応し、DockerとKubernetesをサポートします。 2)パフォーマンスとデータ処理の効率を改善するために、JITコンパイラと列挙タイプを導入します。 3)パフォーマンスを継続的に最適化し、ベストプラクティスを促進します。

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

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

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

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

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

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

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