Loading content...
We've established that cascadeless schedules prevent cascading rollbacks by ensuring transactions only read committed data. But consider this scenario:
Question: What should X be after T₂'s abort?
The answer seems obvious—restore X to T₁'s committed value of 100. But the recovery process is surprisingly tricky. T₂'s abort needs to undo its write, but simply restoring the "before image" (the value before T₂'s write) would restore T₁'s uncommitted value of 100—which happens to be correct only because T₁ committed. What if T₁ had aborted instead?
Strict schedules eliminate this complexity entirely. By preventing any transaction from reading OR writing data that another uncommitted transaction has written, strict schedules make recovery trivially simple: undo using before-images, redo using after-images, with no concern for uncommitted transaction interactions.
By the end of this page, you will understand the formal definition of strict schedules, why they provide the strongest recoverability guarantees, how strict schedules simplify recovery operations, the relationship between strictness and locking protocols, and the performance implications of strictness in practice.
Cascadeless schedules handle reads of uncommitted data, but they don't address writes to data that's been written by an uncommitted transaction. This creates a subtle recovery problem.
The Write-Write Dependency Problem:
1234567891011121314151617181920212223242526
Initial State: X = 50 (committed) Time T₁ T₂ X Value Notes──── ────────────── ────────────── ───────────── ──────────────────────t1 W(X) ← 100 100 T₁ writes (uncommitted)t2 W(X) ← 200 200 T₂ overwrites (uncommitted)t3 C 200 T₁ commitst4 A ??? T₂ aborts - what should X be? RECOVERY DILEMMA: Option A: Restore T₂'s "before image" (what X was before T₂'s write)- Before image = 100 (T₁'s uncommitted value at t2)- But T₁ has now committed, so 100 is actually correct- This works... but only by accident Option B: What if the order was different?Time T₁ T₂ X Value ──── ────────────── ────────────── ───────────── t1 W(X) ← 100 100 T₁ writest2 W(X) ← 200 200 T₂ writest3 C 200 T₂ commitst4 A ??? T₁ aborts Now what? T₁'s before image was 50, but restoring 50 would undoT₂'s committed value of 200! We have a dependency conflict.The core issue is that when transactions can write to data modified by uncommitted transactions, before-images become unreliable for recovery. The before-image might be:
Strict schedules solve this by preventing the situation entirely.
Without strictness, before-images for recovery might contain uncommitted data from other transactions. This means undoing one transaction could accidentally undo or corrupt another transaction's changes, even if that other transaction commits.
Definition: A schedule S is strict if and only if, for every pair of transactions Tᵢ and Tⱼ in S where Tᵢ writes a data item X:
No other transaction Tⱼ can read or write X until Tᵢ has committed or aborted.
Formally: If Tᵢ writes X at time t and Tⱼ (j ≠ i) reads or writes X at time t', then:
t' > commit(Tᵢ) or t' > abort(Tᵢ)
In simpler terms: Once a transaction writes to a data item, that item is "locked" until the transaction completes (commits or aborts). No other transaction can touch it.
Comparison with cascadeless:
| Property | Dirty Read | Write After Uncommitted Write | Guarantees |
|---|---|---|---|
| Recoverable | Allowed (with ordering) | Allowed | Recovery is possible |
| Cascadeless | Prevented | Allowed | No cascading rollbacks |
| Strict | Prevented | Prevented | Simple before-image recovery |
Set-theoretic relationship:
Strict Schedules ⊂ Cascadeless Schedules ⊂ Recoverable Schedules ⊂ All Schedules
Strict schedules represent the strongest practical recoverability property. There's an even stronger property (rigorous schedules, which also prevent reads until commit), but strict schedules are the most commonly used in practice because they enable efficient recovery while maintaining good concurrency.
The power of strict schedules lies in their recovery properties. With strictness, the database can use a simple, efficient recovery algorithm:
Before-Image Reliability:
In a strict schedule, when transaction Tᵢ writes to data item X:
Undo Recovery Algorithm (Strict Schedule):
For each aborted transaction Tᵢ:
For each data item X that Tᵢ wrote:
Restore X to its before-image (guaranteed committed value)
Mark Tᵢ as aborted
Done. No need to check other transactions.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
from dataclasses import dataclass, fieldfrom typing import Dict, List, Any, Optionalfrom enum import Enum class TransactionStatus(Enum): ACTIVE = "active" COMMITTED = "committed" ABORTED = "aborted" @dataclassclass LogEntry: txn_id: str item: str before_image: Any after_image: Any operation: str # "WRITE", "COMMIT", "ABORT" @dataclassclass StrictScheduleRecovery: """ Demonstrates why strict schedules enable simple recovery. Key insight: In a strict schedule, before-images are always committed values, making undo operations trivially correct. """ committed_data: Dict[str, Any] = field(default_factory=dict) current_data: Dict[str, Any] = field(default_factory=dict) log: List[LogEntry] = field(default_factory=list) locks: Dict[str, str] = field(default_factory=dict) # item -> txn holding exclusive lock def write(self, txn_id: str, item: str, value: Any) -> bool: """ Write with strict scheduling - must acquire exclusive lock. Lock held until commit/abort (enforces strictness). """ # Check for strict scheduling constraint if item in self.locks and self.locks[item] != txn_id: print(f" {txn_id}: BLOCKED - {item} locked by {self.locks[item]} (strict)") return False # Acquire exclusive lock (or already have it) self.locks[item] = txn_id # Before image is ALWAYS the committed value (strict guarantee) before = self.committed_data.get(item) after = value self.log.append(LogEntry(txn_id, item, before, after, "WRITE")) self.current_data[item] = value print(f" {txn_id}: W({item}) = {value}, before_image = {before} (guaranteed committed)") return True def commit(self, txn_id: str) -> None: """Commit - make writes permanent and release locks.""" # Update committed data for entry in self.log: if entry.txn_id == txn_id and entry.operation == "WRITE": self.committed_data[entry.item] = entry.after_image # Release locks items_to_unlock = [item for item, holder in self.locks.items() if holder == txn_id] for item in items_to_unlock: del self.locks[item] self.log.append(LogEntry(txn_id, "", None, None, "COMMIT")) print(f" {txn_id}: COMMIT - locks released, data committed") def abort(self, txn_id: str) -> None: """ Abort - restore before-images (simple due to strictness). In a strict schedule, this is trivially correct: - Before-images are always committed values - No other transaction has written to our items - Simply restore and release locks """ print(f" {txn_id}: ABORT - restoring before-images...") # Restore before-images (guaranteed correct due to strictness) for entry in reversed(self.log): if entry.txn_id == txn_id and entry.operation == "WRITE": old_value = self.current_data.get(entry.item) self.current_data[entry.item] = entry.before_image print(f" Restore {entry.item}: {old_value} → {entry.before_image} (committed value)") # Release locks items_to_unlock = [item for item, holder in self.locks.items() if holder == txn_id] for item in items_to_unlock: del self.locks[item] print(f" Release lock on {item}") self.log.append(LogEntry(txn_id, "", None, None, "ABORT")) print(f" {txn_id}: ABORT complete - no cascade, no dependency analysis needed") def show_state(self): print(f" Current data: {self.current_data}") print(f" Committed data: {self.committed_data}") print(f" Active locks: {self.locks}") # Demonstrate strict schedule recoveryprint("=" * 70)print("STRICT SCHEDULE RECOVERY DEMONSTRATION")print("=" * 70) db = StrictScheduleRecovery()db.committed_data = {"X": 50, "Y": 100}db.current_data = {"X": 50, "Y": 100} print("\nInitial state:")db.show_state() print("\n--- T1 writes X ---")db.write("T1", "X", 150) print("\n--- T2 tries to write X (blocked by strict scheduling) ---")result = db.write("T2", "X", 200)if not result: print(" T2 must wait for T1 to complete") print("\n--- T2 writes Y (different item, allowed) ---")db.write("T2", "Y", 250) print("\nState after writes:")db.show_state() print("\n--- T1 commits ---")db.commit("T1") print("\n--- Now T2 can write X ---")db.write("T2", "X", 300) print("\n--- T2 aborts ---")db.abort("T2") print("\nFinal state (after T2 abort):")db.show_state()print("\n✓ Recovery was simple: X = T1's committed value (150), Y = original (100)")The most common way to enforce strict schedules is through Strict Two-Phase Locking (Strict 2PL). This protocol extends basic 2PL with a simple additional rule:
Strict 2PL Rule:
All exclusive (write) locks held by a transaction must be retained until the transaction commits or aborts.
This single rule guarantees strict schedules:
| Protocol | Lock Release Rule | Schedule Property | Recovery Complexity |
|---|---|---|---|
| Basic 2PL | After lock point (shrinking phase) | Serializable | Complex |
| Strict 2PL | Exclusive locks at commit/abort | Serializable + Strict | Simple |
| Rigorous 2PL | All locks at commit/abort | Serializable + Rigorous | Simplest |
Why Strict 2PL works:
Two-Phase property (basic 2PL): Guarantees serializability—the schedule is equivalent to some serial execution.
Exclusive lock retention: By holding write locks until commit/abort:
Most commercial databases use Strict 2PL for their locking-based concurrency control because it provides both serializability and simple recovery.
12345678910111213141516171819202122232425262728293031323334353637383940414243
STRICT 2PL EXECUTION EXAMPLE Transaction T₁ Transaction T₂ Locks Held─────────────────────────────────────────────────────────────────────────────BEGIN {} BEGIN {} WRITE(X) {X-T₁}[Acquire exclusive lock] WRITE(Y) {X-T₁, Y-T₂} [Acquire exclusive lock] READ(Y) {X-T₁, Y-T₂}[BLOCKED - T₂ holds exclusive lock on Y] COMMIT {X-T₁} [Release Y lock at commit] READ(Y) [now succeeds] {X-T₁, Y-T₁}[Acquire shared lock, sees T₂'s committed value] WRITE(Z) {X-T₁, Y-T₁, Z-T₁}[Acquire exclusive lock] COMMIT {}[Release ALL locks at commit] ─────────────────────────────────────────────────────────────────────────────OBSERVATIONS: 1. T₁ was blocked when trying to read Y (held by uncommitted T₂) → No dirty reads (cascadeless) 2. No write could happen to X while T₁ held it uncommitted → No write-after-uncommitted-write (strict) 3. If T₁ had aborted, X would simply restore to its before-image → Simple recovery 4. Neither transaction needed to check what the other was doing → Decoupled, simple logicWhen you use SERIALIZABLE isolation in most databases with locking (SQL Server, MySQL with traditional locking), you're likely using Strict 2PL. This gives you both serializable schedules AND simple recovery properties.
Multi-Version Concurrency Control (MVCC) systems like PostgreSQL and Oracle handle strictness differently than lock-based systems. Understanding this is important because MVCC is the dominant concurrency control mechanism today.
MVCC Read Behavior:
MVCC Write Behavior:
| Database | Read Strictness | Write Strictness | Mechanism |
|---|---|---|---|
| PostgreSQL | Cascade less (snapshot) | Strict (row locks) | MVCC + Exclusive row locks for writes |
| Oracle | Cascadeless (read consistency) | Strict (row locks) | MVCC with undo segments + row locks |
| MySQL InnoDB | Cascadeless (MVCC) | Strict (row locks) | Clustered index + row locks |
| SQL Server (RCSI) | Cascadeless (row versions) | Strict (row locks) | Version store + row locks |
The MVCC + Locking Hybrid:
Most MVCC systems use a hybrid approach:
This provides:
Example: PostgreSQL Write Behavior
123456789101112131415161718192021222324252627282930313233
-- Demonstrating write strictness in PostgreSQL -- Session 1: Start transaction and updateBEGIN;UPDATE accounts SET balance = 1000 WHERE id = 1;-- Row is now locked by Session 1 -- Session 2: Try to update the same rowBEGIN;UPDATE accounts SET balance = 2000 WHERE id = 1;-- Session 2 BLOCKS here, waiting for Session 1's exclusive lock -- This blocking enforces strictness:-- Session 2 cannot write to data that Session 1 has written-- until Session 1 commits or aborts -- Session 1: CommitCOMMIT;-- Now Session 2's UPDATE proceeds -- Session 2's before-image will be Session 1's COMMITTED value (1000)-- Not Session 1's uncommitted value from earlier-- This makes recovery straightforward -- If Session 2 later aborts:ROLLBACK;-- Simply restore before-image (1000), which is a committed value-- Recovery is trivially correct -- Note: Reads are not blocked (MVCC)-- Session 2 could READ accounts WHERE id = 1 during Session 1's transaction-- It would see the committed value before Session 1's update-- This is cascadeless (not dirty read) but not blocking like writesMVCC reads are cascadeless but not strict (they happen while the writer is active). However, MVCC writes are strict (blocked until previous writer commits). Since the recovery concern is primarily about writes (before-images for undo), MVCC systems achieve the practical benefits of strictness for recovery purposes.
Strict schedules come with performance trade-offs. Understanding these helps in making informed decisions about database configuration and workload design.
Primary Performance Impact: Write Blocking
In strict schedules, transactions writing to the same data items must serialize:
The Lock Duration Problem:
With Strict 2PL, exclusive locks are held from acquisition until transaction completion. For long-running transactions, this can:
| Property | Impact on Contention | Impact on Recovery | Typical Use Case |
|---|---|---|---|
| Recoverable only | Lowest blocking | Complex recovery | Rarely used (risky) |
| Cascadeless | Read blocking only | Moderate recovery | READ UNCOMMITTED (rare) |
| Strict | Read + write blocking | Simple recovery | Most production systems |
Mitigation Strategies for Strict Schedule Performance:
Most applications benefit from strict schedules despite the contention cost. The simplicity of recovery, predictability of behavior, and elimination of subtle bugs outweighs the performance cost for typical OLTP workloads. High-performance systems achieve scaling through sharding and replication rather than weakening consistency.
There's an even stronger schedule property called rigorous schedules. Understanding the distinction helps complete the picture of recoverability hierarchy.
Definition of Rigorous Schedule:
A schedule is rigorous if no transaction Tⱼ can read or write a data item X until every transaction Tᵢ that has read or written X has committed or aborted.
The difference from strict:
Rigorous schedules are even more restrictive—they also wait for readers to finish before allowing new operations on data.
| Aspect | Strict Schedule | Rigorous Schedule |
|---|---|---|
| Blocks write after uncommitted read? | No | Yes |
| Blocks write after uncommitted write? | Yes | Yes |
| Blocks read after uncommitted write? | Yes | Yes |
| Blocks read after uncommitted read? | No | No (reading doesn't modify) |
| Lock equivalent | Exclusive locks held to commit | All locks held to commit |
| Implementation | Strict 2PL | Rigorous 2PL |
| Practical usage | Very common | Rare (overly restrictive) |
Why rigorous schedules are rarely used:
The additional restriction (blocking writes after uncommitted reads) provides minimal benefit:
For recovery: The key benefit of strictness is reliable before-images for writes. Reads don't create before-images, so protecting reads from overwrites during their transaction doesn't help recovery.
For concurrency: Holding read locks until commit significantly reduces concurrency. A long-running read query would block all writers to that data.
For correctness: Strict schedules already provide all the correctness properties needed—serializability and simple recovery.
The bottom line: Strict schedules hit the sweet spot—simple recovery properties with reasonable concurrency. Rigorous schedules add restrictions that cost concurrency without providing commensurate benefits.
MVCC systems typically don't match either strict or rigorous definitions exactly because reads don't block and don't take traditional locks. However, they achieve equivalent recovery properties through version management and write locks.
Strict schedules provide the strongest practical recoverability guarantees, enabling simple and reliable database recovery. Let's consolidate the key concepts:
What's next:
We've explored the complete hierarchy of recoverability properties: recoverable, cascadeless, and strict schedules. The final page brings everything together, examining the recovery implications of these different schedule types and how databases design their recovery systems around these properties.
You now understand strict schedules—the property that enables simple, reliable database recovery by ensuring before-images are always committed values. You've learned how Strict 2PL and MVCC implement these guarantees, and the performance considerations involved. Next, we'll explore the broader recovery implications of these schedule properties.