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 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.threePhaseCommit.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);
}
}
OrderController.java
package com.threePhaseCommit.OrderService.controller;
import com.threePhaseCommit.OrderService.service.OrderServiceImpl;
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("/api/v1/order")
public class OrderController {
@Autowired
OrderServiceImpl orderService;
@PostMapping("/canCommit")
public String canCommitOrder(@RequestParam Long orderId) {
// Logic to check if order can commit (e.g., check inventory)
String response = orderService.canCommitOrder();
return response;
}
@PostMapping("/preCommit")
public String preCommitOrder(@RequestParam Long orderId) {
// Logic to prepare the order (e.g., reserve inventory)
String response = orderService.preCommitOrder();
return response;
}
@PostMapping("/doCommit")
public String doCommitOrder(@RequestParam Long orderId) {
// Logic to commit the order (e.g., deduct inventory)
String response = orderService.doCommitOrder();
return response;
}
@PostMapping("/abort")
public String abortOrder(@RequestParam Long orderId) {
// Logic to abort the order (e.g., release reserved inventory)
String response = orderService.abortOrder();
return response;
}
}
OrderService.java
package com.threePhaseCommit.OrderService.service;
import com.threePhaseCommit.OrderService.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl {
@Autowired
OrderRepository orderRepository;
//Your own logic for respective methods
public String doCommitOrder() {
// Logic to commit the order (e.g., deduct inventory)
return "DoCommit Order in Order Service";
}
public String canCommitOrder() {
// Logic to check if order can commit (e.g., check inventory)
return "Can Commit Order in Order Service";
}
public String preCommitOrder() {
// Logic to prepare the order (e.g., reserve inventory)
return "Pre Commit Order in Order Service";
}
public String abortOrder() {
// Logic to abort the order (e.g., release reserved inventory)
return "Aborted Order in Order Service";
}
}
OrderRepository.java
package com.threePhaseCommit.OrderService.repository;
import com.threePhaseCommit.OrderService.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface OrderRepository extends JpaRepository<Order, UUID> {
}
Order.java
package com.threePhaseCommit.OrderService.entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy= GenerationType.UUID)
private UUID orderId;
private String orderName;
private String orderDesc;
private Date createdAt;
private Date updatedAt;
private String createdBy;
private String updatedBy;
private List<Object> items;
}
application.properties file for Order Service
spring.application.name=OrderService
server.port=8081
Expected Output
- For Can Commit Order.
- For Do Commit Order.
- For Pre-Commit Order.
- For Abort Order.
PaymentService
PaymentServiceApplication.java
package com.threePhaseCommit.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);
}
}
PaymentController.java
package com.threePhaseCommit.PaymentService.controller;
import com.threePhaseCommit.PaymentService.repository.PaymentRepository;
import com.threePhaseCommit.PaymentService.service.PaymentServiceImpl;
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("/api/v1/payment")
public class PaymentController {
@Autowired
PaymentServiceImpl paymentService;
@PostMapping("/canCommit")
public String canCommitPayment(@RequestParam Long paymentId) {
// Logic to check if payment can commit (e.g., check funds availability)
String response = paymentService.canCommitPayment();
return response;
}
@PostMapping("/preCommit")
public String preCommitPayment(@RequestParam Long paymentId) {
// Logic to prepare the payment (e.g., hold funds)
String response = paymentService.preCommitPayment();
return response;
}
@PostMapping("/doCommit")
public String doCommitPayment(@RequestParam Long paymentId) {
// Logic to commit the payment (e.g., transfer funds)
String response = paymentService.doCommitPayment();
return response;
}
@PostMapping("/abort")
public String abortPayment(@RequestParam Long paymentId) {
// Logic to abort the payment (e.g., release hold on funds)
String response = paymentService.abortPayment();
return response;
}
}
PaymentService.java
package com.threePhaseCommit.PaymentService.service;
import com.threePhaseCommit.PaymentService.repository.PaymentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PaymentServiceImpl {
@Autowired
PaymentRepository paymentRepository;
// Your own logic for respective methods
public String canCommitPayment() {
return "DoCommit Payment in Payment Service";
}
public String preCommitPayment() {
return "DoCommit Payment in Payment Service";
}
public String doCommitPayment() {
return "DoCommit Payment in Payment Service";
}
public String abortPayment() {
return "DoCommit Payment in Payment Service";
}
}
Payment Repository.java
package com.threePhaseCommit.paymentService.repository;
import com.threePhaseCommit.paymentService.entity.payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface paymentRepository extends JpaRepository<Payment, UUID> {
}
Payment.java
package com.threePhaseCommit.PaymentService.entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {
@Id
@GeneratedValue(strategy= GenerationType.UUID)
private UUID paymentId;
private String paymentGatewayName;
private String paymentMode;
private String paymentDesc;
private Date createdAt;
private Date updatedAt;
private String createdBy;
private String updatedBy;
}
application.properties file for Payment Service
spring.application.name=PaymentService
server.port=8082
- For Pre Commit Payment.
- For Can Commit Payment.
- For Do Commit Payment.
- For Abort Payment.
Coordinator Service
Create a coordinator service to manage the three-phase commit process.
CoordinatorServiceApplication.java
package com.threePhaseCommit.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);
}
}
CoordinatorController.java
package com.threePhaseCommit.Coordinate_Service.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 orderPrepareUrl = "http://localhost:8081/api/v1/order/prepare?orderId=" + orderId;
String paymentPrepareUrl = "http://localhost:8082/api/v1/payment/prepare?paymentId=" + paymentId;
ResponseEntity<String> orderResponse = restTemplate.postForEntity(orderPrepareUrl, null, String.class);
ResponseEntity<String> paymentResponse = restTemplate.postForEntity(paymentPrepareUrl, null, String.class);
if ("prepared".equals(orderResponse.getBody()) && "prepared".equals(paymentResponse.getBody())) {
restTemplate.postForEntity("http://localhost:8081/api/v1/order/commit?orderId=" + orderId, null, String.class);
restTemplate.postForEntity("http://localhost:8082/api/v1/payment/commit?paymentId=" + paymentId, null, String.class);
return "Transaction committed";
} else {
restTemplate.postForEntity("http://localhost:8081/api/v1/order/abort?orderId=" + orderId, null, String.class);
restTemplate.postForEntity("http://localhost:8082/api/v1/payment/abort?paymentId=" + paymentId, null, String.class);
return "Transaction aborted";
}
}
}
RestTemplateConfig.java
package com.threePhaseCommit.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();
}
}
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.