if() et INSERT SQL simultanés sur 2 sessions différentes

VaN
Mammouth du PHP | 1107 Messages

04 oct. 2011, 17:43

Bonjour,

je poste mon problème dans la section PHP, car je ne sais pas vraiment si la solution viendra du PHP ou du SQL.

J'ai un projet qui propose des événements, sur lesquels on peut réserver des places. Chaque événement à un nombre de réservations maximum ainsi qu'un nombre de réservations maximum par utilisateur.
Lorsqu'un utilisateur pose une réservation, je vérifie que le nombre ce place restantes sur l'événement est suffisant pour accueillir sa réservation (en plus de vérifier qu'il ne dépasse pas sa limite de réservations pour cet événement).

Si le nombre de place qu'il réserve est supérieur au nombre de places restantes sur l'événement, rien ne se passe.

Le problème est que aujourd'hui, sur un événement très prisé, 3 réservations ont été effectuées à la même seconde ! Avant ces 3 réservations, le nombre de réservations sur l'événement était de 6, pour un maximum de 7 places disponibles. Donc évidemment, le formulaire préalable leur a permis de poster une demande pour 1 place. Et la requête d'insertion s'est faite à la même secondes, passant le nombre de réservations pour cet événement à 9, pour un maximum de 7.

Voici mon code PHP faisant les vérifications juste avant l'insertion :
// On va vérifier combien de réservations pour cet événement ont déjà été faites
			$current_bookings = get_value_from_query(sprintf("SELECT SUM(booking_seats) FROM " . TABLES__BOOKINGS . " WHERE booking_event_id = '%d'", $clean['booking_event_id']));
			if(!$current_bookings) {
				$current_bookings = 0;
			}
			
			$max_seats_available_for_event = $max_seats_available - $current_bookings;
			
			if($max_seats_available_for_event <= $max_seats_available_for_user) {
				$max_seats_available = $max_seats_available_for_event;
			}
			else {
				$max_seats_available = $max_seats_available_for_user;
			}
			
			if($clean['booking_seats'] <= $max_seats_available) {
				
				sql_query(sprintf("INSERT INTO " . TABLES__BOOKINGS . " (booking_event_id, booking_user_id, booking_datetime, booking_seats, booking_status) VALUES('%d', '%d', NOW(), '%d', 0)", $clean['booking_event_id'], $clean['booking_user_id'], $clean['booking_seats']));
				$clean['booking_id'] = mysql_insert_id();
}
$clean['booking_seats'] est le nombre de place demandées par l'utilisateur au moment de sa réservation (pour cet événement, l'utilisateur ne peut demander qu'une seule place).

Le problème est que lorsque mes 3 utilisateurs ont soumis le formulaire à la même seconde, $max_seats_available était bien supérieur ou égal au nombre de places demandées, et donc leur réservation à été insérée.. Et je me retrouve donc avec 9 réservations pour un événement ne proposant que 7 places : )

Je cherche donc des solutions, PHP ou SQL. J'ai pensé au LOCK TABLE de mysql, mais cela ne résoudra pas le problème, la requête sera en queue et passera ensuite. Ou alors je lock la table juste avant ma première requete qui va chercher le nombre de places encore disponibles pour l'évènement, et je la delock après l'insertion ? Si le mon script rencontre un LOCK en SQL, il freeze le temps que la table soit DELOCK ?

Si vous voyez une autre solution, je suis preneur : )

ViPHP
xTG
ViPHP | 7331 Messages

04 oct. 2011, 17:47

Il faut lock avant le test, et delock après l'insert. ;)

VaN
Mammouth du PHP | 1107 Messages

04 oct. 2011, 18:18

Et donc le fait que le script rencontre un LOCK va faire qu'il va freeze completement jusqu'à ce que ça delock ? pas de risque qu'il zappe la requête et continuer à tourner ?

ViPHP
xTG
ViPHP | 7331 Messages

04 oct. 2011, 19:36

Non il attendra jusqu'à timeout du serveur si besoin sur cette instruction.

VaN
Mammouth du PHP | 1107 Messages

05 oct. 2011, 11:25

je viens de modifier mon script, avec les locks :
// On va vérifier combien de réservations pour cet événement ont déjà été faites
sql_query("LOCK TABLES " . TABLES__BOOKINGS . " READ, " . TABLES__BOOKINGS . " AS bookings2 WRITE");
			$current_bookings = get_value_from_query(sprintf("SELECT SUM(booking_seats) FROM " . TABLES__BOOKINGS . " WHERE booking_event_id = '%d'", $clean['booking_event_id']));
			if(!$current_bookings) {
				$current_bookings = 0;
			}
			
			$max_seats_available_for_event = $max_seats_available - $current_bookings;
			
			if($max_seats_available_for_event <= $max_seats_available_for_user) {
				$max_seats_available = $max_seats_available_for_event;
			}
			else {
				$max_seats_available = $max_seats_available_for_user;
			}
			
			if($clean['booking_seats'] <= $max_seats_available) {
				
				sql_query(sprintf("INSERT INTO " . TABLES__BOOKINGS . " (booking_event_id, booking_user_id, booking_datetime, booking_seats, booking_status) VALUES('%d', '%d', NOW(), '%d', 0)", $clean['booking_event_id'], $clean['booking_user_id'], $clean['booking_seats']));
				$clean['booking_id'] = mysql_insert_id();
sql_query("UNLOCK TABLES");
}
Et je me retrouve avec une grosse erreur
Table 'bookings' was locked with a READ lock and can't be updated
Comment cela se fait-il ? Normalement le lock sert à vérouiller les tables sauf pour la session mysql courante non ?

Eléphant du PHP | 267 Messages

05 oct. 2011, 16:10

Bonjour,

1) tu peux gérer avec les transactions SQL mais c'est assez couteux en execution

2) tu peux gérer avec les LOCK comme cité plus haut mais le risque c'est que si ça plante entre le LOCK et le DELOCK, ça peut rester locker tres longtemps

3) une solution simple en PHP :
- tu vérifies le nombre de places (ex: 7 sur 8 disponibles)
- tu réserves une nouvelle place
- tu revérifies le nombre de places :
- si 8 places réservées, c'est OK
- si 9 places réservées, supprimer la réservation en cours si c'est la 9eme (il faut donc mémoriser l'ordre des réservations avec l'heure pour ne supprimer que la 9eme réservation et pas la 8eme)

a+
dix2

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

06 oct. 2011, 01:39

Pourquoi pas utiliser un trigger before insert qui vérifie que le nombre de place dispo est bon ?
Ce qui bien entendu fera la meme chose qu'en php mais sur le sgbd moin de risque le trigger agi sur la table au moment de l'insertion tu peut donc gérer la chose correctement.
Je n'ai pas retrouvé où j'ai pu lire cela mais il me semble que le trigger effectue un lock. Sinon tu le fait via une transaction.

Un peu de lecture http://sqlpro.developpez.com/cours/sqlaz/techniques/#L3

@+
Il en faut peu pour être heureux ......