Loading learning content...
In lock-based concurrency control, we prevent conflicts by forcing transactions to wait. But what if we could detect conflicts after they occur and simply abort the offending transaction? This is the fundamental insight behind timestamp-based protocols, and it begins with a deceptively simple question: When was this data item last read?
The Read Timestamp (RTS) is the answer to this question—a metadata field maintained for every data item in the database that records the largest timestamp of any transaction that has successfully read that item. Understanding RTS is essential because it forms half of the conflict detection mechanism that enables timestamp ordering to guarantee serializability without ever blocking a transaction.
By the end of this page, you will understand what read timestamps are, why they're maintained, how they're updated during read operations, and how they enable detection of write-after-read conflicts. You'll see RTS as a critical piece of the timestamp ordering puzzle that we'll assemble across this module.
Every data item in a timestamp-based system maintains metadata about when it was last accessed. The Read Timestamp (RTS) of a data item X, denoted RTS(X), stores the largest timestamp of any transaction that has successfully read X.
Consider what this means: if RTS(X) = 100, we know that some transaction with timestamp 100 has read X, and no transaction with a timestamp larger than 100 has read X since the current value was written. This seemingly simple piece of information has profound implications for conflict detection.
Remember that each transaction receives a unique timestamp TS(T) when it begins, typically from a system clock or logical counter. This timestamp represents the transaction's position in the serialization order—transactions with smaller timestamps are logically "older" and should appear earlier in any equivalent serial schedule.
Formal Definition:
For any data item X in the database:
RTS(X) = max{TS(Ti) | Ti has successfully read X}
This definition captures the essential property: RTS tracks the "youngest" (highest timestamp) transaction that has read the current value of the data item. When a new transaction T reads X:
The read timestamp is initialized when a data item is first written—typically set to the write timestamp of the creating transaction.
| Property | Description | Significance |
|---|---|---|
| Monotonically Increasing | RTS(X) never decreases | Once a young transaction reads, older writes are violations |
| Per-Item Granularity | Each data item has its own RTS | Enables fine-grained conflict detection |
| Updated on Successful Reads | Only successful reads update RTS | Failed/aborted reads don't affect protocol |
| Bounds Future Writes | RTS(X) constrains which transactions can write X | Prevents temporal violations |
The read timestamp serves a specific and critical purpose: detecting write-after-read conflicts that would violate serializability. To understand why, we must think about what it means for a schedule to be serializable.
In timestamp ordering, we're trying to ensure that the schedule is equivalent to running all transactions in timestamp order—T1 before T2 if TS(T1) < TS(T2). Now consider what happens if:
In a serial schedule respecting timestamp order, T1 would execute completely before T2 starts. T2 would read the value written by T1. But in our actual execution, T2 has already read X—potentially an old value that T1 hasn't yet updated. This is a temporal violation: T2 has read a value that, in the equivalent serial schedule, wouldn't exist yet.
When an older transaction (smaller timestamp) tries to write a data item that a younger transaction (larger timestamp) has already read, we have a non-serializable situation. The younger transaction has seen a "future" value that will be overwritten by the older transaction's write. RTS(X) lets us detect exactly this scenario.
How RTS Enables Detection:
When transaction T wants to write data item X, the protocol checks:
If TS(T) < RTS(X), then T is trying to write a value that a younger transaction has already read.
This is precisely the write-after-read violation described above. The transaction T must be aborted and restarted with a new (larger) timestamp, giving it a chance to execute without violating the intended serialization order.
Let's trace through a concrete example:
| Time | Transaction | Operation | RTS(X) | Outcome |
|---|---|---|---|---|
| t1 | — | Initial state | 0 | X has never been read |
| t2 | T2 (TS=20) | Read(X) | 20 | RTS(X) updated to 20 |
| t3 | T1 (TS=10) | Write(X) | 20 | TS(T1)=10 < RTS(X)=20 → ABORT T1 |
| t4 | T1' (TS=25) | Write(X) | 20 | TS(T1')=25 > RTS(X)=20 → OK |
In this example, T1's original attempt to write X fails because T2 (a younger transaction) has already read X. When T1 restarts as T1' with a new timestamp of 25, it becomes younger than T2, and the write is allowed—the serialization order is now T2 before T1', which is consistent with T2 reading before T1' writes.
Understanding exactly when and how RTS is updated is essential for correctly implementing and reasoning about timestamp protocols. The mechanics are straightforward but have important nuances.
The RTS Update Rule:
When transaction T successfully reads data item X:
if TS(T) > RTS(X) then
RTS(X) := TS(T)
end if
Note the key word successfully. If a read operation is rejected (which, as we'll see, can happen in certain cases), the RTS is not updated. Only reads that actually return data to the transaction affect the timestamp.
Implementation Considerations:
In practice, maintaining read timestamps introduces storage overhead—every data item needs an additional field. For tuple-level granularity, this means every row in every table has an RTS field. The update operation must also be atomic, which can create contention on frequently-read items.
Some systems optimize by:
These optimizations trade precision for performance, but must be carefully designed to preserve correctness.
RTS(X) is monotonically non-decreasing: once RTS(X) = k, it can never become less than k without the data item being rewritten. This invariant is what enables reliable conflict detection—if RTS(X) = 50, we're guaranteed that any transaction with TS < 50 cannot safely write X.
Read timestamps don't work in isolation—they're one half of a dual-timestamp system. Each data item maintains both:
Together, these timestamps form a complete picture of how a data item has been accessed, enabling the protocol to detect both write-after-read and read-after-write conflicts.
The Serialization Principle:
The combination of RTS and WTS enforces a fundamental invariant: operations occur in timestamp order. Specifically:
Any violation of these conditions indicates a non-serializable access pattern and triggers an abort. The beauty of this approach is that we never block—we either allow the operation immediately or abort and retry.
This is in stark contrast to locking protocols where transactions wait for locks. Timestamp ordering trades potential blocking for potential aborts, which can be advantageous in systems where most transactions don't conflict and waiting is costly.
Let's work through several examples to solidify understanding of how RTS behaves in various scenarios.
Example 1: Sequential Reads by Increasingly Younger Transactions
| Step | Transaction | Operation | RTS(X) Before | RTS(X) After |
|---|---|---|---|---|
| 1 | T10 (TS=10) | Read(X) | 0 | 10 |
| 2 | T25 (TS=25) | Read(X) | 10 | 25 |
| 3 | T30 (TS=30) | Read(X) | 25 | 30 |
| 4 | T50 (TS=50) | Read(X) | 30 | 50 |
In this simple case, each successive read is by a younger transaction, so RTS is updated each time. After all reads, RTS(X) = 50, indicating that no transaction with TS < 50 can safely write X.
Example 2: Out-of-Order Reads
| Step | Transaction | Operation | RTS(X) Before | RTS(X) After | Notes |
|---|---|---|---|---|---|
| 1 | T50 (TS=50) | Read(X) | 0 | 50 | RTS jumps to 50 |
| 2 | T10 (TS=10) | Read(X) | 50 | 50 | TS(T10)=10 < RTS=50, no update |
| 3 | T30 (TS=30) | Read(X) | 50 | 50 | TS(T30)=30 < RTS=50, no update |
| 4 | T60 (TS=60) | Read(X) | 50 | 60 | TS(T60)=60 > RTS=50, update |
This example shows that once a young transaction reads, RTS "locks in" that high value. Older transactions can still read (reads almost always succeed in basic timestamp ordering), but they don't affect RTS.
Example 3: Write Rejection Due to RTS
| Step | Transaction | Operation | Action | Result |
|---|---|---|---|---|
| 1 | T40 (TS=40) | Read(X) | RTS(X) := 40 | Read succeeds |
| 2 | T20 (TS=20) | Write(X, v) | Check: 20 < RTS(X)=40 | ABORT T20 |
| 3 | T20 restarts as T50 | — | New timestamp assigned | — |
| 4 | T50 (TS=50) | Write(X, v) | Check: 50 ≥ RTS(X)=40 | Write succeeds |
If T20 had already performed other operations before being aborted, those might need to be undone. Additionally, if other transactions read T20's uncommitted writes, they too might need to abort. This is the cascading abort problem, which we'll address when discussing strict timestamp protocols.
Example 4: Same-Timestamp Tie-Breaking
What if two transactions have the same timestamp? In practice, true timestamp collisions are prevented by:
For theoretical analysis, we assume all timestamps are unique. In implementation, the system must guarantee this property.
Maintaining read timestamps has real performance costs that database designers must carefully consider. Unlike write timestamps (which are updated only on writes), RTS is updated on every read—and reads are typically far more common than writes.
The Write Amplification Problem:
Consider a pure read-only analytics query scanning a million rows. In a naive timestamp implementation, this query would:
What should be an I/O-read-only workload becomes half reads, half writes—drastically increasing latency and system load.
Optimization Strategies:
| Strategy | Approach | Trade-off |
|---|---|---|
| Lazy Updates | Batch RTS updates, apply periodically | Reduced precision, may miss conflicts |
| In-Memory RTS | Keep RTS in memory, not on disk | Requires memory, lost on crash |
| Epoch-Based | Update RTS only when epoch changes | Coarser granularity, more aborts |
| Read-Only TXN Optimization | Skip RTS update for read-only transactions | Complex detection, may require declarations |
| Probabilistic | Update RTS randomly or for samples | Trade correctness for performance (rarely used) |
The write amplification problem is one reason pure timestamp ordering is rarely used in practice. MVCC (Multi-Version Concurrency Control) avoids this by maintaining multiple versions of data, allowing reads to access appropriate versions without updating any timestamps. We'll explore MVCC in depth later in this chapter.
We've explored the first critical component of the Basic Timestamp Protocol. Let's consolidate the key concepts:
What's Next:
Now that we understand read timestamps, the next page examines Write Timestamps (WTS)—the complementary mechanism that tracks when data items were last written. Together, RTS and WTS form the foundation upon which the complete timestamp ordering protocol is built.
You now understand the Read Timestamp (RTS) concept—what it is, why it's maintained, how it's updated, and what conflicts it detects. This is the first building block of the Basic Timestamp Protocol. Next, we'll examine its counterpart: the Write Timestamp.