如何处理下面的并发问题
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命令即可。

Outils d'IA chauds

Undresser.AI Undress
Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover
Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool
Images de déshabillage gratuites

Clothoff.io
Dissolvant de vêtements AI

AI Hentai Generator
Générez AI Hentai gratuitement.

Article chaud

Outils chauds

Bloc-notes++7.3.1
Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise
Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1
Puissant environnement de développement intégré PHP

Dreamweaver CS6
Outils de développement Web visuel

SublimeText3 version Mac
Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Sujets chauds

PHP 8.4 apporte plusieurs nouvelles fonctionnalités, améliorations de sécurité et de performances avec une bonne quantité de dépréciations et de suppressions de fonctionnalités. Ce guide explique comment installer PHP 8.4 ou mettre à niveau vers PHP 8.4 sur Ubuntu, Debian ou leurs dérivés. Bien qu'il soit possible de compiler PHP à partir des sources, son installation à partir d'un référentiel APT comme expliqué ci-dessous est souvent plus rapide et plus sécurisée car ces référentiels fourniront les dernières corrections de bogues et mises à jour de sécurité à l'avenir.

Visual Studio Code, également connu sous le nom de VS Code, est un éditeur de code source gratuit – ou environnement de développement intégré (IDE) – disponible pour tous les principaux systèmes d'exploitation. Avec une large collection d'extensions pour de nombreux langages de programmation, VS Code peut être c

Ce tutoriel montre comment traiter efficacement les documents XML à l'aide de PHP. XML (Language de balisage extensible) est un langage de balisage basé sur le texte polyvalent conçu à la fois pour la lisibilité humaine et l'analyse de la machine. Il est couramment utilisé pour le stockage de données et

Une chaîne est une séquence de caractères, y compris des lettres, des nombres et des symboles. Ce tutoriel apprendra à calculer le nombre de voyelles dans une chaîne donnée en PHP en utilisant différentes méthodes. Les voyelles en anglais sont a, e, i, o, u, et elles peuvent être en majuscules ou en minuscules. Qu'est-ce qu'une voyelle? Les voyelles sont des caractères alphabétiques qui représentent une prononciation spécifique. Il y a cinq voyelles en anglais, y compris les majuscules et les minuscules: a, e, i, o, u Exemple 1 Entrée: String = "TutorialSpoint" Sortie: 6 expliquer Les voyelles dans la chaîne "TutorialSpoint" sont u, o, i, a, o, i. Il y a 6 yuans au total

Les tableaux sont des structures de données linéaires utilisées pour traiter les données dans la programmation. Parfois, lorsque nous traitons les tableaux, nous devons ajouter de nouveaux éléments au tableau existant. Dans cet article, nous discuterons de plusieurs façons d'ajouter des éléments à la fin d'un tableau en PHP, avec des exemples de code, une sortie et une analyse de complexité du temps et de l'espace pour chaque méthode. Voici les différentes façons d'ajouter des éléments à un tableau: Utilisez des crochets [] En PHP, la façon d'ajouter des éléments à la fin d'un tableau est d'utiliser des crochets []. Cette syntaxe ne fonctionne que dans les cas où nous ne voulons ajouter qu'un seul élément. Ce qui suit est la syntaxe: $ array [] = valeur; Exemple

Nexo Exchange: Plateforme de prêt de crypto-monnaie suisse Analyse approfondie Nexo est une plate-forme qui fournit des services de prêt de crypto-monnaie, en soutenant l'hypothèque et les prêts de plus de 40 actifs cryptographiques, devises fiduciaires et de stablescoines. Il domine les marchés européens et américains et s'engage à améliorer l'efficacité, la sécurité et la conformité de la plate-forme. De nombreux investisseurs veulent savoir où est enregistré l'échange Nexo et la réponse est: la Suisse. Nexo a été fondée en 2018 par la société suisse FinTech Credssimo. NEXO Exchange Emplacement géographique et réglementation: Nexo a son siège à Zug, en Suisse, une région bien connue adaptée aux crypto-monnaies. La plate-forme coopère activement à la supervision de divers gouvernements et a été dans le réseau d'application de la loi sur le crime financier des États-Unis (FINCEN) et le Canadian Finance

Numéro d'Armstrong Le nombre Armstrong fait référence à la somme de n pouvoirs de chaque chiffre d'un nombre égal au nombre lui-même, où n est le nombre de chiffres du nombre. Cet article discutera de la façon de vérifier si un numéro donné est un numéro Armstrong. Exemple Apprenons le numéro Armstrong avec quelques exemples d'entrée et de sortie. entrer 9474 Sortir Oui expliquer Il s'agit d'un numéro à quatre chiffres. Les nombres de ce nombre sont de 9, 4, 7 et 4. 9474 = 94 44 74 44 = 6561 256 2401 256 = 9474 C'est donc un numéro Armstrong. entrer 153 Sortir Oui expliquer Ceci est un numéro à triple chiffre. Les nombres de ce nombre sont 1, 5 et 3

Une surveillance efficace des bases de données Redis est essentielle pour maintenir des performances optimales, identifier les goulots d'étranglement potentiels et garantir la fiabilité globale du système. Redis Exporter Service est un utilitaire robuste conçu pour surveiller les bases de données Redis à l'aide de Prometheus. Ce didacticiel vous guidera tout au long de l'installation et de la configuration complètes de Redis Exporter Service, vous garantissant ainsi d'établir une solution de surveillance de manière transparente. En suivant ce didacticiel, vous obtiendrez une configuration de surveillance entièrement opérationnelle pour surveiller efficacement les mesures de performances de votre base de données Redis.
