Imagine this: you have an order in an online shop that needs to do two things at the same time:
- Decrease the stock quantity in the warehouse.
- Charge the customer's bank card.
These operations happen in two different systems — for example, one database manages inventory while another handles payments. How can you be sure both operations succeeded? What if one of them fails? That's where the two-phase commit comes in.
Two-Phase Commit (2PC) is a protocol for ensuring data consistency across transactions that span more than one system. It coordinates participants (like databases or microservices) so that they either all commit the operation or all roll back in case of an error.
The two phases of Two-Phase Commit
As the name implies, the process runs in two phases:
Prepare Phase:
- The transaction coordinator asks all participants (databases or microservices) to prepare for the transaction.
- Each participant does preparatory work (e.g., reserves resources) and replies to the coordinator:
- OK, if it's ready to perform the operation.
- FAIL, if it can't perform the operation.
Commit Phase:
- If ALL participants replied OK, the coordinator issues a "commit" command.
- If at least one participant replied FAIL, the coordinator issues a "rollback" command.
Here's a short diagram:
Coordinator -----> Participants: "Are you ready?"
Participants -------> Coordinator: "Ready/Not ready"
Coordinator -----> Participants: "Commit/Rollback"
Benefits and limitations of Two-Phase Commit
Let's break down what's good and what's not so great about this approach.
Benefits
- Consistency: the protocol guarantees that all participants end up in the same state — either all changes are committed or all are rolled back.
- Simple model: 2PC gives a straightforward way to manage transactions across distributed systems.
Limitations
- Performance: the prepare phase adds network latency. In large systems this can become a bottleneck.
- Resource locking: resources on participants are locked during the transaction, which reduces overall throughput.
- Coordinator failure: if the coordinator goes down, a transaction can get stuck. Yeah, that's annoying.
Implementing Two-Phase Commit in Spring
Spring supports two-phase commit via the Java Transaction API (JTA). Let's see how it works.
Setting up JTA in Spring Boot
To use two-phase commit we need JTA and support for multiple data sources. As an example, we'll use two databases: one for orders and another for payments.
Add JTA dependencies to pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jta</artifactId>
<version>5.0.8</version>
</dependency>
Atomikos is a popular transaction manager that supports JTA. It will act as the coordinator.
Configure two data sources
@Configuration
public class DataSourceConfig {
@Bean(name = "ordersDataSource")
@Primary
public DataSource ordersDataSource() {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("ordersDB");
dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/orders", "root", "password"));
return dataSource;
}
@Bean(name = "paymentsDataSource")
public DataSource paymentsDataSource() {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("paymentsDB");
dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/payments", "root", "password"));
return dataSource;
}
private Properties createDatabaseProperties(String url, String username, String password) {
Properties properties = new Properties();
properties.setProperty("user", username);
properties.setProperty("password", password);
properties.setProperty("url", url);
return properties;
}
}
Defining the TransactionManager
Spring will auto-detect JTA data sources if they're configured correctly.
Example of a two-phase transactional method
Now that we have two data sources, we can write transactional methods that operate across them.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentRepository paymentRepository;
@Transactional
public void createOrderAndProcessPayment(Order order, Payment payment) {
// Save the order in one database
orderRepository.save(order);
// Process the payment in the other database
paymentRepository.save(payment);
// If something goes wrong, the transaction will be rolled back automatically
if (payment.getAmount() > 1000) {
throw new IllegalArgumentException("Amount too large!");
}
}
}
When to use Two-Phase Commit?
2PC works well when:
- You have strict consistency requirements.
- Transactions span multiple systems or databases.
However, consider its downsides: high cost and vulnerability to failures. If strict consistency isn't critical, you might look at lighter alternatives like an event-driven architecture (EDA) — we'll cover that later in the course.
Common mistakes and how to avoid them
The most common mistake is not accounting for coordinator failure. In that case a transaction can stay in a pending state, and locked resources can cause trouble.
To avoid this, it's important to:
- Use reliable transaction coordinators (for example, Atomikos).
- Monitor transaction states and promptly resolve stuck ones.
That wraps up our deep dive into Two-Phase Commit. In the next lecture we'll discuss how to optimize transactions in applications without sacrificing performance.
GO TO FULL VERSION