Gestion de la concurrence d’accès aux données en .NET
La plupart des logiciels multi-utilisateurs sont tôt ou tard confrontés à des problèmes liés à la concurrence d’accès aux données : typiquement, deux personnes modifient les informations d’une même commande au même moment. L’une d’elle sauvegarde la première, puis la seconde fait de même. Si aucune politique de gestion de la concurrence n’a été mise en place, les modifications effectuées par la première personne seront perdues et cette personne n’en saura rien. Du moins jusqu’à ce qu’elle consulte la commande à nouveau. Bien entendu, ni elle ni le développeur appelé en renfort ne comprendra pourquoi des modifications auront été perdues. L’utilisateur doutera un peu plus du développeur ; le développeur se méfiera un peu plus de l’utilisateur…
Cela dit, les solutions permettant de gérer la concurrence d’accès sont bien connues et sont assez simples à mettre en œuvre lorsqu’elles sont appliquées assez tôt dans le cycle de développement. En revanche, les appliquer après-coup peut être passablement coûteux puisqu’il faudra retester une bonne partie de l’application. En conséquence, mieux vaut discuter ce sujet avant le début du développement. On préparera une petite librairie (voir plus bas), on la documentera convenablement et les développements pourront commencer.
Cet article décrit les problèmes soulevés par la concurrence d’accès aux données ; il propose une solution et décrit une implémentation possible en .NET.
Définition du problème
Il y a concurrence d’accès aux données lorsque deux ou plusieurs processus accèdent en même temps aux mêmes informations. Si cette concurrence ne pose aucun problème lorsque tous les accès sont effectués en lecture, il en va autrement lorsque l’un des processus, ou plusieurs d’entre eux, tentent de mettre à jour une partie des mêmes données en même temps.
Situations à risque :
- Consultation de données en cours de modification. Le processus A consulte des données qui sont en train d’être mises à jour par le processus B. Est-ce que A doit être prévenu ? Doit-on interdire l’accès aux données désuètes ?
- Modifications simultanées des mêmes données. Le processus A et le processus B modifient les mêmes données en même temps. Quel jeu de modifications doit être conservé ? Faut-il avertir l’un ou l’autre processus ? Faut-il interdire l’action de l’un d’entre eux ? Lequel ?
- Modifications simultanées de données liées. Le processus A modifie des informations qui dépendent des informations en cours de modification par le processus B (typiquement, A calcule l’en-cours alors que B crée des notes). Ce que fait B peut avoir une influence sur ce que calcule A. Faut-il avertir l’un ou l’autre des processus ? Faut-il interdire l’action de l’un d’entre eux ? Lequel ?
Approches
Évidemment, on ne conçoit pas le système embarqué d’un missile balistique comme on conçoit une application de gestion des ressources humaines : dans ce dernier cas, les situations énumérées ci-haut peuvent être ignorées le plus souvent. Voici les approches les plus courantes concernant ces situations :
- L’approche béate : ne rien faire. Certaines applications sont faites de telle sorte qu’il est à peu près impossible que deux processus mettent à jour les mêmes informations en même temps. Dans ce cas, ne rien faire peut être le meilleur choix. TOUTEFOIS, ce cas est plutôt rare concernant les logiciels multi-utilisateurs maison. Généralement, des conflits liés à la concurrence d’accès surviennent après quelques mois ou quelques années d’utilisation : quelqu’un quelque part modifie un processus, un traitement en lot est ajouté au système, … Les raisons sont nombreuses. Inconvénients : L’écrasement de données provoquée par des mises à jour simultanées constitue une anomalie difficile à diagnostiquer. Inversement, l’absence de stratégie de gestion de la concurrence est parfois évoquée pour expliquer des anomalies qui sont dues à d’autres causes. Finalement, une stratégie de gestion de la concurrence est difficile à mettre en place après-coup.
- L’approche optimiste : les cas de collision seront rares. La possibilité d’une collision est faible, on ne pose donc pas de verrou au moment de la lecture des données. Lors de la sauvegarde, toutefois, si on s’aperçoit que les données ont évolué entretemps, on annule cette sauvegarde et on prévient l’utilisateur qu’il devra refaire ses modifications. Inconvénients : un utilisateur peut perdre à tout moment l’ensemble des modifications qu’il vient d’apporter. Les traitements en lot (batch) doivent éventuellement prévoir de retraiter les informations si la sauvegarde n’est pas possible. Finalement, le code effectuant la sauvegarde devra effectuer une vérification (donc un accès à la base) le plus souvent pour rien.
- L’approche pessimiste : les cas de collision seront fréquentes. La possibilité d’une collision est forte, on pose donc un verrou au moment de la lecture des données. Tout autre processus ne pourra lire les mêmes données tant que le verrou persistera (on peut toutefois permettre la lecture malgré la présence du verrou si le processus impliqué ne compte pas modifier l’information – à voir au cas par cas). Inconvénients : Un utilisateur inattentif peut bloquer des informations pendant des heures (voire des jours s’il ne sauvegarde pas ses modifications avant de partir en vacances !). Les traitements en lot devront éventuellement gérer ces verrous soit en patientant, soit en annulant tout ou partie du traitement en cours. Des problèmes de verrous non relâchés sont à prévoir (par exemple, lorsque l’application plante après avoir verrouiller une information).
Solution technique
Dans la plupart des développements maison, les approches 1 et 2 conviennent à tous les cas. La solutions proposée par cet article ignore donc la troisième approche. (Cette troisième approche peut toutefois être mise en œuvre avec une solution similaire mais un peu plus complexe.)
Ne rien faire est souvent la bonne approche car beaucoup de processus prévoit un seul gestionnaire par dossier (par exemple, tous les clients du nord de la France sont affectés à M. Dupont – lui seul est habilité à prendre leur commande ou à modifier leur fiche).
Bien entendu, si le nombre de dossiers est important, M. Dupont sera épaulé par une équipe et n’importe qui dans l’équipe sera susceptible de mettre à jour les informations liées à n’importe quel client. Si le coût de la perte de données consécutive à des mises à jour concurrentes peut être important (par exemple : perte de commandes ou d’informations telles que la cote de solvabilité de clients), une approche optimiste doit être mise en place.
Versionnage
La solution proposée repose sur le versionnage des objets (commandes, clients, secteurs de vente, unités assurables, lignes de fabrication…). Le principe est simple : tout objet possède un numéro de version, lequel est incrémenté chaque fois que l’objet est mis à jour. Typiquement, un traitement qui souhaite modifier un objet lit d’abord cet objet ainsi que son numéro de version courant. Au moment de la sauvegarde, le traitement redemande la version de l’objet : si elle n’a pas évolué, la sauvegarde se fait et le numéro de version est incrémenté. Autrement, la sauvegarde est annulée et l’utilisateur est prévenu.
Simulation :
- Le traitement A lit la commande C1. La version de C1 est 1.
- Le traitement B fait de même. La version de C1 est toujours 1.
- Le traitement B sauvegarde la commande :
- Il demande la dernière version de C1.
- Cette version est toujours 1.
- Il verrouille cette version, sauvegarde C1, incrémente la version qui passe à 2 et la déverrouille.
- Le traitement A sauvegarde à son tour C1 :
- Il demande la version courant de C1 et obtient 2.
- Il s’aperçoit que C1 a évolué entretemps : il annule donc la sauvegarde et prévient l’utilisateur.
Granularité
Le versionnage doit se faire au niveau conceptuel et non au niveau physique des données. Par exemple, il ne doit pas être possible de modifier une commande pendant qu’un autre processus modifie la quantité à livrer d’un article de cette commande. Il faut donc versionner le concept de commande (ce qui inclut son détail) et non simplement un enregistrement de la table Commande.
Pour chaque application, il faudra donc déterminer les entités conceptuelles qui seront versionnées ainsi que les entités physiques correspondantes. Exemple :
| Entité versionnée | Entités physiques correspondantes |
| Commande | Table Commande Table CommandeDetail |
Ainsi, tout traitement souhaitant modifier l’une ou l’autre des tables énumérées ci-dessus devra obtenir la version de la commande correspondante. Le verrou sera toujours posé au niveau de la commande.
Framework
Les composants du framework sont décrits plus bas. Voici comment ils s’utilisent :
1) Exemple de lecture
Public Function GetClient(idClient as Integer) As Client Dim client = New Client() Dim client.Version = GetCurrentObjectVersion(ObjectType.Client, idClient) (Lire le client en base) ' Note : on peut éviter un aller-retour en base en incluant la version dans les informations ' retournées par la procédure stockée lisant la définition d’un client. Dans ce cas, il ' n’est pas nécessaire d’appeler GetCurrentobjectVersion(). Return client End Function
2) Exemple de mise à jour (la suppression étant similaire)
Public Sub SaveClient(myClient as Client) (Debuter une transaction SQL) Try Try If LockCurrentObjectVersion(ObjectType.Client, idClient, myClient.Version) Then (sauvegarder le client) End If Finally UnlockAndIncrementObjectVersion(ObjectType.Client, idClient) EndTry Finally (Fin de la transaction SQL -- Commit ou rollback, selon le cas) End Try End Function
Description du framework
ObjectType
Type énuméré listant tous les types d’informations versionnés (toutes applications confondues). Exemple:
Enum ObjectType Client , Commande , Contrat End Enum
GetCurrentObjectVersion(ByVal type As ObjectType, objectId as String) As Integer
Retourne le numéro de version actuel de l’objet dont l’identifiant et le type sont indiqués.
Implémentation
- Vérifier si l’objet indiqué existe dans la table CurrentObjectVersion.
- S’il existe, retourner la valeur du champ CurrentVersion.
- S’il n’existe pas, créer une entrée, lui assigner la version 1 et retourner cette valeur.
LockCurrentObjectVersion(ByVal type As ObjectType, objectId As String, currentVersion As Integer) As Boolean
Si currentVersion correspond à la version visible en base de l’objet indiqué, cette version est verrouillée et la fonction retourne True. Si la version en base ne correspond pas, c’est qu’un autre traitement à mis à jour cet objet – dans ce cas, la version n’est pas verrouillée et la fonction retourne False.
Cette méthode retourne également False si l’enregistrement est déjà verrouillé par un autre traitement.
Implémentation
- Lire la version de l’objet indiqué en base.
- Si cette version ne correspond pas à currentVersion, retourner False.
- Autrement, verrouiller l’enregistrement de la table CurrentObjectVersion et retourner True.
UnlockAndIncrementObjectVersion(ByVal type As ObjectType, objectId As String)
Incrémente la version de l’objet indiqué puis déverrouille l’enregistrement de la table CurrentObjectVersion correspondant à cet objet.
Une exception est générée si l’enregistrement n’est pas verrouillé au moment de l’appel.
LockRequiredException
Exception générée lorsqu’un traitement appelle UnlockAndIncrementObjectVersion sans avoir appelé
LockCurrentObjectVersionpréalablement.
Données
La gestion des numéros de version se fait via la table CurrentObjectVersion. Cette table possède la structure suivante :
| Champ | Description |
| ObjectType | String. Type de l’objet. Exemple : “Declaration”, “Client”, … |
| ObjectId | Integer. Identifiant unique de l’objet. Exemple : l’id de la déclaration, du client, … |
| CurrentVersion | Integer. Version actuelle de l’objet. Est incrémenté de 1 chaque fois que l’objet est mis à jour. |
| AUDCREE | Date de création de cet enregistrement. |
| AUDMAJ | Date de la dernière modification apportée à cet enregistrement. |
| USERCREE | Identifiant (login) de l’utilisateur responsable de la création de cet enregistrement. |
| USERMAJ | Identifiant (login) de l’utilisateur responsable de la dernière mise à jour apportée à cet enregistrement. |