Two-Phase Locking Protocol
Shrinking Phase
Read aloud is unavailable in this browser.
The Art of Letting Go
If the Growing Phase is about establishing territory, the Shrinking Phase is about gracefully relinquishing control. This phase determines when locked resources become available to other transactions, directly impacting system concurrency and throughput.
The Shrinking Phase might seem straightforward—just release the locks when you're done, right? But the timing and strategy of lock releases have profound implications for system performance, recoverability, and even correctness. A single premature release can cascade into data corruption. Holding locks too long starves other transactions of resources.
In this page, we'll explore every aspect of the Shrinking Phase, from the fundamental mechanics of lock release to the sophisticated strategies that production databases employ to balance correctness with concurrency.
By the end of this page, you will understand the mechanics of lock release during the shrinking phase, how lock release timing affects other waiting transactions, the relationship between lock release and transaction commitment, and why premature release can lead to serious correctness problems.
Mechanics of Lock Release
During the Shrinking Phase, a transaction methodically releases the locks it acquired during the Growing Phase. While conceptually simple, the actual process involves careful coordination with the lock manager and waiting transactions.
The Lock Release Lifecycle:
Step 1: Release Request Issuance
When a transaction decides to release a lock (either explicitly or implicitly at COMMIT/ROLLBACK), it signals the lock manager. The release request specifies:
- Transaction ID (TID): The transaction releasing the lock
- Data Item Identifier: The specific item being unlocked
- Lock Mode: The mode being released (relevant for transactions holding multiple lock types)
Step 2: Lock Table Update
The lock manager updates its internal data structures:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
FUNCTION process_lock_release(tid, data_item): // Validate the release request current_locks = lock_table.get(data_item) IF tid NOT IN current_locks.holders: RAISE ERROR "Transaction does not hold lock on item" // Remove transaction from holders list current_locks.holders.remove(tid) lock_table.update(data_item, current_locks) // Log the release for recovery purposes log_record = create_log("UNLOCK", tid, data_item), write_to_log(log_record) // Check if waiting transactions can now proceed grant_waiting_requests(data_item) RETURN SUCCESS FUNCTION grant_waiting_requests(data_item): wait_queue = get_wait_queue(data_item) current_holders = lock_table.get(data_item).holders // Process wait queue in order granted_list = [] FOR EACH (waiting_tid, requested_mode) IN wait_queue: // Check if request is now compatible with current holders IF is_compatible_with_all(requested_mode, current_holders): // Grant the lock lock_table.add_holder(data_item, waiting_tid, requested_mode) wait_queue.remove(waiting_tid) granted_list.append(waiting_tid) // Update current holders for next iteration current_holders.append((waiting_tid, requested_mode)) ELSE: // Cannot grant - stop processing (preserve FIFO for writes) // Continue checking for compatible shared requests (optional) IF requested_mode == EXCLUSIVE: BREAK // Don't skip over exclusive requests // Wake up all granted transactions FOR EACH tid IN granted_list: signal_transaction(tid, LOCK_GRANTED)A single lock release can trigger a cascade of grants. If Transaction A releases an exclusive lock, and the wait queue contains [B(Shared), C(Shared), D(Exclusive), E(Shared)], the lock manager grants locks to B, C simultaneously. D still waits (conflicts with B, C), and E might be granted (compatible with B, C) or wait (if strict FIFO is enforced).
When Does the Shrinking Phase Begin?
The transition from Growing Phase to Shrinking Phase is a critical moment in a transaction's lifecycle. This transition occurs the instant a transaction releases its first lock—and once crossed, there's no going back.
The Lock Point Revisited:
| Time | Operation | Phase | Lock Count |
|---|---|---|---|
| t₁ | BEGIN TRANSACTION | — | 0 |
| t₂ | Lock(A) | Growing | 1 |
| t₃ | Read(A) | Growing | 1 |
| t₄ | Lock(B) | Growing | 2 |
| t₅ | Write(B) | Growing | 2 |
| t₆ | Lock(C) | Growing | 3 |
| t₇ | Read(C) | Growing | 3 |
| t₈ | Unlock(A) ⚡ | → Shrinking | 2 |
| t₉ | Unlock(B) | Shrinking | 1 |
| t₁₀ | Unlock(C) | Shrinking | 0 |
| t₁₁ | COMMIT | Complete | 0 |
Key Observations:
-
Lock Point occurs at t₇ — This is the moment of maximum lock holding. All locks the transaction will ever need have been acquired.
-
Phase transition occurs at t₈ — The first Unlock operation marks the beginning of the Shrinking Phase. This is an irreversible state change.
-
No new locks after t₈ — Once Unlock(A) occurs, the transaction cannot acquire Lock(D) or any other new lock. Attempting to do so violates 2PL.
-
Work can continue — Notice that reads and writes can occur in both phases. The phase distinction only constrains lock operations, not data operations.
In basic 2PL, the first unlock is irrevocable. Even if the transaction later realizes it needs to access another data item, it cannot acquire a new lock. This constraint is why transaction designers must carefully plan which data items will be needed before releasing any locks. In practice, this is why Strict 2PL (holding all locks until commit) became the dominant variant.
1234567891011121314151617181920
-- INCORRECT: This transaction violates 2PL BEGIN TRANSACTION; Lock(Account_A); -- Growing phaseRead(Account_A); -- balance = $1000Unlock(Account_A); -- Shrinking phase begins ⚡ -- Transaction decides it needs to check Account_B-- Perhaps based on a business rule discovered after reading A Lock(Account_B); -- ❌ VIOLATION! Cannot acquire after releasing -- This would re-enter growing phase -- 2PL FORBIDS THIS -- The transaction is now INVALID under 2PL-- Database would either:-- (a) ABORT the transaction-- (b) Block and wait (if using Conservative 2PL variant)-- (c) Never happen (if using Strict 2PL - locks held until commit)Lock Release Ordering
A subtle but important aspect of the Shrinking Phase is the order in which locks are released. While 2PL correctness doesn't depend on release order (any order maintains serializability), the choice of ordering significantly impacts concurrency and deadlock behavior.
- •Pattern: Release in reverse acquisition order
- •Example: Lock(A), Lock(B), Lock(C) → Unlock(C), Unlock(B), Unlock(A)
- •Advantage: Matches natural nested access patterns
- •Advantage: Can help prevent certain deadlocks
- •Use Case: Hierarchical locking scenarios
- •Pattern: Release in acquisition order
- •Example: Lock(A), Lock(B), Lock(C) → Unlock(A), Unlock(B), Unlock(C)
- •Advantage: Releases "oldest" locks earliest
- •Advantage: May improve concurrency for hot resources
- •Use Case: Sequential processing pipelines
Strategic Release for Concurrency:
In practice, the optimal release order often depends on workload patterns:
12345678910111213141516171819202122232425262728
-- Scenario: Processing an order (involves inventory, account, shipping) BEGIN TRANSACTION; -- Growing PhaseLock(Inventory_Item_42); -- Check stockLock(Customer_Account_7); -- Check payment methodLock(Shipping_Zone_3); -- Calculate shipping -- Process the order...Read(Inventory_Item_42); -- 50 units availableRead(Customer_Account_7); -- Credit card on fileRead(Shipping_Zone_3); -- $5.99 shipping rate -- Shrinking Phase - Strategic Release Order -- OPTION A: Release hottest resource firstUnlock(Inventory_Item_42); -- High contention - release earlyUnlock(Customer_Account_7); -- Medium contentionUnlock(Shipping_Zone_3); -- Low contention - release last -- OPTION B: Release least-needed first Unlock(Shipping_Zone_3); -- Done with shipping calcUnlock(Customer_Account_7); -- Done with payment checkUnlock(Inventory_Item_42); -- Keep stock locked longest -- The 2PL guarantee holds regardless of order-- But system throughput may vary based on contention patternsIn high-contention systems, consider releasing the most contended locks first during the shrinking phase. This unblocks waiting transactions sooner, improving overall system throughput. However, ensure you truly won't need those resources again—remember, you can't re-acquire after releasing!
The Danger of Early Release
We've emphasized that 2PL guarantees serializability. But let's deeply understand why early release violates this guarantee. This understanding is crucial for debugging concurrency anomalies.
12345678910111213141516171819202122232425262728293031
-- THE LOST UPDATE VIA EARLY RELEASE -- Scenario: Two transactions updating the same account balance-- Both want to add to the balance based on reading the current value -- Initial state: Account_A.balance = $1000 -- Transaction T₁ (Early Release - INCORRECT)T₁: Lock(Account_A) T₁: Read(Account_A) → $1000 T₁: Unlock(Account_A) -- ❌ EARLY RELEASE!T₁: compute: new_balance = 1000 + 200 = $1200-- T₁ needs to write... but released the lock! -- Transaction T₂ (executes during T₁'s computation)T₂: Lock(Account_A) -- Granted! T₁ released itT₂: Read(Account_A) → $1000 -- Still $1000T₂: compute: new_balance = 1000 + 300 = $1300T₂: Write(Account_A) ← $1300 -- Balance now $1300T₂: Unlock(Account_A)T₂: COMMIT -- T₁ continues (assumes it still has valid data)T₁: Lock(Account_A) -- Re-acquired (VIOLATES 2PL)T₁: Write(Account_A) ← $1200 -- OVERWRITES T₂'s update!T₁: Unlock(Account_A)T₁: COMMIT -- RESULT: balance = $1200-- CORRECT: balance should be $1000 + $200 + $300 = $1500-- LOST: T₂'s $300 deposit vanished!This is a classic lost update scenario. By releasing the lock early, T₁ allowed T₂ to read stale data and base its update on it. Then T₁'s re-acquisition (which violates 2PL) overwrote T₂'s committed work. If T₁ had held the lock throughout its growing phase, T₂ would have waited, read $1200, and written $1500.
Why 2PL Prevents This:
Under proper 2PL, T₁ would:
- Lock Account_A
- Read → $1000
- Compute new value
- Write ← $1200
- Only then Unlock(Account_A)
T₂ would block at step 1, waiting for T₁'s exclusive lock. After T₁ commits:
- T₂ Lock(Account_A) — granted
- T₂ Read → $1200 (sees T₁'s update)
- T₂ Compute: 1200 + 300 = $1500
- T₂ Write ← $1500
- T₂ Unlock and Commit
Result: $1500 — Both updates are correctly applied.
- •Lost Updates — Concurrent writes overwrite each other's changes
- •Inconsistent Reads — Transactions observe impossible database states
- •Constraint Violations — Invariants broken by interleaved operations
- •Cascading Failures — One corrupted value propagates to dependent computations
- •Audit Trail Corruption — Historical records become unreliable
Implicit Release at Commit and Abort
In most database systems, explicit lock release commands are rare. Instead, locks are released implicitly as part of transaction termination (COMMIT or ROLLBACK). This approach simplifies programming and, crucially, enables Strict 2PL.
The Commit-Release Pattern:
12345678910111213141516171819202122
-- Typical transaction - no explicit lock/unlock statements BEGIN TRANSACTION; -- Implicit Lock(customers) on touched rowsUPDATE customers SET last_purchase = NOW() WHERE id = 42; -- Implicit Lock(orders) on new rowINSERT INTO orders (customer_id, total) VALUES (42, 99.99); -- Implicit Lock(inventory) on touched rows UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 101; COMMIT; -- ← ALL locks released here atomically -- What happens at COMMIT:-- 1. Transaction log is flushed (durability)-- 2. Commit record written to log-- 3. Lock Manager notified of transaction completion-- 4. ALL locks held by this transaction released-- 5. Waiting transactions notified-- 6. Transaction resources freedAbort and Rollback Release:
When a transaction aborts (either explicitly via ROLLBACK or due to an error/deadlock), locks are also released—but with additional considerations:
| Aspect | COMMIT | ABORT/ROLLBACK |
|---|---|---|
| Changes Persisted | Yes | No — Changes undone |
| Lock Release Timing | After commit record durable | After undo complete |
| Waiting Txns See | Committed values | Original values (pre-abort) |
| Log Records | Commit record written | Abort record + CLRs written |
| Resource Cleanup | Standard cleanup | Buffer pool pages invalidated |
When a transaction aborts, its changes must be undone before locks are released. If locks were released while undo was still in progress, other transactions might read partially-undone data. This would violate atomicity—transactions would observe states that never "existed" in any committed view of the database.
Shrinking Phase and System Concurrency
The Shrinking Phase directly controls how quickly resources return to the shared pool. Understanding this relationship is essential for capacity planning and performance tuning.
The Concurrency Equation:
Consider a simplified model where:
- N transactions want to access resource R
- Each transaction holds R for time T
- Only one transaction can hold the exclusive lock at a time
Maximum Throughput = 1/T transactions per time unit
The Shrinking Phase duration directly impacts T. Longer lock holding = Lower throughput.
123456789101112131415161718192021222324252627282930313233343536
SCENARIO: E-commerce checkout (exclusive lock on inventory item) Processing Timeline for Single Item: [────────────── Transaction Duration ──────────────][──Growing──][────────Work────────][──Shrinking──] Lock(I) Process Write Unlock(I) COMMIT Timeline with Basic 2PL (early shrinking allowed):T₁: |--Lock--|---Work---|---Shrink/Commit---|T₂: |--Lock--|---Work---|---Shrink/Commit---|T₃: |--Lock--|---Work---| Total time for 3 transactions: ~2.5x single transaction(Overlap possible in shrinking phase) Timeline with Strict 2PL (shrinking only at commit):T₁: |--Lock--|---Work---|-Commit-|T₂: |--Lock--|---Work---|-Commit-|T₃: |--Lock--... Total time for 3 transactions: 3x single transaction(No overlap - each waits for previous to fully complete) TRADE-OFF ANALYSIS:┌──────────────────┬────────────────┬────────────────────┐│ Property │ Basic 2PL │ Strict 2PL │├──────────────────┼────────────────┼────────────────────┤│ Concurrency │ Higher │ Lower ││ Recoverability │ Not guaranteed │ Guaranteed ││ Cascading Abort │ Possible │ Impossible ││ Lock Hold Time │ Shorter │ Longer ││ Deadlock Risk │ Similar │ Similar │└──────────────────┴────────────────┴────────────────────┘The Recoverability Trade-off:
Basic 2PL allows lock release before commit, which enables higher concurrency but introduces a subtle problem: cascading aborts.
If T₁ releases a lock on X before committing, and T₂ reads X (with the value T₁ wrote), then T₂ depends on T₁'s success. If T₁ later aborts, T₂ has read data that was never actually committed—T₂ must also be aborted! This can cascade through many transactions.
Strict 2PL solves this by holding all locks until commit. Since no other transaction can read uncommitted data, aborts never cascade. The trade-off is reduced concurrency.
Virtually all production database systems implement Strict 2PL (or its stronger variant, Rigorous 2PL) precisely because of the recoverability guarantee. The concurrency cost is considered acceptable compared to the complexity of handling cascading aborts. We'll explore these variants in detail in the next module.
Lock Release in Distributed Systems
When databases span multiple nodes, the Shrinking Phase becomes significantly more complex. Lock release must be coordinated across the distributed system to maintain correctness.
The Distributed Commit Challenge:
Two-Phase Commit (2PC) and Lock Release:
In distributed transactions using Two-Phase Commit (2PC), the Shrinking Phase is intimately tied to the commit protocol:
- Prepare Phase: Each node continues holding its locks. No releases occur.
- Vote: Nodes vote YES (can commit) or NO (must abort). Locks still held.
- Commit Decision: Coordinator decides COMMIT or ABORT.
- Commit Phase: Each node commits/aborts and only then releases locks.
Critical Constraint: All nodes must maintain their locks through the entire prepare phase. If a node released locks after voting YES but before receiving the final COMMIT, another transaction could see incomplete state.
- •Lock Holding During Prepare: Nodes hold locks during the uncertainty window between PREPARE and COMMIT/ABORT decision
- •Coordinator Failure: If coordinator fails after PREPARE, nodes are stuck holding locks until recovery
- •Network Partitions: Nodes may be unable to receive the final decision, blocking lock release
- •Clock Skew: Distributed timeout detection is complicated by clock differences
- •Ordered Release: May need to release locks in specific order across nodes to prevent distributed deadlocks
Modern distributed databases like CockroachDB, Spanner, and YugabyteDB use sophisticated variations of 2PC with features like parallel commits, pipelining, and optimistic lock release to minimize the performance impact of distributed lock coordination. These systems maintain 2PL semantics while achieving high throughput across distributed nodes.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.