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.
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.
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.
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.
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.
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.
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.
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.
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
}
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)
}