Optimistic Concurrency Control (OCC)

Distributed Systems advanced Building with AI 17 min read

Overview

Optimistic Concurrency Control (OCC) is a transaction management strategy that assumes conflicts between concurrent transactions are rare. Instead of acquiring locks before reading or writing data, transactions proceed freely, recording which versions they read. At commit time, the system validates that none of the read versions have been overwritten by another transaction. If validation succeeds, the transaction commits atomically; if it fails, the transaction aborts and the client retries.

OCC is the natural choice for distributed databases where lock-based approaches would require expensive cross-node lock coordination. In these systems, a transaction’s read set and write set are collected locally on the client or coordinator, and validation happens at commit time through a lightweight protocol that checks version stamps against the current state of each shard.

The tradeoff is clear: OCC eliminates lock contention and deadlocks entirely, but under high contention the abort rate climbs and wasted work increases. Understanding when OCC shines (read-heavy, low-contention OLTP) versus when it struggles (hot-key update patterns) is critical for choosing the right concurrency model for each workload.

Core Concepts

Key Concept

OCC’s three-phase model (read, validate, write) means transactions never block each other during execution. Conflicts are detected only at commit time, making OCC lock-free and deadlock-free by design.

Key Concept

The read set is the transaction’s “bet” that the world hasn’t changed. Validation checks this bet. The smaller the read set and the lower the contention, the more likely the bet pays off.

How It Works

Version-Based Validation

Each row or key in the database carries a version stamp — typically an HLC timestamp or a monotonic sequence number. When a transaction reads key K, it records (K, version_read). At commit time, the validator checks: for every (K, v) in the read set, is the current version of K still v? If yes for all keys, the transaction is serializable. If any key’s version has advanced, a concurrent transaction modified it, and this transaction must abort.

Key Concept

Version-based validation is equivalent to checking that the transaction’s snapshot is still consistent. It provides serializable isolation without read locks because any conflicting write would have bumped the version.

Distributed OCC with Two-Phase Commit

In a distributed setting, the coordinator sends the read set and write set to each involved shard during the prepare phase of 2PC. Each shard independently validates its local portion of the read set and acquires short-lived write locks on the write set. If all shards vote “prepared,” the coordinator sends commit. If any shard detects a conflict, it votes abort and the entire transaction rolls back.

Write Skew and Phantom Prevention

Basic OCC validates individual key versions but can miss write skew anomalies where two transactions read overlapping keys and write disjoint keys. To prevent this, advanced OCC implementations validate predicate reads (range scans) by checking that no new keys matching the predicate have been inserted. This is often implemented via gap locks or predicate versioning in the validation phase.

Timestamp Ordering Optimization

Some OCC implementations assign a tentative commit timestamp at the start of validation and check that all read versions are visible at that timestamp. This turns validation into a single timestamp comparison per key, which is efficient when combined with MVCC storage that indexes versions by timestamp.

Practical Applications

Implementation Patterns

Code Examples

Go: OCC Transaction Lifecycle

type Transaction struct {
    ID       uint64
    ReadSet  map[string]uint64 // key -> version read
    WriteSet map[string][]byte // key -> buffered value
    StartTS  Timestamp
}

func (tx *Transaction) Get(store *Store, key string) ([]byte, error) {
    val, version, err := store.ReadAt(key, tx.StartTS)
    if err != nil {
        return nil, err
    }
    tx.ReadSet[key] = version
    // Return buffered write if we already wrote this key
    if buf, ok := tx.WriteSet[key]; ok {
        return buf, nil
    }
    return val, nil
}

func (tx *Transaction) Put(key string, value []byte) {
    tx.WriteSet[key] = value
}

func (tx *Transaction) Commit(store *Store) error {
    // Validation phase: check all read versions are still current
    for key, readVersion := range tx.ReadSet {
        currentVersion, err := store.CurrentVersion(key)
        if err != nil {
            return fmt.Errorf("validation lookup: %w", err)
        }
        if currentVersion != readVersion {
            return ErrTransactionConflict
        }
    }

    // Write phase: apply all buffered writes atomically
    commitTS := store.hlc.Now()
    for key, value := range tx.WriteSet {
        store.WriteAt(key, value, commitTS)
    }
    return nil
}

Go: Retry Loop with Exponential Backoff

func RunTransaction(store *Store, fn func(tx *Transaction) error) error {
    const maxRetries = 5
    for attempt := 0; attempt < maxRetries; attempt++ {
        tx := store.Begin()
        if err := fn(tx); err != nil {
            return err // application error, don't retry
        }
        if err := tx.Commit(store); err != nil {
            if errors.Is(err, ErrTransactionConflict) {
                // Backoff: 2^attempt * 10ms + jitter
                base := time.Duration(1<<attempt) * 10 * time.Millisecond
                jitter := time.Duration(rand.Int63n(int64(base / 2)))
                time.Sleep(base + jitter)
                continue
            }
            return err // non-conflict error
        }
        return nil // success
    }
    return fmt.Errorf("transaction failed after %d retries", maxRetries)
}

Common Pitfalls

Key Concepts Recap

Further Reading