lucaong / cubdb

Elixir embedded key/value database
Apache License 2.0
556 stars 23 forks source link

Atomic transactions with arbitrary operations #53

Closed lucaong closed 2 years ago

lucaong commented 2 years ago

Transactions are created with CubDB.transaction/2, and are used to perform arbitrary write operations atomically, using functions in CubDB.Tx.

Transactions block writers, but do not block concurrent readers.

Example:

Suppose the keys :a and :b map to balances, and we want to transfer 5 from :a to :b, if :a has enough balance:

CubDB.transaction(db, fn tx ->
  a = CubDB.Tx.get(tx, :a)
  b = CubDB.Tx.get(tx, :b)

  if a >= 5 do
    tx = CubDB.Tx.put(tx, :a, a - 5)
    tx = CubDB.Tx.put(tx, :b, b + 5)
    {:commit, tx, :ok}
  else
    {:cancel, :insufficient_balance}
  end
end)

The read functions in CubDB.Tx read the in-transaction state, as opposed to the live database state, so they see writes performed inside the transaction even before they are committed:

# Assuming we start from an empty database
CubDB.transaction(db, fn tx ->
  tx = CubDB.Tx.put(tx, :a, 123)

  # CubDB.Tx.get sees the in-transaction value
  CubDB.Tx.get(tx, :a)
  # => 123

  # CubDB.get instead does not see the uncommitted write
  CubDB.get(db, :a)
  # => nil

  {:commit, tx, nil}
end)

# After the transaction is committed, CubDB.get sees the write
CubDB.get(db, :a)
# => 123