Cette page explique les transactions dans Spanner et inclut des exemples de code pour exécuter des transactions.
Présentation
Une transaction dans Spanner est un ensemble de lectures et d'écritures qui s'exécutent de manière atomique à un seul moment logique dans les colonnes, les lignes et les tables de une base de données.
Spanner est compatible avec les modes de transaction suivants :
Verrouillage des lectures-écritures. Ces transactions reposent sur un verrouillage pessimiste et, si nécessaire, en deux phases. Le verrouillage des transactions en lecture-écriture peut être annulé, ce qui nécessite une nouvelle tentative d'exécution de l'application.
Lecture seule. Ce type de transaction offre une cohérence garantie pour plusieurs lectures, mais n'autorise pas les écritures. Par défaut, les transactions en lecture seule s'exécutent à un code temporel choisi par le système qui garantit la cohérence externe, mais elles peuvent également être configurées pour lire à un code temporel passé. Lecture seule les transactions n'ont pas besoin d'être validées et n'utilisent pas de verrous. De plus, les transactions en lecture seule peuvent attendre que les écritures en cours soient terminées avant de en cours d'exécution.
LMD partitionné. Ce type de transaction exécute une manipulation de données en tant que LMD partitionné. Le LMD partitionné est conçu pour les mises à jour et les suppressions groupées, notamment un nettoyage et un rembourrage périodiques. Si vous devez engager un grand nombre en écriture aveugle, mais ne nécessitant pas de transaction atomique, vous pouvez modifier vos tables Spanner à l'aide de l'écriture par lot. Pour plus d'informations, consultez la page Modifier des données à l'aide d'écritures par lot.
Cette page décrit les propriétés générales et la sémantique des transactions dans Spanner, et présente les interfaces de transaction en lecture-écriture, en lecture seule et en LMD partitionné dans Spanner.
Transactions en lecture-écriture
Voici des scénarios dans lesquels il est recommandé d'utiliser le verrouillage des transactions en lecture-écriture :
- Si vous effectuez une écriture qui dépend du résultat d'une ou de plusieurs lectures, effectuez cette écriture et la ou les lectures de la même transaction en lecture-écriture.
- Exemple : doubler le solde du compte bancaire A. La lecture du solde de A doit avoir lieu dans la même transaction que l'écriture visant à remplacer le solde par la valeur doublée.
- Si vous effectuez une ou plusieurs écritures qui doivent être validées de manière atomique, faites-le dans la même transaction en lecture-écriture.
- Exemple : transférer 200 $ du compte A au compte B. Les deux écritures (une pour soustraire 200 $ de A, l'autre pour ajouter 200 $ à B) et la lecture du solde initial des comptes doivent correspondre à la même transaction.
- Si vous pouvez effectuer une ou plusieurs écritures, en fonction des résultats d'une ou de plusieurs lectures, effectuez ces écritures et ces lectures dans la même transaction en lecture-écriture, même si les écritures ne sont pas exécutées.
- Exemple : transférer 200 $ du compte en banque A au compte en banque B si le solde actuel de A est supérieur à 500 $. Votre transaction doit contenir une lecture du solde de A et une instruction conditionnelle contenant les écritures.
Voici un scénario dans lequel vous n'avez pas besoin d'utiliser le verrouillage des transactions en lecture-écriture :
- Si vous n'effectuez que des lectures et que vous pouvez les exprimer à l'aide d'une méthode de lecture unique, utilisez cette méthode ou une transaction en lecture seule. Les lectures uniques ne se verrouillent pas, contrairement aux transactions en lecture-écriture.
Propriétés
Une transaction en lecture-écriture dans Spanner exécute un ensemble de lectures et d'écritures de manière atomique à un moment logique unique. En outre, l'horodatage d'exécution des transactions en lecture-écriture correspond à l'heure de l'horloge murale, et l'ordre de sérialisation correspond à l'ordre d'horodatage.
Pourquoi utiliser une transaction en lecture-écriture ? Les transactions en lecture-écriture fournissent les propriétés ACID des bases de données relationnelles. (En fait, les transactions en lecture-écriture de Spanner offrent des garanties encore plus solides que les transactions ACID traditionnelles ; consultez la section Sémantique ci-dessous.)
Isolation
Voici les propriétés d'isolation pour les transactions en lecture-écriture et en lecture seule.
Transactions en lecture et écriture
Voici les propriétés d'isolation que vous obtenez après avoir validé une transaction contenant une série de lectures (ou de requêtes) et d'écritures :
- Toutes les lectures de la transaction ont renvoyé des valeurs qui reflètent un instantané cohérent pris au moment du commit de la transaction.
- Les lignes ou plages vides sont restées vides au moment du commit.
- Toutes les écritures de la transaction ont été validées au moment du commit de la transaction.
- Les écritures n'étaient visibles par aucune transaction avant le commit de la transaction.
Certains pilotes clients Spanner contiennent une logique de nouvelle tentative de transaction pour masquer les erreurs temporaires. Pour ce faire, ils réexécutent la transaction et valident les données observées par le client.
Il en résulte que toutes les lectures et écritures semblent s'être produites à un moment donné, à la fois du point de vue de la transaction elle-même, et d'autres lecteurs et auteurs de la base de données Spanner. En d'autres termes, les opérations de lecture et d'écriture se produisent au même horodatage (une illustration de ce phénomène se trouve dans la section Sérialisabilité et cohérence externe ci-dessous).
Transactions en lecture seule
Les garanties pour une transaction en lecture-écriture qui ne fait qu'effectuer des lectures sont similaires : toutes les lectures au sein de cette transaction renvoient des données du même horodatage, même pour les lignes inexistantes. Une des principales différences entre les deux types de transactions est que, si vous lisez des données, puis validez la transaction en lecture-écriture sans aucune écriture, rien ne garantit que les données n'aient pas été modifiées dans la base de données après la lecture et avant le commit. Si vous voulez savoir si les données ont été modifiées depuis votre dernière lecture, la meilleure approche consiste à les relire (soit dans une transaction en lecture-écriture, soit en utilisant une lecture forte). De même, pour des raisons d'efficacité, si vous savez à l'avance que vous allez seulement lire et pas écrire, vous devez utiliser une transaction en lecture seule au lieu d'une transaction en lecture-écriture.
Atomicité, cohérence, durabilité
En plus de la propriété d'isolation, Spanner fournit des propriétés d'atomicité (si l'une des écritures dans la transaction est validée, elles le sont toutes), de cohérence (la base de données reste dans un état cohérent après la transaction) et de durabilité (les données validées restent validées).
Avantages de ces propriétés
Ces propriétés aident les développeurs d'applications à se concentrer sur l'exactitude de chaque transaction, sans se soucier de la protection de son exécution par rapport à d'autres transactions pouvant être exécutées simultanément.
Interface
Les bibliothèques clientes Spanner fournissent une interface pour l'exécution de tâches dans le contexte d'une transaction en lecture/écriture, avec la possibilité de nouvelles tentatives d'exécution en cas d'échec de la transaction. Voici un peu de contexte pour expliquer ce point : une transaction Spanner devra sans doute être testée plusieurs fois avant d'être validée. Par exemple, si deux transactions tentent de manipuler des données en même temps et éventuellement créent un blocage, Spanner abandonne l'une d'entre elles afin que l'autre puisse progresser. Plus rarement, des événements temporaires dans Spanner entraîner l'annulation de certaines transactions.) Comme les transactions sont atomiques, une transaction annulée n'a aucun effet visible sur la base de données. Par conséquent, les transactions doivent être réexécutées jusqu'à ce qu'elles aboutissent.
Lorsque vous utilisez une transaction dans une bibliothèque cliente Spanner, vous définissez corps d'une transaction (c'est-à-dire les lectures et écritures à effectuer sur un ou plusieurs tableaux d'une base de données) sous la forme d'un objet fonction. En arrière-plan, la bibliothèque cliente Spanner exécute la fonction à plusieurs reprises jusqu'au commit de la transaction ou l'apparition d'une erreur pour laquelle les nouvelles tentatives ne sont pas possibles.
Exemple
Supposons que vous avez ajouté une colonne MarketingBudget
à la table Albums
affichée sur la page "Schéma et modèle de données" :
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
Votre service marketing décide de lancer une campagne marketing pour l'album correspondant à Albums (1, 1)
et vous a demandé de transférer 200 000 $ du budget de Albums
(2, 2)
, mais uniquement si la somme est disponible dans le budget de cet album. Vous devez utiliser le verrouillage des transactions en lecture-écriture pour cette opération, car la transaction peut effectuer des écritures en fonction du résultat de la lecture.
L'exemple suivant montre comment exécuter une transaction en lecture-écriture :
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Sémantique
Sérialisabilité et cohérence externe
Spanner offre une "sérialisabilité", ce qui signifie que toutes les transactions apparaissent comme s'ils s'exécutaient dans un ordre sérialisé, même si une partie des lectures, écritures, et d'autres opérations de transactions distinctes ont eu lieu en parallèle. Spanner attribue des horodatages de commit reflétant l'ordre des validations transactions pour implémenter cette propriété. En fait, Spanner offre plus forte que la sérialisabilité, appelée cohérence externe: les transactions sont validées. une commande qui est reflétée dans leurs horodatages de commit, Les codes temporels indiquent en temps réel que vous pouvez les comparer à votre montre. Les lectures effectuées au sein d'une transaction voient tout ce qui a été validé avant le commit de la transaction et les écritures sont vues par tout ce qui commence après le commit de la transaction.
Par exemple, considérons l'exécution de deux transactions comme illustré dans le diagramme ci-dessous :
La transaction Txn1
en bleu lit des données A
, met en mémoire tampon une écriture dans A
, puis est validée. La transaction Txn2
en vert commence après Txn1
, lit des données B
, puis lit les données A
. Comme Txn2
lit la valeur de A
après que Txn1
a effectué un commit de son écriture dans A
, Txn2
voit les effets de l'écriture de Txn1
dans A
, même si Txn2
a démarré avant que Txn1
n'ait abouti.
Même si Txn1
et Txn2
se chevauchent dans le temps, leurs horodatages de commit c1
et c2
respectent un ordre de transaction linéaire, ce qui signifie que tous les effets des lectures et écritures de Txn1
semblent s'être produits à un seul moment donné (c1
), et, de la même manière, tous les effets des lectures et des écritures de Txn2
semblent s'être produits à un seul moment donné (c2
). De plus, c1 < c2
(garanti, car Txn1
et Txn2
ont toutes les deux effectué le commit de leur écriture ; c'est le cas même si les écritures ont eu lieu sur des machines différentes), ce qui respecte l'ordre d'exécution de Txn1
avant Txn2
.
Cependant, si Txn2
n'a exécuté que des lectures dans la transaction, alors c1 <= c2
.
Les lectures observent un préfixe de l'historique de commit ; si une lecture voit les effets de Txn2
, elle voit aussi ceux de Txn1
. Toutes les transactions validées comportent cette propriété.
Garanties liées aux lectures et aux écritures
Si un appel à exécuter une transaction échoue, les garanties en lecture et en écriture dépendent de l'erreur qui a entraîné l'échec de l'appel de commit sous-jacent.
Par exemple, une erreur telle que "Ligne non trouvée" ou "Ligne déjà existante" signifie que l'écriture des mutations mises en mémoire tampon a rencontré une erreur, par exemple une des lignes que le client tente de mettre à jour n'existe pas. Dans ce cas, les lectures sont garanties cohérentes, les écritures ne sont pas appliquées et la non-existence de la ligne est également garantie cohérente avec les lectures.
Annuler des opérations de transaction
L'utilisateur peut annuler à tout moment les opérations de lecture asynchrone (par exemple, lorsqu'une opération de niveau supérieur est annulée ou si vous décidez d'arrêter une lecture en fonction des résultats initiaux reçus de la lecture) sans affecter les autres opérations existantes dans la transaction.
Toutefois, même si vous essayez d'annuler la lecture, Spanner n'effectue pas garantir que la lecture est effectivement annulée. Une fois que vous avez demandé l'annulation d'une lecture, celle-ci peut quand même aboutir ou échouer pour une autre raison (par exemple, en cas d'annulation). En outre, cette lecture annulée peut en réalité renvoyer des résultats ; ces résultats, éventuellement incomplets, seront validés dans le cadre du commit de la transaction.
Notez que, contrairement aux lectures, l'annulation d'une opération de commit entraînera l'abandon de la transaction (sauf si la transaction a déjà été validée ou a échoué pour une autre raison).
Performances
Verrouillage
Spanner permet à plusieurs clients d'interagir simultanément avec le même base de données. Afin de garantir la cohérence de plusieurs transactions simultanées, Spanner utilise une combinaison de verrous partagés et de verrous exclusifs pour contrôler l'accès aux données. Lorsque vous effectuez une lecture dans le cadre transaction, Spanner acquiert des verrous de lecture partagés, ce qui permet à d'autres en lecture seule pour accéder aux données jusqu'à ce que la transaction soit prête à être validée. Une fois la transaction validée et les écritures appliquées, la transaction tente de passer à un verrou exclusif. Elle bloque les nouveaux verrous en lecture partagés sur les données, attend que les verrous en lecture partagés existants soient annulés, puis place un verrou exclusif pour un accès exclusif aux données.
Notes au sujet des verrous :
- Les verrous sont placés de façon granulaire au niveau des lignes et des colonnes. Si la transaction T1 a verrouillé la colonne "A" de la ligne "foo", et que la transaction T2 souhaite écrire dans la colonne "B" de la ligne "foo", il n'y a pas de conflit.
- Les écritures sur un élément de données qui n'effectuent pas la lecture des données en cours d'écriture ("écriture cachée") n'entrent pas en conflit avec d'autres auteurs cachés du même élément (l'horodatage de commit de chaque écriture détermine l'ordre dans lequel elle est appliquée dans la base de données). En conséquence, Spanner n'a besoin de mettre à niveau à un verrou exclusif si vous avez lu les données que vous écrivez. Sinon, Spanner utilise un verrou partagé appelé "verrou partagé de rédacteur".
- Lorsque vous effectuez des recherches sur des lignes au sein d'une transaction en lecture/écriture, utilisez des index secondaires pour limiter le nombre de lignes analysées à une plage plus petite. Spanner verrouille donc moins de nombres de lignes, ce qui permet de modifier simultanément les lignes situées en dehors de la plage d'adresses IP.
Les verrous ne doivent pas être utilisés pour garantir un accès exclusif à une ressource située en dehors Spanner. Les transactions peuvent être annulées pour différentes raisons par Spanner, par exemple pour autoriser le transfert de données entre les ressources de calcul de l'instance. Si une transaction est relancée, que ce soit explicitement par un code d'application ou implicitement par un code client tel que le pilote JDBC Spanner, la seule garantie est le maintien des verrous pendant la tentative réellement effectuée.
L'outil d'introspection Verrouiller les statistiques vous permet d'examiner les conflits de verrouillage dans votre base de données.
Détection des blocages
Spanner détecte l'interblocage de plusieurs transactions.
Force l'annulation de toutes les transactions, sauf une. Par exemple, considérons le scénario suivant : la transaction Txn1
maintient un verrou sur l'enregistrement A
et attend un verrou sur l'enregistrement B
, tandis que Txn2
maintient un verrou sur l'enregistrement B
et attend un verrou sur l'enregistrement A
. Le seul moyen d'avancer dans cette situation consiste à annuler l'une des transactions pour qu'elle procède au déverrouillage, permettant ainsi à l'autre transaction de progresser.
Spanner utilise la méthode standard "wound-wait" pour gérer les interblocages la détection. En coulisses, Spanner garde une trace de l'âge de chaque qui demande des verrous en conflit. et permet également aux transactions plus anciennes d'abandonner les transactions plus récentes ("ancien" désignant la lecture, la requête ou le commit de transaction ayant commencé plus tôt).
En donnant la priorité aux transactions plus anciennes, Spanner garantit que chaque transaction a une chance d'acquérir des verrous, une fois obsolètes pour avoir une priorité plus élevée que les autres transactions. Par exemple, une transaction qui obtient un verrou partagé pour le lecteur peut être annulée par une transaction plus ancienne nécessitant un verrou partagé pour l'auteur.
Exécution distribuée
Spanner peut exécuter des transactions sur les données couvrant plusieurs serveurs. Cette puissance a un coût en termes de performances comparé aux transactions sur un seul serveur.
Quels types de transactions peuvent être distribués ? En arrière-plan, Spanner peut répartir la responsabilité des lignes de la base de données sur plusieurs serveurs. Une ligne et les lignes correspondantes dans les tables entrelacées sont généralement diffusées par le même serveur, comme le sont deux lignes d'une même table ayant des clés proches. Spanner peut effectuer des transactions entre les lignes sur des serveurs différents ; Toutefois, en règle générale, les transactions qui affectent de nombreuses lignes colocalisées sont plus rapides et moins chères Des transactions qui affectent de nombreuses lignes dispersées dans la base de données, ou dans une grande table.
Les transactions les plus efficaces dans Spanner n'incluent que les lectures les écritures qui doivent être appliquées de manière atomique. Les transactions sont plus rapides lorsque toutes les lectures et écritures accèdent aux données dans une même partie de l'espace clé.
Transactions en lecture seule
En plus de verrouiller les transactions en lecture-écriture, Spanner offre les transactions en lecture seule.
Utilisez une transaction en lecture seule lorsque vous devez exécuter plusieurs lectures au même horodatage. Si vous pouvez exprimer votre lecture à l'aide de l'une des méthodes de lecture unique de Spanner, utilisez plutôt cette méthode. Les performances liées à l'utilisation d'un appel en lecture unique devraient être comparables à celles d'une lecture unique effectuée dans une transaction en lecture seule.
Si vous lisez une grande quantité de données, envisagez d'utiliser des partitions pour lire les données en parallèle.
Parce que les transactions en lecture seule n'effectuent aucune écriture, elles ne peuvent ni être verrouillées, ni bloquer les autres transactions. Les transactions en lecture seule observent un préfixe cohérent de l'historique de commit des transactions. De la sorte, votre application obtient toujours des données cohérentes.
Propriétés
Une transaction Spanner en lecture seule exécute un ensemble de lectures au cours d'une à un moment précis, du point de vue de la transaction en lecture seule du point de vue des autres lecteurs et écrivains, Spanner. Ainsi, les transactions en lecture seule observent toujours un état cohérent de la base de données à un moment donné de l'historique des transactions.
Interface
Spanner fournit une interface pour l'exécution de tâches dans le contexte d'une transaction en lecture seule, avec la possibilité de nouvelles tentatives d'exécution en cas d'échec de la transaction.
Exemple
Le code qui suit montre comment utiliser une transaction en lecture seule afin d'obtenir des données cohérentes pour deux lectures au même horodatage :
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Transactions à LMD partitionné
À l'aide du langage de manipulation de données partitionné (LMD partitionné), vous pouvez exécuter des instructions UPDATE
et DELETE
à grande échelle sans vous heurter à des limites de transaction ni verrouiller une table entière.
Spanner partitionne l'espace clé et exécute les instructions LMD sur chaque
dans une transaction en lecture/écriture distincte.
Les instructions LMD s'exécutent dans des transactions en lecture-écriture que vous créez explicitement dans votre code. Pour plus d'informations, consultez la section Utiliser LMD.
Propriétés
Vous ne pouvez exécuter qu'une seule instruction en mode LMD partitionné à la fois, que vous utilisiez une méthode de bibliothèque cliente ou la CLI Google Cloud.
Les transactions partitionnées ne sont pas compatibles avec les commits ou les restaurations. Spanner s'exécute et applique immédiatement l'instruction LMD. Si vous annulez l'opération, ou si l'opération échoue, Spanner annule toutes les opérations et ne démarre aucune des partitions restantes. Spanner n'effectue aucun rollback des partitions déjà exécutées.
Interface
Spanner fournit une interface pour l'exécution d'un seul LMD partitionné .
Examples
L'exemple de code suivant met à jour la colonne MarketingBudget
de la table Albums
.
C++
Vous utilisez la fonction ExecutePartitionedDml()
pour exécuter une instruction DML partitionnée.
C#
Utilisez la méthode ExecutePartitionedUpdateAsync()
pour exécuter une instruction en mode LMD partitionné.
Go
Utilisez la méthode PartitionedUpdate()
pour exécuter une instruction en mode LMD partitionné.
Java
Utilisez la méthode executePartitionedUpdate()
pour exécuter une instruction en mode LMD partitionné.
Node.js
Utilisez la méthode runPartitionedUpdate()
pour exécuter une instruction en mode LMD partitionné.
PHP
Utilisez la méthode executePartitionedUpdate()
pour exécuter une instruction en mode LMD partitionné.
Python
Utilisez la méthode execute_partitioned_dml()
pour exécuter une instruction en mode LMD partitionné.
Ruby
Utilisez la méthode execute_partitioned_update()
pour exécuter une instruction en mode LMD partitionné.
L'exemple de code suivant supprime les lignes de la table Singers
, en fonction de la colonne SingerId
.