Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TXN 607: Transaction Scheduler

TXN 404 described the data that a transaction leaves in the write, default, and lock column families. This chapter follows how TiKV produces those changes for one transaction command.

TiDB groups a transaction's mutations by Region and sends a separate request to each Region. Inside TiKV, the transaction scheduler handles each request: it reads the current transaction state, decides whether the request can proceed, and sends the resulting key-value changes to Raftstore.

Suppose a transaction starts at start_ts = t0 and transfers 10 units from Alice to Bob:

account:alice:balance -> 90
account:bob:balance   -> 110

Assume that the two keys belong to different Regions. TiDB sends one prewrite request to each Region. This chapter follows the scheduler handling Alice's key.

A successful prewrite means that this Region has checked the key and recorded the transaction's intent to write it. It does not mean the whole transaction has committed. TiDB must still commit the primary key before the transaction gets a final outcome.

Latches Come First

Before the scheduler reads or writes Alice's key, it takes an in-memory latch for that key. A latch briefly serializes conflicting commands on the same TiKV store.

TiKV maps keys to hash slots. For a command that affects several keys, it acquires their latches in a fixed order. If another command already holds a conflicting latch, the later command waits:

keys in one command
      |
      v
hash each key
      |
      v
acquire latches in order
      |
      v
read MVCC state and prepare mutations

The scheduler keeps these latches while it processes the command and sends its write through Raftstore. It releases them only after that write finishes.

A latch is different from a transaction lock. A latch lives only in memory and protects one local command. A transaction lock is persisted in the lock CF and represents a transaction that remains unfinished between requests.

Checking the Transaction State

With the latch held, the scheduler obtains a consistent snapshot through Raftstore and reads the transaction state for the key. A prewrite cannot blindly add a new transaction lock. It must answer two questions:

  1. Is another transaction still working on this key?
  2. Did another transaction commit a newer version after this transaction started?

The first answer comes from the lock CF. A lock means an earlier transaction has prewritten the key but has not finished. The new prewrite cannot write over it.

The second answer comes from the write CF. It contains committed versions and their commit timestamps.

initial account:alice:balance: 100

Transaction A: start_ts = 10, plans to write account:alice:balance = 90
Transaction B: start_ts = 12, plans to write account:alice:balance = 80

Transaction B commits at commit_ts = 15
Transaction A prewrites and sees commit_ts 15 > start_ts 10

Transaction A must abort. If it wrote 90 afterward, it would overwrite B's change using an older view of the balance. Snapshot isolation still needs this write-conflict check to prevent a lost update.

Preparing the Prewrite

After the command holds its latches, it reads the lock CF and write CF, performs conflict checks, and prepares the changes for Raftstore.

For a successful prewrite, the important output is:

lock CF
  key -> { start_ts, primary, ttl, ... }

default CF
  (key, start_ts) -> value

The scheduler packages these modifications into an async_write request. Raftstore replicates and commits the write before it becomes durable Region state, as described in RAFTSTORE 501. The scheduler then releases the latch and returns the prewrite result.

There is still no new record in the write CF. The later commit command removes the transaction lock and creates that committed MVCC record.

Optimistic and Pessimistic Locking

The path above is optimistic: the transaction discovers a conflict when it prewrites. It may have already done substantial work before learning that it must retry.

A pessimistic transaction takes persistent locks earlier. Other writers then wait or fail before they can create a conflicting write. This reduces late rollbacks when several transactions are likely to update the same keys.

Both modes still use the same basic pieces: latches serialize local command execution, while persisted locks coordinate transactions across requests and Regions.

Resolving Unfinished Transactions

TiDB may disappear after prewrite and leave locks behind. In this classic 2PC path, a later read or write that encounters one cannot tell from the secondary key whether the transaction eventually committed. Each lock therefore records the transaction's primary key and a time-to-live value, or ttl.

The later request uses CheckTxnStatus to inspect the primary key, then uses ResolveLock to bring related locks to the same result:

check the primary key
       |
       +-- committed -> commit the related locks
       |
       +-- rolled back -> roll back the related locks
       |
       +-- still pending -> wait or retry

If the primary is still locked but its TTL has expired, CheckTxnStatus can roll it back before the related locks are resolved. This lets the transaction reach a final state even when the original TiDB coordinator is gone.

Long-lived locks cause more of this resolution work because later requests need to inspect the primary instead of proceeding immediately.

The Scheduler Pipeline

The transaction scheduler turns a Region-scoped command into reads and writes in this order:

receive command
      |
      v
acquire latches
      |
      v
obtain a snapshot
      |
      v
read MVCC records and prepare modifications
      |
      v
async_write through Raftstore
      |
      v
release latches and return the result

The scheduler executes the transaction command and its MVCC reads. This is different from the read pool, which handles standalone client reads such as Get, BatchGet, and coprocessor requests. The read pool does not own latches or the transaction protocol.


This chapter covers the classic 2PC path. TXN 708 introduces how Async Commit and 1PC shorten that path.