web-dev-qa-db-fra.com

Will laravel table de verrouillage de transaction de base de données?

J'utilise la transaction de base de données de laravel5.5 pour une application de paiement en ligne. J'ai une table company_account pour enregistrer chaque paiement (type, amount, create_at, gross_income). J'ai besoin d'accéder au gross_income Du dernier enregistrement lors de la création d'un nouvel enregistrement. J'ai donc besoin de verrouiller la table lors de la transaction avec un verrou de table en lecture et en écriture pour éviter de nombreux paiements en même temps.

J'ai fait référence au document de laravel, mais je ne sais pas si la transaction verrouillera la table. Si la transaction verrouille la table, quel est le type de verrou (verrou en lecture, verrou en écriture ou les deux)?

DB::transaction(function () {
    // create company_account record

    // create use_account record
}, 5);

Code:

DB::transaction(function ($model) use($model) {
    $model = Model::find($order->product_id);
    $user = $model->user;

    // **update** use_account record
    try {
        $user_account = User_account::find($user->id);
    } catch (Exception $e){
        $user_account = new User_account;
        $user_account->user_id  = $user->id;
        $user_account->earnings = 0;
        $user_account->balance  = 0;
    }
    $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->save();

    // **create** company_account record
    $old_tiger_account = Tiger_account::latest('id')->first();

    $tiger_account = new Tiger_account;
    $tiger_account->type = 'model';
    $tiger_account->order_id = $order->id;
    $tiger_account->user_id = $user->id;
    $tiger_account->profit = $order->fee;
    $tiger_account->payment = 0;
    $tiger_account->gross_income = $old_tiger_account-> gross_income + $order->fee;
    $tiger_account->save();
}, 5);

les références:
Comment passer le paramètre à Laravel DB :: transaction ()

19
Kris Roofe

Comme vous mettez à jour 2 tables, vous devez toujours utiliser la transaction pour synchroniser les modifications. Considérez le code suivant:

DB::transaction(function () {
    $model = Model::find($order->product_id);
    $user = $model->user();

    DB::insert("
        insert into user_account (user_id, earnings, balance) values (?, ?, ?)
        on duplicate key update
        earnings = earnings + values(earnings),
        balance = balance + values(balance)
    ", [$user->id, $order->fee * self::USER_COMMISION_RATIO, $order->fee * self::USER_COMMISION_RATIO]);

    DB::insert(sprintf("
        insert into tiger_account (`type`, order_id, user_id, profit, payment, gross_income)
            select '%s' as `type`, %d as order_id, %d as user_id, %d as profit, %d as payment, gross_income + %d as gross_income
            from tiger_account
            order by id desc
            limit 1
    ", "model", $order->id, $user->id, $order->fee, 0, $order->fee));

}, 5);

Il y a 2 requêtes atomiques. Tout d'abord, insérez un enregistrement dans user_account table, une autre insère un enregistrement dans tiger_account.

Vous avez besoin de la transaction pour garantir qu'aucune modification n'est appliquée si quelque chose de terrible s'est produit entre ces 2 requêtes. La chose terrible n'est pas une demande simultanée, mais une mort subite de l'application php, de la partition réseau ou de tout autre élément qui empêche l'exécution d'une deuxième requête. Dans ce cas, les modifications de la première requête ont été annulées, de sorte que la base de données reste dans un état cohérent.

Les deux requêtes sont atomiques, ce qui garantit que les calculs dans chaque requête sont effectués de manière isolée, et aucune autre requête ne change la table pour le moment. Dire qu'il est possible que 2 demandes simultanées traitent 2 paiements pour le même utilisateur en même temps. Le premier insérera ou mettra à jour un enregistrement dans le user_account table et la deuxième requête mettra à jour l'enregistrement, les deux ajouteront un enregistrement à tiger_account, et toutes les modifications seront définitivement définies dans la base de données lorsque chaque transaction est validée.

Quelques hypothèses que j'ai faites:

  • user_id est une clé primaire dans user_account table.
  • Il y a au moins 1 enregistrement dans tiger_account. Celui appelé $old_tiger_account dans le code OP, car il n'est pas clair quel est le comportement attendu lorsqu'il n'y a rien dans la base de données.
  • Tous les champs monétaires sont des entiers et non des flottants.
  • C'est MySQL DB. J'utilise la syntaxe MySQL pour illustrer l'approche. D'autres versions SQL peuvent avoir une syntaxe légèrement différente.
  • Tous les noms de table et noms de colonne dans les requêtes brutes. Ne vous souvenez pas des conventions de nommage illuminate.

n mot d'avertissement. Ce sont des requêtes brutes. Vous devriez faire très attention à la refactorisation des modèles à l'avenir, et écrire quelques tests d'intégration supplémentaires, car une logique d'application est passée de l'impératif PHP au SQL déclaratif. Je pense que c'est un prix équitable de garantir conditions de course, mais je tiens à ce qu'il soit clair qu'il ne vient pas gratuitement.

9
Alex Blex

Je suis tombé sur cette réponse de la question MySQL: Transactions vs Locking Tables , qui explique la transaction et la table de verrouillage. Il montre que la transaction et le verrouillage doivent être utilisés ici.

Je me réfère à Laravel lockforupdate (Pessimistic Locking) et Comment passer le paramètre à Laravel DB :: transaction () , puis passer au-dessous du code .

Je ne sais pas si c'est une bonne mise en œuvre , au moins ça marche maintenant.

DB::transaction(function ($order) use($order) {
    if($order->product_name == 'model')
    {
        $model = Model::find($order->product_id);
        $user = $model->user;

        $user_account = User_account::where('user_id', $user->id)->lockForUpdate()->first();

        if(!$user_account)
        {
            $user_account = new User_account;
            $user_account->user_id  = $user->id;
            $user_account->earnings = 0;
            $user_account->balance  = 0;
        }

        $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
        $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
        $user_account->save();

        $old_tiger_account = Tiger_account::latest('id')->lockForUpdate()->first();
        $tiger_account = new Tiger_account;
        $tiger_account->type = 'model';
        $tiger_account->order_id = $order->id;
        $tiger_account->user_id = $user->id;
        $tiger_account->profit = $order->fee;              
        $tiger_account->payment = 0;

        if($old_tiger_account)
        {
            $tiger_account->gross_income = $old_tiger_account->gross_income + $order->fee;
        } else{
            $tiger_account->gross_income = $order->fee;
        }

        $tiger_account->save();
    }
}, 3);
5
Kris Roofe

À mon avis, si vous calculez le revenu brut à la volée pour chaque enregistrement, séparément, vous n'avez même pas besoin de verrouiller la table, vous savez que le verrouillage d'une table ralentira directement votre site Web.

DB::transaction(function () use($order) {
    $model = Model::find($order->product_id);
    $user = $model->user;

    // **update** use_account record
    try {
        $user_account = User_account::find($user->id);
    } catch (Exception $e){
        $user_account = new User_account;
        $user_account->user_id  = $user->id;
        $user_account->earnings = 0;
        $user_account->balance  = 0;
    }
    $user_account->earnings += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->balance += $order->fee * self::USER_COMMISION_RATIO;
    $user_account->save();

    // **create** company_account record
    $tiger_account = Tiger_account::create([
        'type' => 'model',
        'order_id' => $order->id,
        'user_id' => $user->id,
        'profit' => $order->fee,
        'payment' => 0,
    ]);

    $tiger_account->update([
        'gross_income' => Tiger_account::where('id', '<=', $tiger_account->id)->sum('fee'),
    ]);
});
1
spirit