web-dev-qa-db-fra.com

PHP / MySQL - Comment éviter deux requêtes * Mise à jour

J'ai une question ... exemple. Un utilisateur achètera quelque chose pour son dollar

  1. Vérifiez son solde en USD
  2. Déduire l'USD de son compte
  3. Faire une commande -> file d'attente
  4. utilisateur obtient son article et l'autre obtient son USD

Disons que les utilisateurs font 5 demandes dans la même seconde (très rapide) . Il est donc possible (et arrive) que 5 demandes soient en cours d'exécution . Il n'a que de l'argent pour acheter qu'à partir de 1 demande. Maintenant, les demandes Sont si rapides que le script vérifie son solde, mais n'est pas si rapide, qu'il déduit l'argent de son compte. Donc, les demandes passeront deux fois! Comment le résoudre?

J'utilise LOCK dans MySQL avant de commencer le processus:

  1. IS_FREE_LOCK - vérifie s'il y a un verrou pour cet utilisateur sinon -> 2.
  2. GET_LOCK - définit le verrou
  3. faire la commande/transaction
  4. RELEASE_LOCK - déverrouille le verrou

Mais cela ne fonctionne pas vraiment. Y a-t-il un autre moyen?

function lock($id) {
  mysql_query("SELECT GET_LOCK('$id', 60) AS 'GetLock'");
}

function is_free($id) {
  $query = mysql_query("SELECT IS_FREE_LOCK('$id') AS 'free'");
  $row = mysql_fetch_assoc($query);
  if($row['free']) {
    return true;
  } else {
    return false;
  }
}

function release_lock($id) {
  mysql_query("SELECT RELEASE_LOCK('$id')");
}

function account_balance($id) {
  $stmt = $db->prepare("SELECT USD FROM bitcoin_user_n WHERE id = ?");
  $stmt->execute(array($id));
  $row = $stmt->fetch(PDO::FETCH_ASSOC);

  return $row['USD'];
}

if(is_free(get_user_id())) {
  lock(get_user_id());
  if(account_balance(get_user_id()) < str2num($_POST['amount'])) {
    echo "error, not enough money";
  } else {
    $stmt = $db->prepare("UPDATE user SET USD = USD - ? WHERE id = ?");
    $stmt->execute(array(str2num($_POST['amount']), get_user_id()));
    $stmt = $db->prepare("INSERT INTO offer (user_id, type, price, amount) VALUES (?, ?, ?, ?)");
    $stmt->execute(array(get_user_id(), 2, str2num($_POST['amount']), 0));
}

Mise à jour Testé la fonction de transaction avec SELECT ... FOR UPDATE

$db->beginTransaction();
$stmt = $db->prepare("SELECT value, id2 FROM test WHERE id = ? FOR UPDATE");
$stmt->execute(array(1));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if($row['value'] > 1) {
  sleep(5);
  $stmt = $db->prepare('UPDATE test SET value = value - 5 WHERE id = 1');
  $stmt->execute();
  $stmt = $db->prepare('UPDATE test SET value = value + 5 WHERE id = 2');
  $stmt->execute();
  echo "did have enough money";
} else {
  echo "no money";
}
$db->commit();
22
DjangoSi

Tout d'abord, vous devez utiliser des transactions, mais cela ne suffit pas. Dans votre transaction, vous pouvez utiliser SELECT FOR UPDATE .

En gros, cela signifie: "Je vais mettre à jour les enregistrements que je sélectionne", ce qui permet de définir les mêmes verrous que ceux définis par UPDATE. Mais rappelez-vous que cela doit se produire dans une transaction avec autocommit désactivé.

25
user1703809

Utilisez _ TRANSACTION et si cela échoue, vous pouvez revenir en arrière.

Par exemple, supposons que le solde actuel est de 20 $.

Connection A               Connection B
=======================    ===========================
BEGIN TRANSACTION         
                           BEGIN TRANSACTION
SELECT AccountBalance  
                           SELECT AccountBalance
--returns $20
--sufficient balance,
--proceed with purchase
                           --returns $20
                           --sufficient balance,
                           --proceed with purchase

                            --update acquires exclusive lock
                           UPDATE SET AccountBalance
                              = AccountBalance - 20
--update blocked due
UPDATE SET AccountBalance
  = AccountBalance - 20

                           --order complete
                           COMMIT TRANSACTION

--update proceeds

--database triggers
--constraint violation
--"AccountBalance >= 0"

ROLLBACK TRANSACTION
6
Oden

C'est comme ça que je le faisais il y a plusieurs années.

results = query("UPDATE table SET value=value-5 WHERE value>=5 AND ID=1")
if (results == 1) YEY!

(Est-ce toujours une méthode fiable?)

5
Kols

vous devez utiliser TRANSACTION au niveau d’isolement SERIALIZABLE.

1
Smoky McPot

Vous devez utiliser Data revision pour MySQL UPDATE.

0
Ego Slayer