Introduction
In distributed systems, ensuring data consistency across multiple services can be quite challenging. The Three-Phase Commit (3PC) protocol is an enhancement of the Two-Phase Commit (2PC) protocol, designed to reduce the possibility of blocking situations during transaction commitment in a distributed environment. This protocol can be particularly useful in a microservices architecture to maintain consistency across services.
What is a Three-Phase Commit?
Three-Phase Commit is a distributed algorithm that ensures all participants in a transaction either commit or abort the transaction, achieving consensus without blocking in the event of certain failures. The protocol introduces an additional phase compared to 2PC, adding a "pre-commit" phase, which helps in further reducing the chances of blocking.
The protocol consists of three phases.
- CanCommit Phase: The coordinator sends a canCommit request to all participants to check if they can commit the transaction.
- PreCommit Phase: If all participants respond positively, the coordinator sends a pre-commit request to all participants to prepare for committing.
- DoCommit Phase: If all participants acknowledge the preCommit, the coordinator sends a doCommit request to all participants to finalize the transaction.
Implementing 3PC in Microservices using Java
To demonstrate the Three-Phase Commit protocol, we'll create a simplified example using Spring Boot. We'll implement two microservices that need to participate in a distributed transaction: OrderService and PaymentService.
Prerequisites
- Java Development Kit (JDK)
- Spring Boot
- Maven
Step-by-Step Implementation
Step 1. Set Up the Project
Create a new Spring Boot project with the following dependencies.
- Spring Web
- Spring Boot Starter Data JPA
- H2 Database
Step 2. Define the Microservices
OrderService
OrderServiceApplication.java
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
Java
Copy
OrderController.java
package com.example.orderservice.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@PostMapping("/order/canCommit")
public String canCommitOrder(@RequestParam Long orderId) {
// Logic to check if order can commit (e.g., check inventory)
return "yes";
}
@PostMapping("/order/preCommit")
public String preCommitOrder(@RequestParam Long orderId) {
// Logic to prepare the order (e.g., reserve inventory)
return "ack";
}
@PostMapping("/order/doCommit")
public String doCommitOrder(@RequestParam Long orderId) {
// Logic to commit the order (e.g., deduct inventory)
return "committed";
}
@PostMapping("/order/abort")
public String abortOrder(@RequestParam Long orderId) {
// Logic to abort the order (e.g., release reserved inventory)
return "aborted";
}
}
Java
Copy
PaymentService
PaymentServiceApplication.java
package com.example.paymentservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PaymentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentServiceApplication.class, args);
}
}
Java
Copy
PaymentController.java
package com.example.paymentservice.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PaymentController {
@PostMapping("/payment/canCommit")
public String canCommitPayment(@RequestParam Long paymentId) {
// Logic to check if payment can commit (e.g., check funds availability)
return "yes";
}
@PostMapping("/payment/preCommit")
public String preCommitPayment(@RequestParam Long paymentId) {
// Logic to prepare the payment (e.g., hold funds)
return "ack";
}
@PostMapping("/payment/doCommit")
public String doCommitPayment(@RequestParam Long paymentId) {
// Logic to commit the payment (e.g., transfer funds)
return "committed";
}
@PostMapping("/payment/abort")
public String abortPayment(@RequestParam Long paymentId) {
// Logic to abort the payment (e.g., release hold on funds)
return "aborted";
}
}
Java
Copy
Coordinator Service
Create a coordinator service to manage the three-phase commit process.
CoordinatorServiceApplication.java
package com.example.coordinatorservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoordinatorServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CoordinatorServiceApplication.class, args);
}
}
Java
Copy
CoordinatorController.java
package com.example.coordinatorservice.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class CoordinatorController {
@Autowired
private RestTemplate restTemplate;
@PostMapping("/transaction")
public String performTransaction(@RequestParam Long orderId, @RequestParam Long paymentId) {
String orderCanCommitUrl = "http://localhost:8081/order/canCommit?orderId=" + orderId;
String paymentCanCommitUrl = "http://localhost:8082/payment/canCommit?paymentId=" + paymentId;
ResponseEntity<String> orderResponse = restTemplate.postForEntity(orderCanCommitUrl, null, String.class);
ResponseEntity<String> paymentResponse = restTemplate.postForEntity(paymentCanCommitUrl, null, String.class);
if ("yes".equals(orderResponse.getBody()) && "yes".equals(paymentResponse.getBody())) {
String orderPreCommitUrl = "http://localhost:8081/order/preCommit?orderId=" + orderId;
String paymentPreCommitUrl = "http://localhost:8082/payment/preCommit?paymentId=" + paymentId;
orderResponse = restTemplate.postForEntity(orderPreCommitUrl, null, String.class);
paymentResponse = restTemplate.postForEntity(paymentPreCommitUrl, null, String.class);
if ("ack".equals(orderResponse.getBody()) && "ack".equals(paymentResponse.getBody())) {
restTemplate.postForEntity("http://localhost:8081/order/doCommit?orderId=" + orderId, null, String.class);
restTemplate.postForEntity("http://localhost:8082/payment/doCommit?paymentId=" + paymentId, null, String.class);
return "Transaction committed";
} else {
restTemplate.postForEntity("http://localhost:8081/order/abort?orderId=" + orderId, null, String.class);
restTemplate.postForEntity("http://localhost:8082/payment/abort?paymentId=" + paymentId, null, String.class);
return "Transaction aborted";
}
} else {
restTemplate.postForEntity("http://localhost:8081/order/abort?orderId=" + orderId, null, String.class);
restTemplate.postForEntity("http://localhost:8082/payment/abort?paymentId=" + paymentId, null, String.class);
return "Transaction aborted";
}
}
}
Java
Copy
RestTemplateConfig.java
package com.example.coordinatorservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Java
Copy
Running the Application
- Start OrderService on port 8081.
- Start PaymentService on port 8082.
- Start CoordinatorService on port 8080.
To test the 3PC implementation, you can use the following endpoint.
POST http://localhost:8080/transaction?orderId=1&paymentId=1
Conclusion
Implementing the Three-Phase Commit protocol in a microservices architecture helps ensure data consistency across distributed services with a reduced risk of blocking compared to Two-Phase Commit. While 3PC adds complexity and overhead, it is useful in scenarios where blocking can cause significant issues. However, it is important to consider the trade-offs and evaluate if other patterns, such as the Saga pattern, might be more suitable for your specific use case.