Blocking Protocols
First, we will explore protocols that ensure consistency by strictly locking data, which necessitates deep interactions between participating components.
Two-Phase Commit (2PC)
The Two-Phase Commit (2PC) protocol is a widely recognized solution for managing distributed transactions. It enables a group of participants to collectively commit (make permanent) or abort (discard) a transaction in a coordinated manner.
As its name implies, 2PC operates in two distinct phases. The protocol requires a designated coordinator; other participating entities are referred to as cohorts.
When a transaction is initiated:
Prepare Phase: Initially, the coordinator instructs the cohorts to prepare for the transaction. Each cohort performs the necessary actions, such as verifying data, acquiring locks, but they do not yet commit these changes.
Commit Phase: The coordinator then makes a decision based on the responses received from all cohorts:
- If all cohorts respond with
Yes
(indicating they are prepared), the coordinator instructs them to commit their dirty data, making the changes permanent. - If any cohort responds with
No
(or fails to respond, indicating it could not prepare), the coordinator instructs all cohorts to abort their dirty data, rolling back any provisional changes.
- If all cohorts respond with
Let’s illustrate this with an example of transferring money between different banks (from Bank A
to Bank B
):
Prepare
The coordinator sends a Prepare
request to the account services of both Bank A
and Bank B
.
These cohort services will verify account details, update balances provisionally,
lock the accounts’ balances to prevent other operations from interfering during the transaction.
If both services can successfully prepare, they respond Yes
back to the coordinator.
Commit
Observing that all cohorts are prepared for the transaction (having received Yes
from all),
the coordinator sends them a Commit
request. The cohorts then make their changes permanent.
Despite its straightforwardness, this process is susceptible to several evident problems.
Coordinator Failure
Firstly, system failures are unavoidable.
How should the system handle a scenario where the coordinator fails after sending the Prepare
requests
but before sending the final Commit
or Abort
decision?
When a cohort responds Yes
, it transitions to a Prepared
state,
locking some data and committing to finalize the transaction as per the coordinator’s eventual instruction.
If the coordinator fails at this juncture, the participants enter an uncertain state and may wait indefinitely.
A participant cannot unilaterally decide whether to commit or abort because it lacks information about the status
of other participants and the coordinator’s final decision.
Therefore, 2PC is a blocking protocol. The failure of a single node (the coordinator) can block the entire transaction indefinitely. Cohorts remain in an uncertain state, holding locks, until the coordinator recovers. This issue significantly degrades system availability.
Cohort Cooperation
It’s evident that the blocking problem arises largely because the coordinator is a Single Point of Failure . What if cohorts could interact with each other to resolve uncertainty?
One idea is that after a timeout period (waiting for the coordinator), cohorts could communicate among themselves. They might decide to commit if they achieve unanimity (all prepared cohorts agree to commit).
Unfortunately, this doesn’t fully resolve the availability issue. If any cohort goes down along with the coordinator, the remaining active cohorts might not achieve unanimity (as they can’t confirm the state of the crashed cohort) and would still be stuck in an uncertain state.
Moreover, in many implementations, the coordinator’s logic is often hosted on one of the cohort machines. This means its failure is equivalent to a coordinator and cohort failure, halting the entire system.
Three-phase Commit (3PC)
Three-Phase Commit (3PC) is a variation of 2PC designed to address some of its blocking issues. In essence, 3PC introduces an additional phase between the Prepare and Commit phases. This extra step aims to ensure that all cohorts are aware of the consensus outcome of the transaction before they proceed to actually commit the data.
Now, a transaction unfolds in three phases:
Prepare Phase
Similar to 2PC, the coordinator asks cohorts if they are willing and able to accept the transaction. Cohorts respond Yes
or No
.
PreCommit Phase
Based on the responses:
- If all cohorts voted
Yes
, the coordinator sends aPreCommit
message to all cohorts. - If any cohort voted
No
(or failed to respond), the coordinator sends anAbort
message.
Cohorts receiving a PreCommit
or Abort
message acknowledge it by responding with an ACK
(Acknowledgement) to the coordinator.
Commit Phase
After receiving ACKs from all cohorts for the PreCommit
message,
the coordinator sends a final Commit
request to all cohorts, instructing them to make their changes permanent.
Instead of blocking indefinitely,
3PC incorporates a timeout mechanism.
If the coordinator becomes unresponsive before the final Commit
,
cohorts can communicate mutually to reach a consensus:
- If at least one cohort has received the
PreCommit
request from the coordinator: This implies that all cohorts must have votedYes
in the prepare phase. Therefore, the cohorts can safely decide to commit the transaction. - If no cohort has received a
PreCommit
request: This indicates that the transaction was likely aborted by the coordinator. Thus, the transaction is aborted.
Let’s consider coordinator crashes during 3PC:
Case 1: Coordinator crashes after sending at least one PreCommit
message
Case 2: Coordinator crashes before sending any PreCommit
message
The PreCommit phase acts as a buffer, holding the final decision of the transaction.
Even if the coordinator fails after initiating the phase,
other cohorts can autonomously finalize the transaction based on whether any cohort reached the PreCommit
state.
Unfortunately, 3PC is not a perfect solution.
It does not guarantee consistency in the presence of network partitions.
Imagine a scenario where Server 1
receives a PreCommit
request but then gets partitioned from other cohorts.
During the timeout and recovery phase:
Server 1
(isolated) will proceed to commit the transaction.Server 2
andServer 3
will detect noPreCommit
among themselves and decide to abort the transaction.
This leads to an inconsistent state across the system.
The choice between 2PC and 3PC often involves a trade-off between Availability and Consistency:
- 2PC favors consistency over availability. If coordinator recovery is fast and the system can tolerate the potential downtime caused by blocking, 2PC offers a simpler solution.
- 3PC aims to improve availability by being non-blocking in more failure scenarios. However, resolving inconsistencies that can arise from network partitions in 3PC can be exceptionally intricate. Consequently, it is less commonly used in practice than 2PC.
Use Cases
The primary advantage of Phase Committing protocols is their ability to achieve strong consistency. Changes can be applied across participating services in a coordinated, seemingly simultaneous manner, which helps prevent inconsistent states from being left in the system.
Phase Committing is typically applied in contexts where a service needs to update data across multiple data sources immediately, for example:
- Between different shards of a single logical database.
- Between different types of data stores (e.g., a database and a message broker). In the context of an Event Streaming Platform, achieving exactly-once delivery semantics often requires additional mechanisms. 2PC can be an effective way to ensure that an operation (like updating a database record) and publishing an associated event occur atomically:
Both 2PC and 3PC are considered low-level distributed algorithms.
Requiring microservices to expose interfaces like PrepareTransaction
, CommitTransaction
, etc.,
for inter-service transactions can create high coupling between services.
Therefore, they are less frequently used for orchestrating transactions between distinct business services in a
Microservice
environment,
where high-level patterns like Saga are often preferred.