Optimistic concurrency control (OCC)
Below which conflict probability does optimism win, and how does it fail above it?
The constraint is economic
If conflicts are rare, a lock manager is a cost paid on every operation to protect against something that almost never happens. Put numbers on it.
- Pessimistic (locking) cost ≈ k × c_lock per transaction. With k = 10 items and c_lock ≈ 150 ns: 10 × 150 ns = 1.5 µs.
- Optimistic cost ≈ p_conflict × (the transaction's entire work), because a conflicting transaction must redo everything. For 100 µs of work, the two costs are equal when p × 100 µs = 1.5 µs, so p ≈ 1.5 / 100 = 1.5%.
- items per transaction
- cost of one lock acquisition
- chance a transaction must redo its work
Data table
| Conflict probability p | series | Cost per transaction |
|---|---|---|
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.001 | locking | 1.5 µs |
| 0.002 | locking | 1.5 µs |
| 0.002 | locking | 1.5 µs |
| 0.002 | locking | 1.5 µs |
| 0.002 | locking | 1.5 µs |
| 0.002 | locking | 1.5 µs |
| 0.003 | locking | 1.5 µs |
| 0.003 | locking | 1.5 µs |
| 0.003 | locking | 1.5 µs |
| 0.003 | locking | 1.5 µs |
| 0.004 | locking | 1.5 µs |
| 0.004 | locking | 1.5 µs |
| 0.004 | locking | 1.5 µs |
| 0.005 | locking | 1.5 µs |
| 0.005 | locking | 1.5 µs |
| 0.006 | locking | 1.5 µs |
| 0.007 | locking | 1.5 µs |
| 0.007 | locking | 1.5 µs |
| 0.008 | locking | 1.5 µs |
| 0.009 | locking | 1.5 µs |
| 0.01 | locking | 1.5 µs |
| 0.011 | locking | 1.5 µs |
| 0.012 | locking | 1.5 µs |
| 0.013 | locking | 1.5 µs |
| 0.014 | locking | 1.5 µs |
| 0.015 | locking | 1.5 µs |
| 0.017 | locking | 1.5 µs |
| 0.019 | locking | 1.5 µs |
| 0.021 | locking | 1.5 µs |
| 0.023 | locking | 1.5 µs |
| 0.025 | locking | 1.5 µs |
| 0.027 | locking | 1.5 µs |
| 0.03 | locking | 1.5 µs |
| 0.033 | locking | 1.5 µs |
| 0.036 | locking | 1.5 µs |
| 0.04 | locking | 1.5 µs |
| 0.044 | locking | 1.5 µs |
| 0.048 | locking | 1.5 µs |
| 0.053 | locking | 1.5 µs |
| 0.058 | locking | 1.5 µs |
| 0.064 | locking | 1.5 µs |
| 0.07 | locking | 1.5 µs |
| 0.077 | locking | 1.5 µs |
| 0.085 | locking | 1.5 µs |
| 0.094 | locking | 1.5 µs |
| 0.103 | locking | 1.5 µs |
| 0.113 | locking | 1.5 µs |
| 0.124 | locking | 1.5 µs |
| 0.137 | locking | 1.5 µs |
| 0.15 | locking | 1.5 µs |
| 0.165 | locking | 1.5 µs |
| 0.182 | locking | 1.5 µs |
| 0.2 | locking | 1.5 µs |
| 0 | optimistic | 0.01 µs |
| 0 | optimistic | 0.011 µs |
| 0 | optimistic | 0.012 µs |
| 0 | optimistic | 0.013 µs |
| 0 | optimistic | 0.015 µs |
| 0 | optimistic | 0.016 µs |
| 0 | optimistic | 0.018 µs |
| 0 | optimistic | 0.019 µs |
| 0 | optimistic | 0.021 µs |
| 0 | optimistic | 0.024 µs |
| 0 | optimistic | 0.026 µs |
| 0 | optimistic | 0.028 µs |
| 0 | optimistic | 0.031 µs |
| 0 | optimistic | 0.034 µs |
| 0 | optimistic | 0.038 µs |
| 0 | optimistic | 0.042 µs |
| 0 | optimistic | 0.046 µs |
| 0.001 | optimistic | 0.05 µs |
| 0.001 | optimistic | 0.055 µs |
| 0.001 | optimistic | 0.061 µs |
| 0.001 | optimistic | 0.067 µs |
| 0.001 | optimistic | 0.074 µs |
| 0.001 | optimistic | 0.081 µs |
| 0.001 | optimistic | 0.089 µs |
| 0.001 | optimistic | 0.098 µs |
| 0.001 | optimistic | 0.108 µs |
| 0.001 | optimistic | 0.118 µs |
| 0.001 | optimistic | 0.13 µs |
| 0.001 | optimistic | 0.143 µs |
| 0.002 | optimistic | 0.157 µs |
| 0.002 | optimistic | 0.173 µs |
| 0.002 | optimistic | 0.19 µs |
| 0.002 | optimistic | 0.209 µs |
| 0.002 | optimistic | 0.23 µs |
| 0.003 | optimistic | 0.253 µs |
| 0.003 | optimistic | 0.278 µs |
| 0.003 | optimistic | 0.306 µs |
| 0.003 | optimistic | 0.336 µs |
| 0.004 | optimistic | 0.37 µs |
| 0.004 | optimistic | 0.407 µs |
| 0.004 | optimistic | 0.447 µs |
| 0.005 | optimistic | 0.492 µs |
| 0.005 | optimistic | 0.541 µs |
| 0.006 | optimistic | 0.595 µs |
| 0.007 | optimistic | 0.654 µs |
| 0.007 | optimistic | 0.719 µs |
| 0.008 | optimistic | 0.791 µs |
| 0.009 | optimistic | 0.87 µs |
| 0.01 | optimistic | 0.956 µs |
| 0.011 | optimistic | 1.05 µs |
| 0.012 | optimistic | 1.16 µs |
| 0.013 | optimistic | 1.27 µs |
| 0.014 | optimistic | 1.4 µs |
| 0.015 | optimistic | 1.54 µs |
| 0.017 | optimistic | 1.69 µs |
| 0.019 | optimistic | 1.86 µs |
| 0.021 | optimistic | 2.05 µs |
| 0.023 | optimistic | 2.25 µs |
| 0.025 | optimistic | 2.47 µs |
| 0.027 | optimistic | 2.72 µs |
| 0.03 | optimistic | 2.99 µs |
| 0.033 | optimistic | 3.29 µs |
| 0.036 | optimistic | 3.62 µs |
| 0.04 | optimistic | 3.98 µs |
| 0.044 | optimistic | 4.37 µs |
| 0.048 | optimistic | 4.81 µs |
| 0.053 | optimistic | 5.29 µs |
| 0.058 | optimistic | 5.82 µs |
| 0.064 | optimistic | 6.4 µs |
| 0.07 | optimistic | 7.03 µs |
| 0.077 | optimistic | 7.73 µs |
| 0.085 | optimistic | 8.5 µs |
| 0.094 | optimistic | 9.35 µs |
| 0.103 | optimistic | 10.3 µs |
| 0.113 | optimistic | 11.3 µs |
| 0.124 | optimistic | 12.4 µs |
| 0.137 | optimistic | 13.7 µs |
| 0.15 | optimistic | 15 µs |
| 0.165 | optimistic | 16.5 µs |
| 0.182 | optimistic | 18.2 µs |
| 0.2 | optimistic | 20 µs |
Three phases
- Read into a private workspace (nothing is visible to others yet).
- Validate: did anything I read change since I read it?
- Write: make the changes visible.
There are two directions for validation:
- Backward validation checks my read set against the write sets of transactions that committed since I started.
- Forward validation checks my write set against the read sets of transactions that are still running. Its useful property: you can choose to abort the other transaction. That is the hook by which priority and starvation avoidance are implemented.
The structural cost: validation is itself a critical section
Check-then-commit must be atomic with respect to other validations. Otherwise two transactions could each validate against a state that neither ends up in. So the scheme that abolished locking has created one global lock, and Amdahl's Law (the speedup limit set by the serial part of a job) now sets its ceiling at the reciprocal of that critical section's length.
Modern designs spend most of their engineering here. Silo (a research in-memory database) replaces the global section with per-record transaction IDs plus epoch-based group commit. This is a fixed per-commit cost deriving a natural batch size, and the batch is an epoch (a short period of time, for example 40 ms, in which commits are grouped).
How OCC fails
The failure mode is different from 2PL's.
- 2PL degrades into waiting: utilization drops, queues grow, and the system is visibly stuck.
- OCC degrades into burning: every core is at 100%, goodput (useful completed work) is near zero, because everyone aborts everyone and nobody finishes.
An aborted transaction also used a concurrency slot for its whole duration, so by Little's Law it consumed real capacity to produce nothing. If you diagnose this in production, the tell is high CPU with a falling commit rate. The metric you need is goodput, not throughput.
OCC starves the large
A long transaction's conflict window is proportional to its length, so under contention it will essentially never commit, while 2PL's first-come-first-served wait queues starve nobody. The standard fix is to fall back to pessimistic execution after N aborts. That is an admission that the scheme has one workload it likes and needs an escape for the other.
Optimism trades a certain small cost on every transaction for an uncertain total loss on a few; the crossover is the conflict probability, and past it the failure is silent because the CPUs stay busy.
A code sketch: optimistic update with a version column
Application code can use OCC even on a database that locks. Keep a version number in the row and make the update conditional on it:
-- 1. Read, remember the version.
SELECT balance, version FROM wallets WHERE owner = 'anna'; -- balance=100, version=7
-- 2. Compute in the application, then write only if nobody changed the row.
UPDATE wallets
SET balance = 70, version = version + 1
WHERE owner = 'anna' AND version = 7;
-- If this reports 0 rows updated, someone else got there first:
-- re-read and try again (the retry must include the read).