Advanced

AcapellaDB Structures and operations with it

Data Structures

  1. KV (Key-Value) Simplest data structure. store value by strong equal key.
  2. DT (Distributed Tree) - can store big sequence of keys (distribute seq over whole cluster). Support next, prev logic. Slow
  3. SortedMap - Store whole sequence on cluster same node replicas (num = replication factor). next, prev logic. Fast like KV

Основная Информация

Key format

Info

Ключи в AcapellaDB состоят из двух частей partition и clustering. Первая часть используется для распределения ключей по кластеру, вторая - для создания упорядоченных последовательностей данных, чтобы увеличить локальность связанных данных.

Tip

кодировние ключей :

  • в url (http api) : first:second,
  • в библиотеке : ["first", "second"]

Для того чтобы упростить описание ключей, состоящих из нескольких уровней, была введена поддержка составных ключей в виде json-массива строк. Например, описать год рождения пользователя <user_name> можно следующим образом:

["users", "<user_name>", "birth-date", "year"]

Такими ключами легко манипулировать программно.

Реплики

Сервис поддерживает возможность задавать количество реплик для каждого ключа в отдельности в каждой операции.

Для этого в каждом запросе задаются параметры N, R, W, которые ответственны за следующее:

  • N - количество реплик у данного ключа.
  • R - количество, необходимое для поддтверждения чтения, то есть сколько реплик должны ответить, прежде чем значение будет считаться правильно прочитаным.
  • W - количество, необходимое для поддтверждения записи, то есть сколько реплик должны сообщить об успешной записи нового значения.

Access Method

AcapellaDB has HTTP API and FOS Python client now.

Low level api based on custom binary protocol over Aeron transport. In this case (box license) we offer to use Java client library .

KV

Операции с Key-Value

Установка значения

Есть возможность установки одного или нескольких значений с помощью одного POST запроса /kv/set.

Важно: если в запросах сразу же читается или пишется несколько значений, то это не гарантирует атомарность. Групповые запросы введены для уменьшения количества обращений к сервису. Если до момента выполнения запроса значение не существовало, то оно будет создано.

Пример установки одного значения:

python client

    # set some
    @async_test
    async def test_set(self):
        await session.entry(random_key()).set(random_value())

   # set None 
   @async_test
   async def test_set_none(self):
        await session.entry(random_key()).set(None)

    # set and check 
    @async_test
    async def test_return_set_value(self):
        key = random_key()
        value = random_value()
        await session.entry(key).set(value)
        assert (await session.get_entry(key)).value == value

curl

# set ["k1","k2","k3"] <= "123"
$ curl -X PUT "http://api.acapella.ru:12000/v2/kv/keys/k1:k2:k3?&n=3&r=2&w=2" \
-H  "accept: application/json" \
-H  "content-type: application/json" \
-d "{  \"foo\": 123,  \"bar\": 456}"

-> 

{"version":"1"}

Если в запросе нет ошибок и он корректно обработан, ответ будет со статусом 200 OK. Если запрос составлен неправильно, вернётся код ошибки 400 Bad Request.

Установка нескольких значений аналогична ситуации с одним значением:

TODO 

В явном виде таких методов нет - есть батчи. смотри api

Чтение значения

Чтение одного или нескольких значений производится с помощью POST запроса /kv/get. Чтение по ключу, которому ещё не было присвоено ни одно значение, выдаёт null. В ответе приходит json-массив значений указанных ключей.

Пример:

curl ["k1", "k2", "k3"] - чтение по одному ключу

# get ["k1", "k2", "k3"] - чтение по одному ключу
$ curl -X GET "http://api.acapella.ru:12000/v2/kv/keys/k1:k2:k3?\
wait-timeout=60&transaction=0&n=3&r=2&w=2" \
-H  "accept: application/json" -H  "content-type: application/json"

-> 

{"version":"0","value":{"foo":123.0,"bar":456.0}}

curl get ["not_exists_key"] - чтение не существующего ключа

# Аргумент запроса `keys` - это массив ключей, которые будут прочитаны. 
# Значения приходят в ответе в том-же порядке, как и ключи в запросе.
# get ["not_exists_key"] - чтение не существующего ключа
$ curl -X GET "http://api.acapella.ru:12000/v2/kv/keys/not_exists_key?\
wait-timeout=60&transaction=0&n=3&r=2&w=2" \
-H  "accept: application/json" -H  "content-type: application/json"

-> 

{"version":"0"}

Если в запросе нет ошибок и он корректно обработан, ответ будет со статусом 200 OK. Если запрос составлен неправильно, вернётся код ошибки 400 Bad Request.

Чтение по нескольким ключам

в явном виде таких методов нет - есть батчи. смотри api

Удаление значения

Данная операция эквивалентна установке значения в null, для любых других операций нет разницы, было ли значение установлено в null, удалено или ещё не было задано. Выполняется удаление с помощью POST запроса /v2/kv/delete.

# del ["foo"] - удаление одного ключа
TODO 

Аргумент keys - это массив ключей, которые будут удалены. Если в запросе нет ошибок и он корректно обработан, ответ будет со статусом 200 OK. Если запрос составлен неправильно, вернётся код ошибки 400 Bad Request.

Удаление нескольких ключей

В явном виде таких методов нет - есть батчи. смотри api

CAS без транзакций

Если ваша модель данных работает без транзакций , то допустимы одиночные Cas операции, по одному ключу в каждом

python client

        # тут все хорошо
        key = random_key()
        value = random_value()
        await session.entry(key).cas(value)
        assert (await session.get_entry(key)).value == value

        # этот код бросит исключение CasError
        key = random_key()
        value = random_value()
        entry = await session.get_entry(key)
        await entry.cas(value, entry.version + 1)

Транзакциионные операции

Создание транзакции \ commit \ rollback

Работа с транзакией требует от клиента знания идентификатора транзакции и только. Никакие объекты и ресурсы ОС не требуются.

python client

    # Библиотека для python явно не работает c иджентификатором транзакции
    async def test_create_tx(self):
        async with session.transaction():
            pass
    # но можно работать и в "ручном" режиме в особых случаях 
    # https://github.com/AcapellaSoft/AcapellaDBClient/blob/develop/python/src/acapella/kv/Session.py#L53
    # example :
    tr =    await session.transaction_manual()
    e =     await tr.get_entry( ['k1_1','k1_2'] )

curl create transaction

$ curl -X POST "http://api.acapella.ru:12000/v2/tx" -H  "accept: application/json" -H  "content-type: application/json"
{"index":"8083882192603548991"}

откат транзакции на python

python rollback transaction

        async with session.transaction() as tx:
            await tx.rollback()

если транзакцию откатили - значение останется старым

test

        key = random_key()
        async with session.transaction() as tx:
            e = await tx.get_entry(key)
            value = e.value
            await e.set(random_value())
            await tx.rollback()

        async with session.transaction() as tx:
            e = await tx.get_entry(key)
            assert value == e.value

Если случиться ошибка - транзакция откатится автоматически

rollback on error

        key = random_key()
        value = None
        try:
            async with session.transaction() as tx:
                e = await tx.get_entry(key)
                value = e.value
                await e.set(random_value())
                raise Exception()
        except Exception:
            pass

        async with session.transaction() as tx:
            e = await tx.get_entry(key)
            assert value == e.value

транзакции изолированы

isolation

        key = random_key()
        value = random_value()

        async with session.transaction() as tx:
            await tx.entry(key).set(value)

        async with session.transaction() as tx:
            e = await tx.get_entry(key)
            assert value == e.value

Чтение и запись ключа в транзакции

просто указываем transaction= в параметрах запроса

$ curl -X GET "http://api.acapella.ru:12000/v2/kv/keys/k1/k2/k3?\
wait-timeout=60&transaction=8083882192603548991&n=3&r=2&w=2" \
-H  "accept: application/json" -H  "content-type: application/json"

->

{"version":"0","value":{"foo":123.0,"bar":456.0}}

CAS операция из под транзакции

Сas операция блокирует на время транзакции прочитанный ключ и отмечает его новое значение (внутри транзакции) до конца транзакции.

Commit транзакции пройдеттолько если все cas ключи не модифицированы , но соответствуют изначальным версиям (первое обращение в транзакции)

DT

Возможности

DT - Distributed Tree (Распределенное дерево) предназачена для хранения больших последовательностей в кластере

Преимущества структуры:

  • + В ключах есть порядок. Можно итерировать ключи и запрашивать range
  • + данных можно поместить в DT очень много - гораздо больше чем "влезет" на любой сервер кластера - вся последовательность хранится "в целом на кластере" - распределенно
  • + производительность высокая, как у KV при чтении данных из Глобального индекса ,

Недостатки:

  • - Небольшая скорость вставки (30-60 insert/sec/tree/client , R,W вставленной записи — как в KV (3 реплики, 2 — кворум) для одного DT Tree : все клиенты сталкиваются и rps не превышает 100 rps для текущего tree (3 реплики, 2 — кворум) )

Подходит для индексирования (reverse index) очень большого объёма данных , например wikipedia, который редко изменяется, но часто происходит обращение к индексу на чтение и поиск.

Два режима. Работа со структурой возможна в двух режимах :

  • Обычный
    • - нет транзакций
    • + скорость выше в 10 раз (на уровне касандры в обычном ее режиме)
  • Транзакционный
    • - медленнее (скорость в 10 раз медленнее кассандры)
    • + надежное изменение набора данных
    • + сохранение консистентности набора данных

режимы не совместимы

Основные операции

python insert, read, cursor

    # чтение значения из ключа 
    await session.tree(random_tree()).get_cursor(random_key())

    # запись значения , вставка 
    await session.tree(random_tree()).cursor(random_key()).set(random_value())

    # получить вставленное значение 
    tree = session.tree(random_tree())
    key = random_key()
    value = random_value()
    await tree.cursor(key).set(value)
    value1 = (await tree.get_cursor(key)).value
    # assert  value1==value

навигация по дереву

python : new DT tree, next, prev

        # делаем "случайное" дерево
        tree = session.tree(random_tree())
        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')
        # читаем ключ, позицию запоминаем
        c = await tree.get_cursor(['A', 'A'])

        # NEXT 
        c = await c.next()
        assert c.key == ['A', 'B']
        assert c.value == 'bar'

        # NEXT again
        c = await c.next()
        assert c.key == ['B', 'A']
        assert c.value == 'baz'

        # NEXT again
        c = await c.next()
        assert c is None

Аналогично с prev

        tree = session.tree(random_tree())
        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')

        c = await tree.get_cursor(['B', 'A'])

        c = await c.prev()
        assert c.key == ['A', 'B']
        assert c.value == 'bar'

        c = await c.prev()
        assert c.key == ['A', 'A']
        assert c.value == 'foo'

        c = await c.prev()
        assert c is None

Примеры с транзакциями

в транзакции из Python клиента работать очень просто

        # create tree object 
        tree = session.tree(random_tree())

        # начинаем транзакцию и в ней работаем , как обычно 
        async with session.transaction() as tx:
            # запись , вставка 
            await tree.cursor(['A'], tx).set('foo')
            await tree.cursor(['B'], tx).set('bar')

        async with session.transaction() as tx:
            # чтение 
            assert (await tree.get_cursor(['A'], tx)).value == 'foo'
            assert (await tree.get_cursor(['B'], tx)).value == 'bar'

транзакции добавляют свою логику. Например, если откатить транзакцию :

        tree = session.tree(random_tree())

        async with session.transaction() as tx:
            await tree.cursor(['A'], tx).set('foo')
            await tree.cursor(['B'], tx).set('bar')
            await tx.rollback()

        async with session.transaction() as tx:
            # данные остались старые = никакие
            assert (await tree.get_cursor(['A'], tx)).value is None
            assert (await tree.get_cursor(['B'], tx)).value is None

Range

Можно запросить диапазон ключей.

Это сложный функционал с точки зрения транзакционности и гарантий...

все параметры range описаны в документации

цитата отуда :

Cite

Возвращает отсортированный список ключей в дереве в указанный пределах.

:param first: начальный ключ, не включается в ответ; по умолчанию - с первого

:param last: последий ключ, включается в ответ; по умолчанию - до последнего включительно

:param limit: максимальное количество ключей в ответе, начиная с первого; по умолчанию - нет ограничений

:param transaction: если указан, запрос выполняется в транзакции

:return: список объектов Cursor с данными

        tree = session.tree(random_tree())

        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')

        # параметры по умолчанию. 

        result = await tree.range()
        assert len(result) == 3
        assert result[0].key == ['A', 'A']
        assert result[0].value == 'foo'
        assert result[1].key == ['A', 'B']
        assert result[1].value == 'bar'
        assert result[2].key == ['B', 'A']
        assert result[2].value == 'baz'

Указание начальной позиции :

        tree = session.tree(random_tree())

        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')

        result = await tree.range(first=['A', 'A'])
        assert len(result) == 2
        assert result[0].key == ['A', 'B']
        assert result[0].value == 'bar'
        assert result[1].key == ['B', 'A']
        assert result[1].value == 'baz'

Указание заключительных позиций :

        tree = session.tree(random_tree())

        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')

        result = await tree.range(last=['A', 'B'])
        assert len(result) == 2
        assert result[0].key == ['A', 'A']
        assert result[0].value == 'foo'
        assert result[1].key == ['A', 'B']
        assert result[1].value == 'bar'

Указание предельного размера выборки :

        tree = session.tree(random_tree())

        await tree.cursor(['A', 'A']).set('foo')
        await tree.cursor(['A', 'B']).set('bar')
        await tree.cursor(['B', 'A']).set('baz')

        result = await tree.range(limit=2)
        assert len(result) == 2
        assert result[0].key == ['A', 'A']
        assert result[0].value == 'foo'
        assert result[1].key == ['A', 'B']
        assert result[1].value == 'bar'

Sorted Map

Sorted Map - Функциональность похожа по "data model" на Cassandra. Предназачена для хранения последовательностей в кластере.

Основные преимущества структуры:

  • + В ключах есть порядок. Можно итерировать ключи и запрашивать range
  • + Скорость работы равна скорости Key-Vavlue.

Подходит для индексирования (reverse index) большого объёма данных, (но индекс должен целиком поместиться на одну машину)

Доступные методы

CreateTransaction(isolationLevel) -> transactionID

Создание транзакции с указанным уровнем изоляции. Возвращает идентификатор транзакции, с которым нужно выполнять все остальные запросы, в них он опущен. Так же не указываются NRW параметры, чтобы не увеличивать сигнатуру методов.

Get(partition, clustering) -> (value, version)

Получение значения одного ключа. clustering-ключ может быть пустым. В зависимости от уровня изоляции при выполнении этого запроса может установиться блокировка на clustering-ключ или на весь partition. Внутри транзакции полученная версия значения может не совпадать с версией вне транзакции или в другой транзакции.

Set(partition, clustering, value) -> (version)

Установка значения одного ключа. Обеспечивает отсутствие потерянных обновлений между транзакциями. Внутри одной транзакции может быть перезатёрто set’ом выполненным параллельно. Возвращает новую версию.

Сas(partition, clustering, value, version) -> (version)

Установка значения, если указанная версия совпадает с текущей. Обеспечивает как отсутствие потерянных обновлений между транзакциями, так и возможность параллельной атомарной работы со значением внутри одной транзакции. Если cas прошёл успешно - возвращает новую версию, иначе - текущую версию.

Range(partition, first, last, limit) -> SortedMap

entry = (value, version)

Выборка clustering-ключей из одного partition-ключа с указанными ограничениями. Выборка идёт от ключа first до ключа last, максимальное количество элементов в выборке - limit. Метод возвращает словарь выбранных clustering-ключей и их значений. Batch(partition, Map) -> SortedMap

Батч-запрос get, set, cas методов.

entry = (value, version)
  • Если указано значение - выполняется set.
  • Если указано значение и версия - cas.
  • Если в entry ничего не указано, то выполняется get.

Версионирование

Версии каждого ключа внутри одной транзакции начинаются с нуля. Они никак не связаны с реальными версиями ключей за пределами транзакций и с версиями в других транзакциях. В уровне READ COMMITTED могут возвращаться разные значения с нулевой версией, так как при чтении они не кэшируются.

Уровни изоляции

  • READ UNCOMMITTED. Реализовывать этот уровень смысла нет, потому что алгоритм транзакций уже его предотвращает.
  • READ COMMITTED. Самый низкий уровень изоляции в KV. Обеспечивает чтение только закоммиченных данных. При этом повторные чтения одного ключа могут возвращать разные данные. Одна и та же выборка внутри партишена может возвращать разное количество ключей. Записи транзакции не видны для остальных, пока не будет произведен коммит.
  • REPEATABLE READ. Устраняется возможность чтения разных данных по одному ключу.
  • SERIALIZABLE. Устраняется возможность чтения разных ключей по одной и той же выборке.

Listeners

Also you can use AcapellaDB for wait new version of keys. details...