Dans les architectures distribuées modernes, la gestion des transactions devient un défi majeur. Ce document explore les principes des transactions distribuées, examine les solutions courantes, et détaille l'utilisation de Seata en mode AT (Atomic Transaction) pour résoudre ces problématiques.
- Comprendre les Transactions en Environnement Distribué
1.1. Les Fondamentaux des Transactions
Une transaction idéale adhère aux propriétés ACID :
- Atomicité (Atomicity) : Toutes les opérations d'une transaction forment une unité indivisible. Soit toutes réussissent, soit aucune n'a lieu. En cas d'échec, le système doit revenir à son état initial.
- Cohérence (Consistency) : Une transaction valide assure que le système passe d'un état cohérent à un autre, respectant toutes les contraintes d'intégrité définies.
- Isolation (Isolation) : L'exécution concurrente de multiples transactions ne doit pas les interférer mutuellement. Chaque transaction doit sembler s'exécuter de manière séquentielle, indépendamment des autres.
- Durabilité (Durability) : Une fois qu'une transaction est validée, ses effets sont permanents et survivent aux pannes du système.
1.2. Le Défi des Transactions Distribuées
Dans un système distribué, les opérations peuvent s'étendre sur plusieurs bases de données ou microservices distincts. Atteindre les propriétés ACID dans un tel contexte est complexe. Par exemple, une requête utilisateur pour créer une commande peut nécessiter l'appel d'un service de commande (base de données A) puis d'un service de gestion de stock (base de données B). Si le service de commande réussit mais que la déduction du stock échoue, une incohérence des données survient, car la commande a été enregistrée sans que le stock ne soit mis à jour.
1.3. Approhces Courantes pour la Gestion des Transactions Distribuées
1.3.1. Protocole de Validation en Deux Phases (2PC / XA)
Le 2PC est une implémentation standard de XA, divisant la transaction en deux étapes orchestrées par un coordinateur de transactions :
- Phase de Préparation (Prepare) : Chaque participant à la transaction exécute ses opérations locales mais ne les valide pas définitivement. Il signale au coordinateur s'il est prêt à valider.
- Phase de Validation/Annulation (Commit/Rollback) : Si tous les participants ont signalé leur préparation réussie, le coordinateur envoie une instruction de validation (commit) à tous. Si un participant échoue ou refuse, le coordinateur envoie une instruction d'annulation (rollback) à tous.
Inconvénients :
- Performance : Les ressources sont verrouillées pendant toute la durée de la transaction distribuée, ce qui peut entraîner des goulots d'étranglement.
- Point de Défaillance Unique : Si le coordinateur tombe en panne après avoir envoyé une instruction de validation à certains participants mais pas à d'autres, une incohérence peut survenir.
1.3.2. Protocole de Validation en Trois Phases (3PC)
Le 3PC tente d'améliorer le 2PC en ajoutant une phase de pré-validation (CanCommit) :
- Phase de Vote (CanCommit) : Le coordinateur interroge les participants pour s'assurer qu'ils peuvent exécuter l'opération.
- Phase de Pré-validation (PreCommit) : Si le vote est positif, les participants préparent les ressources.
- Phase de Validation (DoCommit) : Les participants valident la transaction.
Avantages et Inconvénients : Le 3PC atténue légèrement le problème du point de défaillance unique du coordinateur (en cas de timeout, les participants peuvent décider de valider), mais il n'élimine pas complètement les incohérences et introduit une latence supplémentaire.
1.3.3. TCC (Try, Confirm, Cancel)
TCC est un modèle de transaction compensatoire qui exige que chaque opération métier dispose de trois étapes distinctes :
- Try : Tente de réserver les ressources nécessaires et de vérifier les conditions préalables.
- Confirm : Confirme l'opération si toutes les étapes Try ont réussi.
- Cancel : Annule les opérations et libère les ressources si une étape Try échoue.
Inconvénients : Le TCC implique une forte intrusion dans le code métier, car chaque opération doit être instrumantée avec ces trois phases, augmentant considérablement la complexité de développement.
1.3.4. Table de Messages Locaux / Sagas
Cette approche utilise une table de messages dans la base de données locale du service initiateur. L'opération métier et l'enregistrement du message sont effectués dans une seule transaction locale. Un processus en arrière-plan envoie ensuite ce message à un système de messagerie (MQ). Le service consommateur traite le message, et en cas de succès, informe le service initiateur pour marquer le message comme traité. En cas d'échec, des mécanismes de retransmission ou de compensation sont mis en œuvre. C'est une forme de transaction par saga, privilégiant la cohérence éventuelle.
1.3.5. Transactions AT (Atomic Transaction) avec Seata
Le mode AT de Seata, proposé par Alibaba, est une amélioration du protocole 2PC traditionnel, optimisée pour les microservices. Il intercepte automatiquement les opérations SQL et crée des instantanés des données avant et après modification. Ces instantanés sont utilisés pour la rétroaction (rollback) en cas d'échec global. L'avantage majeur est que chaque transaction locale peut être validée immédiatement, libérant ainsi les verrous et améliorant significativement le débit du système par rapport au 2PC classique.
- Introduction à Seata
Seata (Simple Extensible Autonomous Transaction Architecture) est une solution open source d'Alibaba pour la gestion des transactions distribuées dans les architectures de microservices. Elle offre plusieurs modes de transaction, dont le mode AT est le plus couramment utilisé en raison de sa facilité d'intégration et de ses performances.
Pour plus d'informations, consultez le site officiel de Seata ou le dépôt GitHub.
- Installation et Configuration du Serveur Seata
Avant de commencer, assurez-vous que JDK 8 ou supérieur est installé sur votre machine.
3.1. Téléchargement du Serveur Seata
Vous pouvez télécharger le package du serveur Seata depuis la page de téléchargement officielle ou directement depuis les releases GitHub (par exemple, seata-server-1.5.2.tar.gz).
Une fois téléchargé, décompressez l'archive :
tar -zxvf seata-server-1.5.2.tar.gz -C /opt/seata
3.2. Intégration avec Nacos (Centre de Configuration et d'Enregistrement)
Pour que Seata Server soit gérable et découvrable, il est recommandé de l'enregistrer auprès de Nacos. Assurez-vous que Nacos est déjà opérationnel.
1. Création d'un Namespace Nacos : Créez un namespace dédié à Seata (par exemple, seata-namespace) dans la console Nacos.
2. Configuration de Seata dans Nacos : Importez ou créez un fichier de configuration pour Seata Server dans le namespace seata-namespace, avec un Data ID comme seata-server et un Group comme SEATA_GROUP.
Le contenu de base peut être trouvé dans le dossier script/config-center/config.txt de l'archive Seata. Voici un exemple des modifications à apporter à conf/application.yml pour l'intégration Nacos :
seata:
config:
type: nacos # Utiliser Nacos comme centre de configuration
nacos:
server-addr: 192.168.1.100:8848 # Adresse de votre serveur Nacos
namespace: seata-namespace # Le namespace créé pour Seata
group: SEATA_GROUP
username: nacos
password: nacos
data-id: seata-server
registry:
type: nacos # Enregistrer Seata comme service dans Nacos
nacos:
application: seata-transaction-coordinator # Nom du service Seata
server-addr: 192.168.1.100:8848
group: SEATA_GROUP
namespace: seata-namespace
cluster: default
username: nacos
password: nacos
3.3. Initialisation de la Base de Données Seata
Seata nécessite une base de données pour stocker les logs de transaction (undo_log et branch_table, global_table). Créez une base de données nommée seata_server_db (ou similaire) et exécutez les scripts SQL fournis. Ces scripts se trouvent généralement dans script/client/mysql.sql ou script/server/db/mysql.sql dans l'archive Seata.
-- Table de logs pour le mode AT
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'ID de la branche de transaction',
`xid` VARCHAR(128) NOT NULL COMMENT 'ID global de la transaction',
`context` VARCHAR(128) NOT NULL COMMENT 'Contexte du undo_log',
`rollback_info` LONGBLOB NOT NULL COMMENT 'Informations de rollback',
`log_status` INT(11) NOT NULL COMMENT 'Statut du log : 0:normal, 1:défense',
`log_created` DATETIME(6) NOT NULL COMMENT 'Date de création',
`log_modified` DATETIME(6) NOT NULL COMMENT 'Date de modification',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='Table undo pour le mode de transaction AT';
-- Tables pour le coordinateur de transaction (Seata Server)
-- Global transaction table
CREATE TABLE `global_table` (
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT DEFAULT NULL,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32) DEFAULT NULL,
`transaction_service_group` VARCHAR(32) DEFAULT NULL,
`transaction_name` VARCHAR(128) DEFAULT NULL,
`timeout` INT DEFAULT NULL,
`begin_time` BIGINT DEFAULT NULL,
`application_data` VARCHAR(2000) DEFAULT NULL,
`gmt_create` DATETIME(6) DEFAULT NULL,
`gmt_modified` DATETIME(6) DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Branch transaction table
CREATE TABLE `branch_table` (
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT DEFAULT NULL,
`resource_group_id` VARCHAR(32) DEFAULT NULL,
`resource_id` VARCHAR(256) DEFAULT NULL,
`branch_type` VARCHAR(8) DEFAULT NULL,
`status` TINYINT DEFAULT NULL,
`client_id` VARCHAR(64) DEFAULT NULL,
`application_data` VARCHAR(2000) DEFAULT NULL,
`gmt_create` DATETIME(6) DEFAULT NULL,
`gmt_modified` DATETIME(6) DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.4. Démarrage du Serveur Seata
Assurez-vous que Nacos est en cours d'exécution. Ensuite, lancez le serveur Seata :
./bin/seata-server.sh -h VOTRE_IP_LOCALE
Vérifiez l'enregistrement du service seata-transaction-coordinator dans Nacos et accédez à l'interface d'administration de Seata via http://VOTRE_IP_LOCALE:7091 (identifiants par défaut : seata/seata).
- Implémentation Client avec Seata AT
Nous allons illustrer l'utilisation de Seata AT avec deux microservices Spring Boot : un service "Order" (Consommateur) et un service "Inventory" (Fournisseur).
4.1. Structure de Base des Bases de Données
Créez deux bases de données distinctes, par exemple order_db et inventory_db. Dans chacune, ajoutez la table undo_log (voir section 3.3) ainsi qu'une table métier simple, par exemple customer_order et product_stock :
-- Pour order_db
CREATE TABLE `customer_order` (
`id` INT NOT NULL AUTO_INCREMENT,
`customer_name` VARCHAR(100) DEFAULT NULL,
`order_amount` DECIMAL(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Pour inventory_db
CREATE TABLE `product_stock` (
`product_id` INT NOT NULL,
`product_name` VARCHAR(100) DEFAULT NULL,
`quantity` INT DEFAULT NULL,
PRIMARY KEY (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2. Service "Order" (Consommateur)
4.2.1. Dépendances Maven (pom.xml)
<dependencies>
<!-- Spring Cloud Alibaba Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version> <!-- Utilisation d'une version plus récente -->
</dependency>
<!-- Spring Web et Feign -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
4.2.2. Configuration (application.yml)
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # Nouveau driver pour MySQL 8+
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: votre_mot_de_passe
seata:
application-id: ${spring.application.name}
client:
rm:
async-commit-buffer-limit: 10000
tm:
commit-retry-count: 3
rollback-retry-count: 3
config:
type: nacos
nacos:
server-addr: 192.168.1.100:8848
namespace: seata-namespace
group: SEATA_GROUP
data-id: seata-server
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: ${spring.application.name}
server-addr: 192.168.1.100:8848
namespace: seata-namespace
cluster: default
username: nacos
password: nacos
4.2.3. Classe d'Application Principale
package com.example.orderservice;
import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableAutoDataSourceProxy // Active le proxy de source de données Seata
@MapperScan("com.example.orderservice.mapper")
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
4.2.4. Entité et Mappers
package com.example.orderservice.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; // Nécessite Lombok
import java.math.BigDecimal;
@Data
@TableName("customer_order")
public class CustomerOrder {
@TableId(type = IdType.AUTO)
private Integer id;
private String customerName;
private BigDecimal orderAmount;
}
package com.example.orderservice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.orderservice.entity.CustomerOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CustomerOrderMapper extends BaseMapper<CustomerOrder> {
}
4.2.5. Service d'Appel (Feign Client)
package com.example.orderservice.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "inventory-service") // Nom du service fournisseur
public interface InventoryServiceClient {
@PostMapping("/inventory/deductStock")
String deductStock(@RequestParam("productId") Integer productId, @RequestParam("quantity") Integer quantity);
}
4.2.6. Service Métier
package com.example.orderservice.service;
import com.example.orderservice.entity.CustomerOrder;
import com.example.orderservice.mapper.CustomerOrderMapper;
import com.example.orderservice.feign.InventoryServiceClient;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class OrderProcessingService {
@Autowired
private CustomerOrderMapper orderMapper;
@Autowired
private InventoryServiceClient inventoryClient;
@GlobalTransactional(rollbackFor = Exception.class) // Marque la méthode comme transaction globale Seata
public String createOrderAndDeductStock(String customerName, Integer productId, Integer quantity, BigDecimal orderAmount) {
// 1. Créer la commande
CustomerOrder newOrder = new CustomerOrder();
newOrder.setCustomerName(customerName);
newOrder.setOrderAmount(orderAmount);
orderMapper.insert(newOrder); // Insère dans order_db
// 2. Déduire le stock via le service Inventory (appel Feign)
String inventoryResult = inventoryClient.deductStock(productId, quantity);
// 3. Provoquer une erreur pour tester le rollback
if (customerName.contains("ERROR")) {
throw new RuntimeException("Erreur simulée pour le client 'ERROR'");
}
return "Order created and stock deducted successfully: " + inventoryResult;
}
}
4.2.7. Contrôleur REST
package com.example.orderservice.controller;
import com.example.orderservice.service.OrderProcessingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderProcessingService orderProcessingService;
@PostMapping("/create")
public String placeOrder(
@RequestParam("customerName") String customerName,
@RequestParam("productId") Integer productId,
@RequestParam("quantity") Integer quantity,
@RequestParam("orderAmount") BigDecimal orderAmount) {
try {
return orderProcessingService.createOrderAndDeductStock(customerName, productId, quantity, orderAmount);
} catch (Exception e) {
return "Failed to place order: " + e.getMessage();
}
}
}
4.3. Service "Inventory" (Fournisseur)
4.3.1. Dépendances Maven (pom.xml)
Similaire au service "Order", mais sans spring-cloud-starter-openfeign.
<dependencies>
<!-- Spring Cloud Alibaba Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
4.3.2. Configuration (application.yml)
spring:
application:
name: inventory-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/inventory_db?useSSL=false&serverTimezone=UTC
username: root
password: votre_mot_de_passe
seata:
application-id: ${spring.application.name}
client:
rm:
async-commit-buffer-limit: 10000
tm:
commit-retry-count: 3
rollback-retry-count: 3
config:
type: nacos
nacos:
server-addr: 192.168.1.100:8848
namespace: seata-namespace
group: SEATA_GROUP
data-id: seata-server
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: ${spring.application.name}
server-addr: 192.168.1.100:8848
namespace: seata-namespace
cluster: default
username: nacos
password: nacos
4.3.3. Classe d'Application Principale
package com.example.inventoryservice;
import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableAutoDataSourceProxy
@MapperScan("com.example.inventoryservice.mapper")
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
}
4.3.4. Entité et Mappers
package com.example.inventoryservice.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("product_stock")
public class ProductStock {
@TableId(type = IdType.INPUT) // Id fourni par le client
private Integer productId;
private String productName;
private Integer quantity;
}
package com.example.inventoryservice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.inventoryservice.entity.ProductStock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface ProductStockMapper extends BaseMapper<ProductStock> {
@Update("UPDATE product_stock SET quantity = quantity - #{quantity} WHERE product_id = #{productId} AND quantity >= #{quantity}")
int deductStock(Integer productId, Integer quantity);
}
4.3.5. Service Métier
package com.example.inventoryservice.service;
import com.example.inventoryservice.mapper.ProductStockMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // Annotation de transaction locale
@Service
public class InventoryManagementService {
@Autowired
private ProductStockMapper productStockMapper;
@Transactional // Transaction locale pour la déduction de stock
public boolean deductProductStock(Integer productId, Integer quantity) {
int updatedRows = productStockMapper.deductStock(productId, quantity);
if (updatedRows == 0) {
// Insuffisance de stock ou produit introuvable
throw new RuntimeException("Stock insuffisant ou produit introuvable pour le produit " + productId);
}
return updatedRows > 0;
}
}
4.3.6. Contrôleur REST
package com.example.inventoryservice.controller;
import com.example.inventoryservice.service.InventoryManagementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/inventory")
public class InventoryController {
@Autowired
private InventoryManagementService inventoryService;
@PostMapping("/deductStock")
public String deductStock(@RequestParam("productId") Integer productId, @RequestParam("quantity") Integer quantity) {
try {
boolean success = inventoryService.deductProductStock(productId, quantity);
return success ? "Stock déduit avec succès." : "Échec de la déduction de stock.";
} catch (Exception e) {
return "Erreur lors de la déduction du stock: " + e.getMessage();
}
}
}
4.4. Vérification des Résultats
Pour tester, assurez-vous que Nacos et Seata Server sont démarrés, puis lancez les services "Order" et "Inventory".
Scenario 1: Transaction réussie (sans erreur simulée)
Appel : POST http://localhost:8080/orders/create?customerName=Alice&productId=1&quantity=2&orderAmount=100.00
Résultat attendu : La commande est créée dans order_db, et le stock est déduit dans inventory_db. La table undo_log des deux bases sera vide après la validation réussie.
Scenario 2: Transaction échouée (avec erreur simulée)
Appel : POST http://localhost:8080/orders/create?customerName=ERROR_TEST&productId=1&quantity=2&orderAmount=100.00
Résultat attendu : Bien que le service "Order" ait initialement inséré la commande et que le service "Inventory" ait déduit le stock, l'erreur simulée dans OrderProcessingService entraînera l'annulation complète de la transaction globale. La commande sera effacée de order_db et le stock sera restauré dans inventory_db. La table undo_log des deux bases montrera des enregistrements temporaires lors de l'exécution, qui seront supprimés après le rollback.
- Principe de Fonctionnement du Mode AT de Seata
Le mode AT de Seata s'appuie sur une approche d'optimisation du 2PC, axée sur la libération rapide des ressources transactionnelles locales :
- Phase 1 (Validation Locale Immédiate) : Lorsqu'une transaction locale est exécutée par un participant (RM - Resource Manager), le proxy de source de données Seata intercepte l'opération SQL. Il capture les images "avant" et "après" des données modifiées et les stocke dans la table locale
undo_log. Ensuite, la transaction locale est immédiatement validée, ce qui libère rapidement les verrous sur la base de données. - Phase 2 (Décision Globale) :
- Validation Globale : Si toutes les branches de la transaction globale signalent leur succès au Transaction Coordinator (TC), le TC demande à chaque RM de nettoyer les enregistrements
undo_logcorrespondants. - Annulation Globale : Si une branche de la transaction globale échoue, le TC envoie une commande d'annulation à tous les RM. Chaque RM utilise les images "avant" stockées dans la table
undo_logpour annuler les modifications des données, puis nettoie ces enregistrements.
- Validation Globale : Si toutes les branches de la transaction globale signalent leur succès au Transaction Coordinator (TC), le TC demande à chaque RM de nettoyer les enregistrements
Ce mécanisme permet d'assurer l'atomicité de la transaction distribuée tout en minimisant l'impact sur la concurrence et la performance des bases de données locales, car les verrous ne sont maintenus que pendant la durée des transactions locales.