Introduction
This is the third part of a series concerning Cachalot
DB. The first part can be found here and the second one here.
The most important thing to understand about two stage transactions is when you really need them.
Most of the time, you don’t.
An operation that involves one single object (Put
, TryAdd
, UpdateIf
, Delete
) is always transactional.
It is durable (operations are synchronously written to an append-only transaction log), and it is atomic. An object will be visible to the rest of the world only fully updated or fully inserted.
On a single-node cluster, operations on multiple objects (PutMany
, DeleteMany
) are also transactional.
You need two stage transactions only if you must transactionally manipulate multiple objects on a multi-node cluster.
As usual, let’s build a small example: a toy banking system that allows money to be transferred between accounts. There are two types of business objects: Account
and AccountOperation
.
public class Account
{
[PrimaryKey(KeyDataType.IntKey)]
public int Id { get; set; }
[Index(KeyDataType.IntKey, true)]
public decimal Balance { get; set; }
}
public class AccountOperation
{
[PrimaryKey(KeyDataType.IntKey)]
public int Id { get; set; }
[Index(KeyDataType.IntKey)]
public int SourceAccount { get; set; }
[Index(KeyDataType.IntKey)]
public int TargetAccount { get; set; }
[Index(KeyDataType.IntKey, ordered:true)]
public DateTime Timestamp { get; set; }
public decimal TransferedAmount { get; set; }
}
Let’s create two accounts. No need for transactions at this stage.
var accountIds = connector.GenerateUniqueIds("account_id", 2);
var accounts = connector.DataSource<Account>();
var account1 = new Account {Id = accountIds[0], Balance = 100};
var account2 = new Account {Id = accountIds[1], Balance = 100};
accounts.Put(account1);
accounts.Put(account2);
When we transfer money between the accounts, we would like to simultaneously (atomically) update the balance of both accounts and to create a new instance of AccountOperation
.
This is how the business logic could be implemented:
private static void MoneyTransfer
(Connector connector, Account sourceAccount, Account targetAccount, decimal amount)
{
sourceAccount.Balance -= amount;
targetAccount.Balance += amount;
var tids = connector.GenerateUniqueIds("transaction_id", 1);
var transfer = new AccountOperation
{
Id = tids[0],
SourceAccount = sourceAccount.Id,
TargetAccount = targetAccount.Id,
TransferedAmount = amount
};
var transaction = connector.BeginTransaction();
transaction.Put(sourceAccount);
transaction.Put(targetAccount);
transaction.Put(transfer);
transaction.Commit();
}
The operations allowed inside a transaction are:
If a conditional update (UpdateIf
) is used and the condition is not satisfied by one object, the whole transaction is rolled back.
In some cases, especially if the quantity of data is bounded and it can be stored on a single node, you can instantiate a Cachalot server directly inside your server process. This will give blazing fast responses as there is no more network latency involved.
In order to do this, pass an empty client configuration to the Connector
constructor. A database server will be instantiated inside the connector
object and communications will be done by simple in-process calls, not a TCP channel.
var connector = new Connector(new ClientConfig());
Connector
implements IDisposable
. Disposing the Connector
will graciously stop the server. You need to instantiate the Connector
once when the server process starts and dispose it once when the server process stops.
The fully open source code is available at:
https://github.com/usinesoft/Cachalot
Precompiled binaries (including demo clients) and full documentation are available at:
https://github.com/usinesoft/Cachalot/releases/latest
The client code is available as nuget package at nuget.org.
To install: Install-Package Cachalot.Client