Разработка → Приложения для Tarantool 1.7. Часть 1. Хранимые процедуры

relevance_17 1 августа в 12:29 7,7k

Привет, Хабр! Сегодня я хочу поделиться с вами опытом написания приложений для Tarantool 1.7. Этот цикл статей будет полезен тем, кто уже собирается использовать Tarantool в своих проектах, либо тем, кто ищет новое решение для оптимизации проектов.


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


Tarantool — это NoSQL база данных, которая хранит данные в памяти либо на диске (в зависимости от подсистемы хранения). Хранилище персистентно за счет продуманного механизма write ahead log. В Tarantool встроен LuaJIT (Just-In-Time Compiler), позволяющий исполнять код на Lua. Также можно писать хранимые процедуры на C.


image


Содержание цикла «Приложения для Tarantool 1.7»


Часть 1. Хранимые процедуры
Часть 2. OAuth2-авторизация
— Часть 3. Тестирование и запуск


Зачем создавать свои приложения для Tarantool


Есть две причины:


  1. Это ускорит работу сервиса. Обработка данных на стороне хранилища сокращает объем передаваемых данных, а объединение нескольких запросов в одну хранимую процедуру позволит сэкономить на сетевых задержках.
  2. Готовые приложения можно переиспользовать. Сейчас экосистема Tarantool активно развивается, появляются новые opensource-приложения на Tarantool, часть которых со временем переносится в сам Tarantool. Такие модули позволяют создавать новые сервисы быстрее.

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


Рассмотрим, как было создано одно из приложений для Tarantool. Оно реализует API для регистрации и аутентификации пользователей. Функционал приложения:


  • регистрация и аутентификация по email в два этапа: создание аккаунта, подтверждение аккаунта с установкой пароля;
  • регистрация через социальные сети (FB, VK, Google+);
  • возможность восстановить пароль.

В качестве примера написания хранимой процедуры Tarantool мы разберем первый этап регистрации по email — получение кода подтверждения. Чтобы оживить примеры, можно воспользоваться исходным кодом, который доступен на github.


Поехали!


Установка Tarantool


О том, как установить Tarantool, прочитайте в документации. Например, для Ubuntu нужно выполнить в терминале:


curl http://download.tarantool.org/tarantool/1.7/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`

sudo apt-get -y install apt-transport-https

sudo rm -f /etc/apt/sources.list.d/*tarantool*.list
sudo tee /etc/apt/sources.list.d/tarantool_1_7.list <<- EOF
deb http://download.tarantool.org/tarantool/1.7/ubuntu/ $release main
deb-src http://download.tarantool.org/tarantool/1.7/ubuntu/ $release main
EOF

sudo apt-get update
sudo apt-get -y install tarantool

Проверим, что установка прошла успешно, вызвав в консоли tarantool и запустив интерактивный режим работы.


$ tarantool
version 1.7.3-202-gfe0a67c
type 'help' for interactive help
tarantool>

Здесь можно попробовать свои силы в программировании на Lua.
Если сил нет, то наберитесь их в этом небольшом tutorial.


Регистрация по email


Идем дальше. Напишем первый скрипт, позволяющий создать пространство (space) с пользователями. Space — это аналог таблиц для хранения данных. Cами данные хранятся в виде кортежей (tuple). Space должен содержать один первичный (primary) индекс, в нем также может быть несколько вторичных (secondary) индексов. Индекс бывает и по одному ключу, и сразу по нескольким. Tuple представляет собой массив, в котором хранятся записи. Рассмотрим схему space’ов сервиса аутентификации:


image


Как видно из схемы, мы используем индексы двух типов: hash и tree. Hash-индекс позволяет находить кортежи по полному совпадению первичного ключа и обязан быть уникальным. Tree-индекс поддерживает неуникальные ключи, поиск по первой части составного индекса и позволяет оптимизировать операции сортировки по ключу, так как значения в индексе хранятся упорядоченно.


В space session хранится ключ (session_secret), которым подписывается сессионная кука. Хранение ключей сессий позволяет разлогинивать пользователей на стороне сервиса, если нужно. Сессия имеет опциональную ссылку на space social. Это необходимо для валидации сессий пользователей, входящих через социальные сети (проверки валидности хранимого OAuth2-токена).


Перейдем к написанию приложения. Для начала рассмотрим структуру будущего проекта:


tarantool-authman
├── authman
│   ├── model
│   │   ├── password.lua
│   │   ├── password_token.lua
│   │   ├── session.lua
│   │   ├── social.lua
│   │   └── user.lua
│   ├── utils
│   │   ├── http.lua
│   │   └── utils.lua
│   ├── db.lua
│   ├── error.lua
│   ├── init.lua
│   ├── response.lua
│   └── validator.lua
└── test
    ├── case
    │   ├── auth.lua
    │   └── registration.lua
    ├── authman.test.lua
    └── config.lua

Модули в Lua импортируются из путей, указанных в package.path переменной.
В нашем случае модули импортируются относительно текущей директории, т. е. tarantool-authman. Однако при необходимости пути импорта можно дополнить:


lua
-- Добавляем новый путь с самым высоким приоритетом (в начало строки)
package.path = "/some/other/path/?.lua;" .. package.path

Прежде чем мы создадим первый space, вынесем необходимые константы в модели. Каждый space и каждый индекс должен определить свое название. Также необходимо определить порядок хранения полей в кортеже. Так выглядит модель пользователя authman/model/user.lua:


-- Наш модуль — это Lua-таблица
local user = {}

-- Модуль содержит единственную функцию — model, которая возвращает таблицу с полями и методами модели
-- На входе функция принимает конфигурацию в виде опять же lua-таблицы
function user.model(config)
    local model = {}

    -- Название спейса и индексов
    model.SPACE_NAME = 'auth_user'
    model.PRIMARY_INDEX = 'primary'
    model.EMAIL_INDEX = 'email_index'

    -- Номера полей в хранимом кортеже (tuple)
    -- Индексация массивов в Lua начинается с 1 (!)
    model.ID = 1
    model.EMAIL = 2
    model.TYPE = 3
    model.IS_ACTIVE = 4

    -- Типы пользователя: email-регистрация или через соцсеть
    model.COMMON_TYPE = 1
    model.SOCIAL_TYPE = 2

    return model
end

-- Возвращаем модуль
return user

В случае с пользователями нам понадобится два индекса. Уникальный по id и неуникальный по email, так как, регистрируясь через социальные сети, два разных пользователя могут получить одинаковый email либо не получить email вовсе. Уникальность email для пользователей, зарегистрировавшихся не через социальные сети, обеспечим логикой приложения.


Модуль authman/db.lua содержит метод для создания space’ов:


local db = {}

-- Импортируем модуль и вызываем функцию model
-- При этом в параметр config попадает nil — пустое значение
local user = require('authman.model.user').model()

-- Метод модуля db, создающий пространства (space) и индексы
function db.create_database()

    local user_space = box.schema.space.create(user.SPACE_NAME, {
        if_not_exists = true
    })
    user_space:create_index(user.PRIMARY_INDEX, {
        type = 'hash',
        parts = {user.ID, 'string'},
        if_not_exists = true
    })
    user_space:create_index(user.EMAIL_INDEX, {
        type = 'tree',
        unique = false,
        parts = {user.EMAIL, 'string', user.TYPE, 'unsigned'},
        if_not_exists = true
    })
end

return db

В качестве id пользователя берем uuid, тип индекса hash, ищем по полному совпадению. Индекс для поиска по email состоит из двух частей: (user.EMAIL, 'string') — email, (user.TYPE, 'unsigned') — тип пользователя. Типы были определены ранее в модели. Составной индекс позволяет искать не только по всем полям, но и по первой части индекса, поэтому доступен поиск только по email (без типа пользователя).


Теперь запустим интерактивную консоль Tarantool в директории с проектом и попробуем воспользоваться модулем authman/db.lua.


$ tarantool
version 1.7.3-202-gfe0a67c
type 'help' for interactive help
tarantool> db = require('authman.db')
tarantool> box.cfg({listen=3331})
tarantool> db.create_database()

Отлично, первый space создан! Внимание: перед обращением к box.schema.space.create необходимо сконфигурировать и запустить сервер методом box.cfg. Теперь рассмотрим несколько простых действий внутри созданного space:


-- Создание пользователей
tarantool> box.space.auth_user:insert({'user_id_1', 'exaple_1@mail.ru', 1})
---
- ['user_id_1', 'exaple_1@mail.ru', 1]
...
tarantool> box.space.auth_user:insert({'user_id_2', 'exaple_2@mail.ru', 1})
---
- ['user_id_2', 'exaple_2@mail.ru', 1]
...
-- Получие Lua-таблицы (массива) всех пользователей
tarantool> box.space.auth_user:select()
---
- - ['user_id_2', 'exaple_2@mail.ru', 1]
  - ['user_id_1', 'exaple_1@mail.ru', 1]
...

-- Получение пользователя по первичному ключу
tarantool> box.space.auth_user:get({'user_id_1'})
---
- ['user_id_1', 'exaple_1@mail.ru', 1]
...

-- Получение пользователя по составному ключу
tarantool> box.space.auth_user.index.email_index:select({'exaple_2@mail.ru', 1})
---
- - ['user_id_2', 'exaple_2@mail.ru', 1]
...

-- Обновление данных с заменой второго поля
tarantool> box.space.auth_user:update('user_id_1', {{'=', 2, 'new_email@mail.ru'}, })
---
- ['user_id_1', 'new_email@mail.ru', 1]
...

Уникальные индексы ограничивают вставку неуникальных значений. Если необходимо создавать записи, которые уже могут находиться в space, воспользуйтесь операцией upsert (update/insert). Полный список доступных методов можно найти в документации.


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


    function model.get_space()
        return box.space[model.SPACE_NAME]
    end

    function model.get_by_email(email, type)
        if validator.not_empty_string(email) then
            return model.get_space().index[model.EMAIL_INDEX]:select({email, type})[1]
        end
    end

    -- Создание пользователя
    -- Поля, не являющиеся частями уникального индекса, необязательны
    function model.create(user_tuple)
        local user_id = uuid.str()
        local email = validator.string(user_tuple[model.EMAIL]) and user_tuple[model.EMAIL] or ''
        return model.get_space():insert{
            user_id,
            email,
            user_tuple[model.TYPE],
            user_tuple[model.IS_ACTIVE],
            user_tuple[model.PROFILE]
        }
    end

    -- Генерация кода, который отправляется в письме, с просьбой активировать аккаунт
    -- Как правило, такой код подставляется GET-параметром в ссылку
    -- activation_secret — один из настраиваемых параметров при инициализации приложения
    function model.generate_activation_code(user_id)
        return digest.md5_hex(string.format('%s.%s', config.activation_secret, user_id))
    end

В приведенном фрагменте кода применены два стандартных модуля Tarantool — uuid и digest, а также один пользовательский — validator. Перед использованием их необходимо импортировать:


-- Стандартные модули Tarantool
local digest = require('digest')
local uuid = require('uuid')
-- Модуль нашего приложения (отвечает за валидацию данных)
local validator =  require('authman.validator')

Переменные объявляются с оператором local, ограничивающим область видимости переменной текущим блоком. В противном случае переменная будет глобальной, чего следует избегать из-за возможного конфликта имен.


А теперь создадим основной модуль authman/init.lua. В этом модуле будут собраны все методы api приложения.


local auth = {}

local response = require('authman.response')
local error = require('authman.error')
local validator = require('authman.validator')
local db = require('authman.db')
local utils = require('authman.utils.utils')

-- Модуль возвращает единственную функцию — api, которая конфигурирует приложение и возвращает его
function auth.api(config)
    local api = {}
    -- Модуль validator содержит проверки различных типов значений
    -- Здесь же выставляются значения по умолчанию
    config = validator.config(config)

    -- Импортируем модели для работы с данными
    local user = require('authman.model.user').model(config)

    -- Создаем space
    db.create_database()

    -- Метод api создает неактивного пользователя с указанным адресом электронной почты
    function api.registration(email)
        -- Перед работой с email — приводим его к нижнему регистру
        email = utils.lower(email)

        if not validator.email(email) then
            return response.error(error.INVALID_PARAMS)
        end

        -- Проверяем, нет ли существующего пользователя с таким email
        local user_tuple = user.get_by_email(email, user.COMMON_TYPE)
        if user_tuple ~= nil then
            if user_tuple[user.IS_ACTIVE] then
                return response.error(error.USER_ALREADY_EXISTS)
            else
                local code = user.generate_activation_code(user_tuple[user.ID])
                return response.ok(code)
            end
        end

        -- Записываем данные в space
        user_tuple = user.create({
            [user.EMAIL] = email,
            [user.TYPE] = user.COMMON_TYPE,
            [user.IS_ACTIVE] = false,
        })

        local code = user.generate_activation_code(user_tuple[user.ID])
        return response.ok(code)
    end

    return api
end

return auth

Отлично! Теперь пользователи смогут создавать аккаунты.


tarantool> auth = require('authman').api(config)
-- Воспользуемся api для получения кода регистрации
tarantool> ok, code = auth.registration('example@mail.ru')
-- Этот код необходимо передать пользователю на email для активации аккаунта
tarantool> code
022c1ff1f0b171e51cb6c6e32aefd6ab

На этом все. В следующей части рассмотрим использование готовых модулей, сетевое взаимодействие и реализацию OAuth2 в tarantool-authman.

Проголосовать:
+63
Сохранить: