[JAVA] Transactions, serrures et double facturation

Cet article est le 23e jour du calendrier de l'avent de l'ingénieur CAM 2019. Hier était de @ cotsupa J'ai fait une commande de recherche avec Shell Script.

introduction

Bonjour, c'est @ takehzi. J'ai fait une tâche pour éviter la double facturation dans mon entreprise, donc ce sera un mémorandum.

L'arrière-plan est lorsque l'utilisateur clique deux fois sur le bouton de paiement. Le processus de paiement fonctionnait deux fois, alors je l'ai corrigé. (Le processus de contrôle même du front est mis en œuvre)

Qu'est-ce qu'une transaction?

Je n'avais qu'une compréhension floue des transactions, alors je l'ai d'abord recherchée. En termes simples, une transaction est une unité de travail qui traite les opérations DB consécutives (une ou plusieurs opérations SQL) comme un groupe.

DB exécute uniquement SQL et ne sait pas d'où et où se trouve un ensemble de traitement. Par conséquent, en définissant une transaction (pour DB) de ce côté, le côté DB peut reconnaître qu'il s'agit d'un ensemble et les traiter comme un processus (groupe).

Le résultat de la transaction est ・ S'engager · Retour en arriere Il n'y en a que deux.

Lorsque tout le traitement de la transaction est réussi, émettez un commit pour activer les modifications de toutes les tables pertinentes et mettre fin à la transaction

Si l'une des opérations échoue, effectuez une restauration pour invalider les modifications apportées à toutes les tables pertinentes (auparavant avant d'entrer dans le processus de transaction) et mettez fin à la transaction.

Fermer à clé

Afin d'éviter la double facturation, il est nécessaire d'effectuer un contrôle de verrouillage pour la base de données, Cette fois, j'ai décidé d'utiliser "SELECT ... FOR UPDATE" pour effectuer un verrouillage exclusif (verrouillage pessimiste).

En outre, il y a de fortes chances qu'un autre utilisateur effectue un paiement pendant la double facturation. Assurez-vous que la ligne est verrouillée.

Puisque vous devez indexer (ou conditionner la clé primaire) pour verrouiller la ligne Vous devez également l'indexer à l'avance.

J'essaierai d'utiliser SELECT ... FOR UPDATE.

mysql> select * from user;
+----+--------------+------+
| id | name         | age  |
+----+--------------+------+
|  1 | takehzi      |   25 |
|  2 | komatsunana  |   23 |
|  3 | yoshiokariho |   26 |
+----+--------------+------+
3 rows in set (0.00 sec)

Utilisez SELECT ... FOR UPDATE sur ce tableau pour changer l'âge de Takehzi de 25 à 20 ans.

A)
==================================
//Début de la transaction
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

//id=Verrouiller les lignes lors de la récupération d'un enregistrement.
mysql> select * from user where id=1 for update;
+----+---------+------+
| id | name    | age  |
+----+---------+------+
|  1 | takehzi |   25 |
+----+---------+------+
1 row in set (0.00 sec)

===================================
B)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id=1 for update;
//...Attendre

B attend car A a un verrou de ligne. Ensuite, lorsque les enregistrements A sont mis à jour et validés, le verrou est libéré et l'instruction select de B s'exécute.

================================
A)
//Changer l'âge de 25 à 20 ans
mysql> update user set age="20" where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

//Fin de transaction
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

==============================
B)
mysql> select * from user where id=1 for update;
+----+---------+------+
| id | name    | age  |
+----+---------+------+
|  1 | takehzi |   20 |
+----+---------+------+
1 row in set (9.26 sec)

Le sql de B est exécuté lorsque A s'engage. C'est la valeur que j'ai en B, mais c'est 20 au lieu de 25.

J'ai utilisé ce SELECT ... FOR UPDATE pour contrôler la double facturation.

Problème de double facturation et contre-mesures

Sur la base de ces conditions, nous avons décidé de mettre en place une table de session pour contrôler la double facturation. Il est contrôlé par l'existence ou non d'un trackingId. Le diagramme d'architecture est le suivant. スクリーンショット 2019-12-22 23.29.38.png

Le code réel.

  public void tempInsert(String serviceName, AmazonPayChargeParam param){

    assert StringUtils.isNoneBlank(serviceName);
    assert StringUtils.isNoneBlank(param.getReferenceId());
    assert StringUtils.isNoneBlank(param.getCustCode());
    assert StringUtils.isNoneBlank(param.getItemId());

    try{
      AmazonPaySession session = new AmazonPaySession();
      session.setReferenceId(param.getReferenceId());
      session.setCustCode(param.getCustCode());
      session.setItemId(param.getItemId());
      session.setServiceName(serviceName);

      amazonPaySessionRepository.insert(session);
    }catch (DuplicateKeyException dke){
      //Contrôle les erreurs de contrainte de première place dues à des insertions en double.
      auditLogHelper.loggingParam(dke.getMessage());
    }
  }

Tout d'abord, l'insertion est traitée dans la table de session en tant que données temporaires. (À ce stade, trackingId est nul)

public String authorization(Long apiAuthId, String serviceName, AmazonPayChargeParam param) {

    ChargeRequest chargeRequest = getChargeRequest(param);
    String trackingId = chargeRequest.getChargeReferenceId();

      AuthorizationDetails authDetails = null;
      switch(chargeRequest.getType()) {

//...Abréviation

        case BILLING_AGREEMENT_ID:

          //Verrouillez la ligne ici.TrackingId peut être obtenu du côté dupliqué.
          AmazonPaySession sessionData = amazonPaySessionRepository.tempSelectForUpdate(serviceName, param.getReferenceId(), param.getCustCode(), param.getItemId());

          //Le côté qui se chevauche suit_Obtenez un identifiant et obtenez un retour anticipé.
          if(Objects.nonNull(sessionData.getTrackingId())){
            return sessionData.getTrackingId();
          }

      //Processus de facturation
          authDetails = execBillingAgreement(serviceName, param, chargeRequest, apiAuthId, authDetails, trackingId, sessionData);

//...Abréviation

      //Retourne enfin trackingId
      return trackingId; 
  }

Ici, déterminez la présence ou l'absence de trackingId, S'il y a un trackingId (côté dupliqué), ce trackingId sera retourné tôt.

Comme "il n'y a pas de trackingId = le paiement n'a pas encore été effectué", le processus de paiement est effectué tel quel. Assurez-vous d'ajouter le trackingId émis à la table de session.

De cette façon, nous avons utilisé SELECT ... FOR UPDATE pour contrôler la facturation en double.

À la fin

Je l'ai écrit assez vaguement (je n'ai pas eu le temps de trop le retarder ...) En fait, il y a plus de choses à considérer.

-Quel est le niveau de séparation des transactions (combien de phénomène de lecture est autorisé) -Cette fois, REPEATABLE-READ permet les lectures fantômes. ・ La caractéristique ACID est-elle garantie? (Je pense que l'indépendance dépend de ce que vous faites) ・ La ligne est-elle correctement verrouillée? ・ Y a-t-il un verrou mort?

Il y a encore d'autres points à prendre en compte lors de l'examen du niveau système et du niveau de code, tels que J'ai brièvement résumé la reconnaissance de cette rénovation.

Pour être honnête, ma compréhension est encore ambiguë, alors j'aimerais continuer à étudier dur! Aussi, lorsque j'ai essayé d'écrire un article comme celui-ci cette fois, j'ai réalisé que j'étais moins conscient que ce à quoi je m'attendais, alors Je voudrais faire une telle sortie plus positivement à l'avenir!

Recommended Posts

Transactions, serrures et double facturation
À propos des guillemets simples et doubles Ruby