Croisement de requêtes SQL

Modérateurs: Ligevum, Crepuscule, Wargaming, Okracoke, Simerion, Heroes, Bestiaire, Exsulare, terato, pentacle, Nainwak

Croisement de requêtes SQL

Messagepar SirGalahaad » 20 Avr 2009, 16:39

bonjour,

voici un cas de figure :

le joueur 1 et le joueur 2 font une synchro via MSN pour tapper sur le joueur 3.

à T=0, joueur 1 récupère la vie de joueur 3 dans la BDD qui est alors de 100 points de vie

à T=1 (une fraction de secondes après) joueur 2 récupère lui aussi cette valeur

à T=2, le joueur 1 inflige 120 points de dégâts et tue le joueur 3, il uploade la nouvelle vie de joueur 3 dans la BDD à savoir 0

à T=3, le joueur 2 inflige 50 points de dégâts à joueur 3 et ré-uploade sa nouvelle vie qui est 100-50=50points de vie, du coup joueur 3 est rescucité et voit dans son historique :

(20-04-2009 18h00m01s)joueur 1 vous a fait 120 points de dégâts et vous a tué
(20-04-2009 18h00m01s)joueur 2 vous a fait 50 points de dégâts


je crois savoir qu'on peut protéger une requête sql de telle sorte qu'il se créé une sorte de file d'attente, est-ce la meilleure façon de gérer ce genre de cas (extrêmement rare mais TRES gênant), si oui comment doit-on faire?

pensez-vous qu'il existe une meilleure façon de procédé et si oui, comment avez vous résolu ce problème ?

d'avance merci pour vos conseils et astuces ! :)
SirGalahaad
 
Messages: 54
Inscription: 13 Nov 2006, 13:44
Localisation: Gréasque

Messagepar Gniarf » 21 Avr 2009, 10:03

ouach. visibilement il y a confusion entre le client (la page dans le navigateur, le click sur le lien "taper") et le serveur (la page php, la base mysql)

à T=0 et T=1 les joueurs récupèrent des valeurs d'affichage : rien n'est changé dans la base

à T=2, là ça bosse vraiment. la requete HTTP (le click sur le lien "taper") a lieu, Apache puis PHP la recoit. après quelques vérifs de base (même case, etc...) le serveur mysql indique le nombre de PVs du joueur 3 (100) à PHP, PHP constate que 120 > 100 et pouf le joueur 3 est mort, ses PV tombent peut-être à zéro ou un système quelconque indique qu'il est mort (un autre champ dans la même table ou une autre). son matériel tombe à terre, le premier joueur marque quelques points d'expérience... MySQL met tout un tas de données à jour

à T=3, une autre requete arrive mais cette fois le joueur 3 est déjà mort ! (c'est dans les mêmes vérifs de base qu'à T=2) du coup rien ne se passe, PHP dira typiquement que le joueur 3 n'est pas sur cette case ou qu'il est déjà mort ou retiré du jeu. il n'y a donc pas de UPDATE joueurs SET pv = pv - 50 WHERE id=$cible

DES FOIS, T=2 et T=3 c'est *vraiment* en même temps. en fait c'est plus rigolo quand le joueur 3 ne meurt pas, que le joueur 1 et le joueur 2 l'entament bien (genre -40 et -50 PV) mais qu'on constate qu'un seul des UPDATE est passé: au lieu qu'ilssoit à 10 PV, il sera à 50 ou 60.

mais c'est au niveau du serveur MySQL, pas d'une page PHP, donc la fenètre où ça peut arriver c'est vraiment très court et en fait comme il y a une file comme tu mentionnes ca arrive euh pratiquement jamais. en gros ca arrive quand MySQL est déjà bien surchargé et qu'il crache ses boyaux et qu'il va pas tarder à planter. à noter que cette bouse en profite alors souvent pour planter ou corrompre ses tables...

il faut alors utiliser des transactions, que MySQL fait assez mal. ça garantit ce qu'on appelle l'atomicité

dans la vraie vie, si c'est pas des données importantes (bancaires...) on s'en fout. et si c'est des données importantes on utilise pas MySQL qui pue qui pête.
Avatar de l’utilisateur
Gniarf
 
Messages: 43
Inscription: 13 Aoû 2005, 04:46

Messagepar SirGalahaad » 21 Avr 2009, 13:52

arf, donc selon toi il n'y a pas de vrai solution à ce problème ? on m'avait pourtant parler d'une requête en mysql permettant de le contourner :cry:
SirGalahaad
 
Messages: 54
Inscription: 13 Nov 2006, 13:44
Localisation: Gréasque

Messagepar Chatissimus » 22 Avr 2009, 00:09

A partir du moment ou tu connais le problème il y a forcement une solution, on est en informatique pas en théologie :-D

pour moi (et HC)

on fait des mises à jour par delta
(si J1 a 100pv et que j2 lui fait 40 deg on a
PV=PV-40
et pas PV=60)

on a un flag pour la mort (qui disparait à la connexion du "mort")

et on utilise des transactions


en gros on a pour l'update PV
UPDATE joueurs SET PV=PV - deg WHERE PV > deg AND Mort=0

et pour le Kill
UPDATE joueurs SET PV=PV_Max, Mort=1 WHERE Mort=0

les commit étant obligatoirement séquencés, si un des ces update ne change rien en BD (mysql_affected_rows), c'est qu'il y a eu des accès concurrentiels incompatibles, donc on fait un rollback sur une partie des infos des joueurs.

oui sur HC on protège nos joueurs comme dans les banques,
vil Gniarf j'ai jamais vu de grosses applis ne faisant pas de transactionnel, même sur des données insignifiantes :-)
(cela soit dit j'en ai jamais vu non plus sous MySQL ;-) )
Avatar de l’utilisateur
Chatissimus
 
Messages: 51
Inscription: 19 Jan 2007, 22:50

Messagepar Chatissimus » 22 Avr 2009, 00:18

(on peut pas éditer ici ?)

petit édit,

l'utilisation de transaction n'est intéressante que si tu as d'autres modifications de la BD, faites avant la mise à jour du joueur, a révoquer.

Sur MySQL les transactions ne peuvent être faite sur des tables MyISAM (Si une table MyISAM est concerné par un rollback, cela ne lève pas d'erreur, mais cela ne fait rien) tu dois utiliser des tables transactionnelles comme innoDB
Avatar de l’utilisateur
Chatissimus
 
Messages: 51
Inscription: 19 Jan 2007, 22:50

Messagepar SirGalahaad » 22 Avr 2009, 07:12

merci beaucoup ! j'ai justement toutes mes nouvelles tables en innodb ! :)

je vais chercher pour voir comment on fait des requêtes permettant des rollback, merci pour les infos !
SirGalahaad
 
Messages: 54
Inscription: 13 Nov 2006, 13:44
Localisation: Gréasque

Messagepar Daimonos Tereutes » 22 Avr 2009, 12:09

A noter que les transaction est un processus très lourd, qui demande au serveur SQL de conserver une version concurrente des données pour chaque joueur. En pratique ça ne peux pas être utilisé pour un jeu php/mysql qui traite plusieurs centaines de requêtes par secondes.

Il existe deux techniques plus adaptées à un jeu php/mysql :
- l'utilisation de modifications atomiques.
C'est à dire plutôt que de faire une requête pour lire une donnée et une autre pour modifier cette donnée, tu modifie cette donnée directement.
C'est l'exemple qu'à donné Chatissimus (PV=PV-40)
Cette technique est assurément la plus rapide et la moins couteuse.
De plus elle est quasiment toujours applicable.
Mais elle est pas toujours suffisante.
- l'utilisation de verrou (à appliquer en plus de la technique précédente)
Là deux manière de procéder soit on utilise le mécanisme propriétaire de mysql. ex : SELECT PV FROM JOUEUR WHERE id=1 [FOR UPDATE | LOCK IN SHARE MODE]
FOR UPDATE va interdire à un autre processus de modifier ou de lire les donnée du SELECT et ainsi faire patienter les autres processus.
LOCK IN SHARED MODE, lui n'interdis que l'écriture ou la modification.

Concrètement ce qui se passe :
J1 attaque J3
SELECT... FOR UPDATE ==> lecture pour J1
J2 attaque J3
SELECT... FOR UPDATE ==> verrou sur J3 de la part de J1, J2 est mis en attente.
calcul des dégats de J1:
UPDATE ... ==> J1 enlève des PV à J3 et suppression du verrou.
le SELECT ... FOR UPDATE de J2 s'exécute et J2 prend le verrou
calcul des dégats de J2:
UPDATE ... ==> J2 enlève des PV à J3 et suppression du verrou.

Il est aussi possible de gérer le verrou à la main, mais ça se justifie rarement.

A noter, là où InnodB ne place un verrou que sur le ou les tuples utilisés, MyISAM lock la table entière.

Il y a aussi toute une théorie sur la manière de procéder lorsque l'on doit verrouiller plusieurs ressources et sur l'ordre à utiliser pour ce faire afin d'éviter les inter-blocages.
Mais ceci sera pour une autre fois ;)
Admin technique de nainwak.org
Daimonos Tereutes
Responsable Technique de l'Association
 
Messages: 926
Inscription: 30 Mar 2004, 18:39

Messagepar SirGalahaad » 24 Avr 2009, 17:11

merci beaucoup pour ces infos !

petite question, si un joueur fait un SELECT FOR UPDATE et qu'il plante son internet avant d'avoir dévérouiller le champ de données (une chance sur des milliards pour que ça arrive) tu sais ce qui se passe ?
le champ reste-t'il bloqué indéfiniment ?
SirGalahaad
 
Messages: 54
Inscription: 13 Nov 2006, 13:44
Localisation: Gréasque

Messagepar Gniarf » 25 Avr 2009, 12:23

ce n'est pas le joueur qui fait ce SELECT FOR UPDATE, c'est MySQL. sur ordre de la page PHP, le déverouillage est quelque lignes plus bas.

MySQL a un système de timeout pour ne pas bloquer, innodb lock wait timeout, ici et à titre indicatif à 50 secondes.

donc ce qui peut se passer est que le serveur (la machine physique) est surchargée, ca rame de tous les cotés, la page PHP prend plus que son temps alloué pour s'executer (genre 30 secondes), elle plante ou arrete de s'executer, et peu après MySQL fera le ménage en annulant ou jetant tout ce qui ne l'interesse pas ou qui n'est plus à l'ordre du jour.

sauf que donc, des fois, MySQL adore planter ou s'écrouler à son tour sous la charge.
Avatar de l’utilisateur
Gniarf
 
Messages: 43
Inscription: 13 Aoû 2005, 04:46

Messagepar Daimonos Tereutes » 26 Avr 2009, 11:28

Si le script php est interrompu, celui-ci va alors automatiquement fermer la connexion avec mysql (de même qu'il ferme tout fichier ouvert).
Mysql ne voyant plus le propriétaire du verrou, va alors libérer les ressources dans l'instant.

Le cas où tu risque de rencontrer un hors-temps (timeout en anglais) sur un verrou, c'est plus dans le cas d'un inter-blocage.

Imagine par exemple que J1 attaque J2 et que J2 attaque J1 en même temps.

J1 va obtenir le verrou sur J2 et J2 va obtenir le verrou sur J1.
Ensuite lorsque J1 va vouloir modifier ses propres données (gain d'XP par exemple) il ne pourra pas car J2 aura déjà le verrou sur J1.

Si on ne fait rien, le serveur Mysql va resté bloqué pendant la durée du hors temps (très très mauvaise idée) puis va finir par virer un des verrous. Le résultat est alors plutôt aléatoire.

Là on peux envisager plusieurs solution, sur un RPG on peux virer le verrou et dire qu'il y a mort simultané des deux :twisted:

Mais dans le cas général on va vouloir soit éviter cette situation, soit établir sa propre règle de résolution du conflit (et si possible qui bloque pas le serveur pendant 30s)

Pour éviter le problème de l'inter-blocage, il y a deux règles de base :
- Toujours verrouiller la totalité des ressources nécessaire.

C'est à dire que lors de l'attaque de J1 sur J2, il faut demander non seulement le verrou sur J2 mais aussi sur J1. Histoire d'être sûr de pouvoir modifier les deux joueurs.

Mais ce n'est pas suffisant car dans le scénario suivant on retrouve le même problème :
J1 attaque J2 et demande le verrou sur J2 et l'obtient.
J2 attaque J2 et demande le verrou sur J1 et l'obtient.
J1 demande le verrou sur lui-même et est mis en attente.
J2 demande le verrou sur lui même et est mis en attente.

Problème, aucun des deux ne peut aller jusqu'au bout et chacun attend l'autre.

- Toujours verrouiller les ressources dans le même ordre.
C'est à dire que si J1 attaque J2 ou J2 attaque J1, ils doivent toujours demander le verrou dans le même ordre:
J1 attaque J2 et demande le verrou sur J1 et l'obtient.
J2 attaque J1 et demande le verrou sur J1 et est mis en attente.
J1 demande ensuite le verrou sur J2 et l'obtient.
J1 fini ses traitement et libère ses verrou.
J2 obtient le verrou sur J1
J2 demande ensuite le verrou sur J2 et l'obtient.
...

A noter que l'ordre de libération des verrou est aussi important que l'ordre d'acquisition et qu'il faut toujours libérer les verrou dans l'ordre inverse d'acquisition.
Pour les curieux qui se demandent pourquoi, refaites le même scénario avec J1 qui attaque J2, qui lui même attaque J3 qui lui attaque J1. Vous verrez alors que libérer les verrous dans le mauvais ordre va aussi engendre un inter-blocage.


A noter que la gestion de verrou sur des ressources multiples en imposant un ordre précis peut être beaucoup plus simple avec une table dédié et quelques fonctions php créées pour l'occasion.
Admin technique de nainwak.org
Daimonos Tereutes
Responsable Technique de l'Association
 
Messages: 926
Inscription: 30 Mar 2004, 18:39

Messagepar Haiken » 27 Avr 2009, 19:01

cf http://www.magazine-jeux.com/La-securit ... MySQL.html chapitre "Concurrence d’accès aux données"

Je recommande toujours d'utiliser les transactions, même si DT a l'air de dire que c'est "lourd" : c'est quand même le rôle principal d'une base de données de gérer des transactions (avec le stockage et le requêtage des données), elles ont été inventées pour ça !
Et puis c'est plus simple (rien qu'à lire certains posts ici, j'ai mal à la tête et pourtant je connais tout ça très bien)

Quand vous aurez des milliers de joueurs, on en reparlera et on optimisera les cas spécifiques (démarche d'optimisation classique !)

=> Tables innoDB avec BEGIN au début et COMMIT à la fin (ROLLLBACK en cas d'erreur)
Avatar de l’utilisateur
Haiken
Président de l'Association
 
Messages: 666
Inscription: 01 Avr 2004, 09:00

Messagepar SirGalahaad » 03 Mai 2009, 07:30

merci pour toutes ces informations, elles me sont très utiles !
SirGalahaad
 
Messages: 54
Inscription: 13 Nov 2006, 13:44
Localisation: Gréasque


Retourner vers Questions Pratiques/Techniques

Qui est en ligne

Utilisateurs parcourant ce forum: Aucun utilisateur enregistré et 1 invité

cron