Optimistic Transactions

View Source

Optimistic Transactions provide light-weight optimistic concurrency control for workloads that do not expect high contention between multiple transactions.

Optimistic vs Pessimistic Transactions

FeatureOptimisticPessimistic
LockingValidation at commitLock on write/GetForUpdate
Conflict handlingRetry on commit failureBlock/timeout on lock acquisition
Best forLow contention workloadsHigh contention workloads
DeadlockN/ADetection & timeout
PerformanceBetter for read-heavyBetter for write-heavy

Choose Optimistic Transactions when:

  • Write conflicts are rare
  • You have many non-transactional writes alongside transactions
  • You prefer retry logic over blocking

Choose Pessimistic Transactions when:

  • Multiple transactions frequently update the same keys
  • You need strict locking guarantees
  • You want automatic deadlock detection

Opening an Optimistic Transaction Database

%% Basic open
{ok, Db, [DefaultCF]} = rocksdb:open_optimistic_transaction_db(
    "my_db",
    [{create_if_missing, true}],
    [{"default", []}]
).

%% With column families
CfOpts = [],
{ok, Db, [DefaultCF, DataCF]} = rocksdb:open_optimistic_transaction_db(
    "my_db",
    [{create_if_missing, true}],
    [{"default", CfOpts}, {"data", CfOpts}]
).

Basic Operations

Creating a Transaction

%% Create a transaction with default options
{ok, Txn} = rocksdb:transaction(Db, []).

%% Create with write options
WriteOpts = [{sync, true}],
{ok, Txn} = rocksdb:transaction(Db, WriteOpts).

Put, Get, Delete

{ok, Txn} = rocksdb:transaction(Db, []),

%% Put a key-value pair
ok = rocksdb:transaction_put(Txn, <<"key1">>, <<"value1">>),

%% Get a value
{ok, Value} = rocksdb:transaction_get(Txn, <<"key1">>, []),

%% Delete a key
ok = rocksdb:transaction_delete(Txn, <<"key1">>),

%% Commit the transaction
ok = rocksdb:transaction_commit(Txn).

Column Family Support

All operations support column families:

{ok, Txn} = rocksdb:transaction(Db, []),

%% Operations with column family
ok = rocksdb:transaction_put(Txn, CfHandle, <<"key">>, <<"value">>),
{ok, Value} = rocksdb:transaction_get(Txn, CfHandle, <<"key">>, []),
ok = rocksdb:transaction_delete(Txn, CfHandle, <<"key">>),

ok = rocksdb:transaction_commit(Txn).

Reading Uncommitted Data

Transactions can read their own uncommitted changes:

%% Write to database first
ok = rocksdb:put(Db, <<"a">>, <<"old_a">>, []),
ok = rocksdb:put(Db, <<"b">>, <<"old_b">>, []),

%% Start transaction
{ok, Txn} = rocksdb:transaction(Db, []),

%% Modify within transaction
ok = rocksdb:transaction_put(Txn, <<"a">>, <<"new_a">>),

%% Read sees uncommitted change
{ok, <<"new_a">>} = rocksdb:transaction_get(Txn, <<"a">>, []),

%% Read sees committed value for unmodified key
{ok, <<"old_b">>} = rocksdb:transaction_get(Txn, <<"b">>, []),

ok = rocksdb:transaction_commit(Txn).

Iterators

Create an iterator that sees uncommitted changes in the transaction:

{ok, Txn} = rocksdb:transaction(Db, []),

%% Add uncommitted data
ok = rocksdb:transaction_put(Txn, <<"key1">>, <<"value1">>),
ok = rocksdb:transaction_put(Txn, <<"key2">>, <<"value2">>),

%% Create iterator - sees both committed and uncommitted data
{ok, Iter} = rocksdb:transaction_iterator(Txn, []),

%% Use standard iterator operations
{ok, Key, Value} = rocksdb:iterator_move(Iter, first),
{ok, NextKey, NextValue} = rocksdb:iterator_move(Iter, next),

ok = rocksdb:iterator_close(Iter),
ok = rocksdb:transaction_commit(Txn).

With column family:

{ok, Iter} = rocksdb:transaction_iterator(Txn, CfHandle, []).

Conflict Detection and Retry

Optimistic transactions validate at commit time. If another transaction modified the same keys, commit will fail:

retry_transaction(Db, Key, UpdateFun) ->
    retry_transaction(Db, Key, UpdateFun, 3).

retry_transaction(_Db, _Key, _UpdateFun, 0) ->
    {error, max_retries};
retry_transaction(Db, Key, UpdateFun, Retries) ->
    {ok, Txn} = rocksdb:transaction(Db, []),
    try
        case rocksdb:transaction_get(Txn, Key, []) of
            {ok, Value} ->
                NewValue = UpdateFun(Value),
                ok = rocksdb:transaction_put(Txn, Key, NewValue),
                case rocksdb:transaction_commit(Txn) of
                    ok -> ok;
                    {error, {busy, _}} ->
                        %% Conflict detected, retry
                        retry_transaction(Db, Key, UpdateFun, Retries - 1)
                end;
            not_found ->
                {error, not_found}
        end
    catch
        _:Error ->
            {error, Error}
    end.

Error Handling

Optimistic transactions can return these errors on commit:

  • {error, {busy, Reason}} - Write conflict detected at commit time
  • {error, {try_again, Reason}} - Transient error, retry the operation
case rocksdb:transaction_commit(Txn) of
    ok ->
        %% Success
        ok;
    {error, {busy, _}} ->
        %% Another transaction modified the same keys
        %% Rollback and retry with fresh data
        handle_conflict();
    {error, Reason} ->
        %% Other error
        {error, Reason}
end.

Complete Example

increment_counter(Db, CounterKey) ->
    {ok, Txn} = rocksdb:transaction(Db, []),
    try
        Value = case rocksdb:transaction_get(Txn, CounterKey, []) of
            {ok, V} -> binary_to_integer(V);
            not_found -> 0
        end,
        NewValue = integer_to_binary(Value + 1),
        ok = rocksdb:transaction_put(Txn, CounterKey, NewValue),
        case rocksdb:transaction_commit(Txn) of
            ok ->
                {ok, Value + 1};
            {error, {busy, _}} ->
                %% Conflict - retry
                increment_counter(Db, CounterKey)
        end
    catch
        _:Error ->
            {error, Error}
    end.