Transaction mode: MVCC | Tarantool
Tarantool
Check out the new release policy
Transactions Transaction mode: MVCC

Transaction mode: MVCC

Since version 2.6.1, Tarantool has another transaction behavior mode that allows “yielding” inside a memtx transaction. This is controlled by the transaction manager.

This mode allows concurrent transactions but may cause conflicts. You can use this mode on the memtx storage engine. The vinyl storage engine also supports MVCC mode, but has a different implementation.

Note

Currently, you cannot use several different storage engines within one transaction.

The transaction manager is designed to isolate concurrent transactions and provides a serializable transaction isolation level. It consists of two parts:

  • MVCC – multi version concurrency control engine, which stores all change actions of all transactions. It also creates the transaction view of the database state and a read view (a fixed state of the database that is never changed by other transactions) when necessary.
  • Conflict manager – a manager that tracks changes to transactions and determines their correctness in the serialization order. The conflict manager declares transactions to be in conflict or sends transactions to read views when necessary.

The transaction manager also provides a non-classical snapshot isolation level – this snapshot is not necessarily tied to the start time of the transaction, like the classical snapshot where a transaction can get a consistent snapshot of the database. The conflict manager decides if and when each transaction gets which snapshot. This avoids some conflicts compared to the classic snapshot isolation approach.

By default, the transaction manager is disabled. Use the memtx_use_mvcc_engine option to enable it via box.cfg.

box.cfg{memtx_use_mvcc_engine = true}

Note

For autocommit transactions (actions with a statement without explicit box.begin/commit calls) there is an obvious rule: read-only transactions (for example, select) are performed with read-confirmed; all others (for example, replace) with read-committed.

The transaction manager has four options for the transaction isolation level that you can set in box-cfg:

  • default.
  • best-effort.
  • read-committed.
  • read-confirmed.

The best-effort option is set by default (or if the level is not specified). This allows MVCC to consider the actions of transactions independently and determine the best isolation level for them. It increases the probability of successful completion of the transaction and helps to avoid possible conflicts.

To set the default isolation level with the other option, for example, to read-committed, use the following command:

box.cfg{default_txn_isolation = 'read-committed'}

If a transaction has an explicit box.begin() call, the level can be specified as follows:

box.begin({tnx_isolation = 'best-effort'})

Note

You can also do this in the net.box stream:begin() method.

Choosing the better option depends on whether you have conflicts or not. If you have many conflicts, you should set the different options or use the default mode.

Create a file init.lua, containing the following:

fiber = require 'fiber'

box.cfg{ listen = '127.0.0.1:3301', memtx_use_mvcc_engine = false }
box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})

tickets = box.schema.create_space('tickets', { if_not_exists = true })
tickets:format({
    { name = "id", type = "number" },
    { name = "place", type = "number" },
})
tickets:create_index('primary', {
    parts = { 'id' },
    if_not_exists = true
})

Connect to the instance:

tarantooctl connect 127.0.0.1:3301

Then try to execute the transaction with yield inside:

box.atomic(function() tickets:replace{1, 429} fiber.yield() tickets:replace{2, 429} end)

You will receive an error message:

---
- error: Transaction has been aborted by a fiber yield
...

Also, if you leave a transaction open while returning from a request, you will get an error message:

127.0.0.1:3301> box.begin()
---
- error: Transaction is active at return from function
...

Change memtx_use_mvcc_engine to true, restart tarantool and try again:

127.0.0.1:3301> box.atomic(function() tickets:replace{1, 429} fiber.yield() tickets:replace{2, 429} end)
---
...

Now check if this transaction was successful:

127.0.0.1:3301> box.space.tickets:select({}, {limit = 10})
---
- - [1, 429]
  - [2, 429]
...

Since v. 2.10.0, IPROTO implements streams and interactive transactions that can be used when memtx_use_mvcc_engine is enabled on the server.

Stream
A stream supports multiplexing several transactions over one connection. Each stream has its own identifier, which is unique within the connection. All requests with the same non-zero stream ID belong to the same stream. All requests in a stream are executed strictly sequentially. This allows the implementation of interactive transactions. If the stream ID of a request is 0, it does not belong to any stream and is processed in the old way.

In net.box, a stream is an object above the connection that has the same methods but allows sequential execution of requests. The ID is automatically generated on the client side. If a user writes their own connector and wants to use streams, they must transmit the stream_id over the IPROTO protocol.

Unlike a thread, which involves multitasking and execution within a program, a stream transfers data via the protocol between a client and a server.

Interactive transaction
An interactive transaction is one that does not need to be sent in a single request. There are multiple ways to begin, commit, and roll back a transaction, and they can be mixed. You can use stream:begin(), stream:commit(), stream:rollback() or the appropriate stream methods – call, eval, or execute – using the SQL transaction syntax.

Let’s create a Lua client (client.lua) and run it with tarantool:

local net_box = require 'net.box'
local conn = net_box.connect('127.0.0.1:3301')
local conn_tickets = conn.space.tickets
local yaml = require 'yaml'

local stream = conn:new_stream()
local stream_tickets = stream.space.tickets

-- Begin transaction over an iproto stream:
stream:begin()
print("Replaced in a stream\n".. yaml.encode(  stream_tickets:replace({1, 768}) ))

-- Empty select, the transaction was not committed.
-- You can't see it from the requests that do not belong to the
-- transaction.
print("Selected from outside of transaction\n".. yaml.encode(conn_tickets:select({}, {limit = 10}) ))

-- Select returns the previously inserted tuple
-- because this select belongs to the transaction:
print("Selected from within transaction\n".. yaml.encode(stream_tickets:select({}, {limit = 10}) ))

-- Commit transaction:
stream:commit()

-- Now this select also returns the tuple because the transaction has been committed:
print("Selected again from outside of transaction\n".. yaml.encode(conn_tickets:select({}, {limit = 10}) ))

os.exit()

Then call it and see the following output:

Replaced in a stream
--- [1, 768]
...

Selected from outside of transaction
---
- [1, 429]
- [2, 429]
...

Selected from within transaction
---
- [1, 768]
- [2, 429]
...

Selected again from outside of transaction
---
- [1, 768]
- [2, 429]
...```