image Многие слышали о NoSQL базе данных Tarantool, знают о том, что она умеет хранить данные в памяти, очень быстро их обрабатывает и обладает высокой производительностью. Тарантул был написан серьезными ребятами, которые обслуживают сервисы с сотнями тысяч запросов в секунду.

Система кажется сложной. Несмотря на российские корни, изначально даже не было документации на русском языке. Чем же может помочь этот мощный инструмент обычным парням — программистам и начинающим разработчикам? Остальные могут сразу посмотреть результат

Попробуем написать простой занимательный сервис, способный выдержать большую нагрузку. И никакого SQL!

Наша цель


Чтобы подогреть интерес к процессу изучения, возьмем простой и интересный пример использования Тарантула в веб-разработке. Мы запустим сайт, на котором выводится попарное голосование за какие-нибудь картинки. Выкладывать фотографии студенток колледжа, как Марк Цукерберг, мы не будем, но зато устроим голосование по выбору стикера для мессенджера Telegram. Наш алгоритм путем голосования будет выбирать топ из лучших наклеек, но на самом деле наша задача найти главного урода коллекции – мистера The Ugly. Теперь, когда появилась мотивация, самое время заняться скучными делами.

Установка


Вороне где-то бог послал кусочек сыру. Нам же достался скромный виртуальный сервер с одним процессорным ядром и оперативной памятью 1 Гбайт — младший отпрыск из одной благовоспитанной немецкой семьи. Операционная система — Debian, поэтому мы можем установить Тарантул из официального репозитория: tarantool.org/download.html

В инструкции написано Copy and Paste, что мы и делаем с большим энтузиазмом:

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

# install https download transport for APT
sudo apt-get -y install apt-transport-https

# append two lines to a list of source repositories
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/debian/ $release main
deb-src http://download.tarantool.org/tarantool/1.7/debian/ $release main
EOF

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

Маленький сюрприз от разработчиков: в официальном репозитории не поддерживается архитектура i386, хотя сервис хостинга пакетов исправно предоставляет все файлы, кроме основного. Убедитесь, что вы устанавливаете пакеты на систему AMD64, чтобы избежать неприятностей.

Начало работы


После установки, у нас появляется процесс tarantool, запущенный с тестовой конфигурацией example.lua, примерно такой:

ps xauf | grep taran
root      2735  0.0  0.2  13972  2132 pts/0    S+   22:05   0:00          \_ grep taran
taranto+   568  0.0  0.8 812304  8632 ?        Ssl  17:03   0:03 tarantool example.lua <running>

Для работы с Tarantool используется скриптовый язык Lua, на котором пишутся команды управления сервером, описываются хранимые процедуры и триггеры. Можно подгрузить готовые программные модули или написать свои.

Запустим утилиту tarantoolctl, напечатаем первую программу и убедимся, что всё работает.

tarantoolctl connect '3301'
connected to localhost:3301
localhost:3301> print ('The good!')
---
...
localhost:3301>

Сейчас мы находимся в тестовой конфигурации, которая поставляется вместе с дистрибутивом. Согласно идеологии Debian, настройки находятся в директории /etc/tarantool/instances.available/*, а запускаемая конфигурация программы создаётся при помощи символической ссылки в директории /etc/tarantool/instances.enabled/*. Скопируем файл примера под новым имением и создадим наш проект.

Наш проект называется the good, the bad and the ugly, сокращённо — gbu. Используйте сокращение полного названия проекта до первых букв, и ваши коллеги всегда будут с уважением и трепетом относиться к вашей работе!

Теперь немного поправим gbu.cfg и запустим сервис. Напомню, что для конфигурации используется синтаксис языка Lua, в котором комментарии начинаются с двух дефисов.

box.cfg {
    -- Хорошая привычка сразу менять порт по умолчанию для сервиса
    listen = 3311;
    -- Подгоняем размер памяти под наши нужды
    slab_alloc_arena = 0.2; 
    -- Остальные параметры оставляем без изменений
}

local function bootstrap()
    local space = box.schema.create_space('example')
    space:create_index('primary')
    -- Закомментируем пользователя по умолчанию
    --  box.schema.user.grant('guest', 'read,write,execute', 'universe')
    --  Создадим нового пользователя
    box.schema.user.create('good', { password = 'secret' })
    box.schema.user.grant('good', 'read,write,execute', 'universe')
end
    -- При первом запуске создаем пространство и назначаем привилегии
box.once('example-2.0', bootstrap)

Запускаем новый инстанс командой tarantoolctl start gbu и убеждаемся, что всё работает как надо:

tarantoolctl connect "good:secret@0:3311"
connected to 0:3311
0:3311>

Мы в деле!

База данных


  • Записи в Tarantool хранятся в пространствах (space), это такой аналог таблицы в реляционной базе данных SQL. Их можно сделать столько, сколько потребуется — до 65 тысяч
  • Внутри пространства находятся кортежи (tuples), которые похожи и на строку в таблице SQL, и на JSON-массив данных. Максимальный размер кортежа — до 1 Мб при настройках по умолчанию. Этот параметр можно менять.
  • Чтобы от базы данных была польза, необходимо создать индексы, так же, как в SQL. Благодаря им, вместо перебора полного списка всех элементов поиск происходит по более быстрому алгоритму. Также доступна сортировка. Выбор индекса зависит от типа данных.

HASH-индекс: значение должно быть уникальным, и может быть произвольным. Так организуются всем известные Key/Value-хранилища, известные также как Map. Хороший пример — контрольная сумма файла MD5 hash.

TREE-индекс: значение может быть неуникальным, но должно быть «плотным» для организации сортированного списка. Получается массив (Array), у которого могут быть пропущенные элементы. Хороший пример — номер заказа, который увеличивается на единицу.

Если нужно уникальное значение, то вы можете использовать HASH или TREE, при этом HASH будет быстрее на разреженных данных. Если же нужно не уникальное поле, по которому будет делаться сортировка, то можно использовать только TREE-индекс.

Также есть индексы RTREE для поиска на двумерной плоскости и BITSET для работы с битовыми данными, но нам они не потребуются. Подробнее об этом написано в документации.

Картинка из статьи Евгения Шадрина Осваиваем Тарантул. Кстати, хорошее руководство для первоначального знакомства.

Tarantool data model

Модель данных проекта


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

  1. unsigned id — уникальный номер стикера
  2. integer rating — рейтинг стикера
  3. string pack — название стикер-пака
  4. string name — название файла стикера
  5. (string) path — URL стикера
  6. (number) up — количество голосов за стикер
  7. (number) down — количество голосов против стикера.

В пространстве packs мы будем хранить список стикер-паков:

  1. string pack — название стикер-пака
  2. integer rating — рейтинг стикер-пака
  3. (string) path — ссылка на страницу описания

В пространстве secrets мы будем хранить токен для шифрования ссылки на картинку, чтобы реализовать простейшую защиту от накрутки:

  1. string token — случайный токен для стикера
  2. integer time — время создания токена (пригодится для удаления старых)
  3. (integer) id — уникальный номер стикера (ключ пространства stickers)
  4. (string) url — URL стикера

В пространстве sessions мы будем записывать посетителей и собирать статистику:

  1. string uuid — уникальный символьный идентификатор посетителя
  2. integer uuid_time — время создания сессии (пригодится для удаления старых)
  3. (number) user_votes — сколько раз проголосовал посетитель
  4. (string) ip — IP-адрес посетителя
  5. (string) agent — тип браузера посетителя

В пространстве server будем просто собирать статистику работы сайта:

  1. Integer id — просто ключ
  2. (number) visitors — количество уникальных пользователей
  3. (number) votes — общее количество показов голосований
  4. (number) clicks — общее количество кликов в голосовании

Обратите внимание, что для назначения индекса нужно явно указать тип поля. Этот тип должен быть выбран из списка возможных вариантов Тарантула.

Остальные поля могут иметь произвольный тип, который поддерживает встроенный интерпретатор Lua. Этот дуализм типов данных является особенностью Тарантула и упоминается в документации. Для удобства, мы указали тип данных Lua в скобках при описании модели.

Важная часть в моделировании — составление индексов. Большое преимущество Тарантула заключается в том, что он умеет делать сложные составные индексы. Благодаря этому, мы можем писать быстрые аналитические запросы на основе различных полей кортежа без уменьшения быстродействия системы. Добавим первичный индекс типа TREE для поля id, чтобы обеспечить случайный выбор элемента для голосования. Второй индекс — типа TREE по полю Raiting, чтобы выводить рейтинг, конечно же! Добавим составной индекс по полям Pack + Emoj типа HASH, который должен быть уникальным. Его можно использовать для анализа популярности наборов стикеров.

Код создания базы данных мы разместим в нашем файле gbu.lua, в процедуре инициализации

function bootstrap()
local function bootstrap()

    box.schema.user.create('good', { password = 'secret' })
    box.schema.user.grant('good', 'read,write,execute', 'universe')
   
   -----------------------------------
    -- Пространство стикеров
    local stickers = box.schema.create_space('stickers')
        -- Индекс для файла
    stickers:create_index('primary', { 
        type = 'TREE', parts = {1, 'unsigned'}
    })

    -- Индекс для рейтинга
    stickers:create_index('secondary', {
        type = 'TREE',
        unique = false,
        parts = {2, 'integer'}
    })

    -- Индекс для названий стикеров
    stickers:create_index('ternary', {
        type ='HASH', parts = {3, 'string', 4, 'string'}
    })

    -----------------------------------
    -- Пространство стикер-пака
    local packs = box.schema.create_space('packs')
    -- Индекс для стикер-пака
    packs:create_index('primary', {
        type = 'HASH', parts = {1, 'string'}
    })
    
    -- Индекс для рейтинга пака
    packs:create_index('secondary', {
        type = 'TREE',
        unique = false, 
        parts = {2, 'integer'}
    })
    
    -----------------------------------
    -- Пространство секретных ссылок
    local secret = box.schema.create_space('secret')
    
    -- Индекс для секретного ключа
    secret:create_index('primary', {
        type = 'HASH', parts = {1, 'string'}
    })
    
    -- Индекс для времени создания ключа
    secret:create_index('secondary', {
        type = 'TREE',
        unique = false, 
        parts = {2, 'integer'}
    })
    
    -----------------------------------
    -- Пространство сессий пользователей
    local sessions = box.schema.create_space('sessions')

    -- Индекс для уникального пользователя   
        sessions:create_index('primary', {
        type = 'HASH', parts = {1, 'string'}
    })
    
    -- Индекс для времени создания сессии
    sessions:create_index('secondary', {
        type = 'TREE',
        unique = false, 
        parts = {2, 'integer'}
    })
    
    -----------------------------------
    -- Пространство статистики сервера
    local server = box.schema.create_space('server')

    -- Просто индекс   
        server:create_index('primary', {
        type = 'TREE', parts = {1, 'unsigned'}
    })
    -- Создадим запись
    server:insert{1, 0, 0, 0}
   
end


Прежде чем перезапустить сервер с настройками, попробуйте создать схему командами в консоли. Если что-то пойдёт не так, можно удалить пространство целиком командой: box.space.stickers:drop()
или отдельный индекс: box.space.stickers.index.ternary:drop()

Не стесняйтесь использовать подсказку по клавише TAB. Для удобства работы в консоли, названия создаваемых элементов схемы мы пишем с маленькой буквы. Команды для работы в консоли становятся интуитивно понятны после краткого ознакомления с документацией.
Очистить пространство: box.space.stickers:truncate()
Удалить пространство: box.space.stickers:drop()

Всё происходит мгновенно, как и положено для In-memory Database!

Установка компонентов


Сильное заявление
Хороший современный язык программирования должен иметь статическую строгую типизацию, Homoiconicity — свойство, позволяющее манипулировать кодом как данными, поддержку ООП, FFI к библиотекам на C, поддержку дженериков, конкурентного программирования, Functions as first-class citizens, lambdas.

Ничего этого в PHP, конечно же, нет! Поэтому мы будем писать код примера именно на нём.

Для начала поставим проверенные инструменты — веб сервер Nginx и интерпретатор PHP — php-fpm: wiki.debian.org/ru/nginx/nginx+php-fpm

В корневой путь конфигурации nginx добавим правило перезаписи запросов:

location / {
        try_files $uri $uri/ /index.php?q=$uri&$args;
}

Таким образом, мы можем получать в PHP-скрипте красивые ссылки вида /good из массива $_REQUEST['q'], и реализуем роутинг HTTP-запросов.

Еще у нас есть локейшин для выполнения CGI-запросов:

location ~* \.php$ {
            try_files $uri =404;
            fastcgi_pass        unix:/var/run/php5-fpm.sock;
            fastcgi_index       index.php;
            include             fastcgi_params;
            expires -1;
        }

Командой expires -1; мы отключаем кэширование запросов, оно не нужно для страниц голосования и вывода Топ-чартов. Остальные локейшины кешируют данные на 24 часа или 30 дней из вышестоящих настроек HTTP. Наверное, у каждого есть свой сборник готовых опции Nginx.

Теперь необходимо поставить модуль для работы с Тарантулом:

sudo apt-get install php5-cli php5-dev php-pear
pecl channel-discover tarantool.github.io/tarantool-php/pecl
pecl install Tarantool-PHP/Tarantool-beta

Читаем, что написано в выводе программы-установщика. В моем случае было сообщение:

Build process completed successfully
Installing '/usr/lib/php5/20131226/tarantool.so'
install ok: channel://tarantool.github.io/tarantool-php/pecl/Tarantool-0.0.13
configuration option "php_ini" is not set to php.ini location
You should add "extension=tarantool.so" to php.ini

Добавляем указанную строчку в файл конфигурации в файл /etc/php5/fpm/php.ini и в /etc/php5/cli/php.ini. К сожалению, при запуске PHP получаем ошибку! Чтобы не страдать с отладкой web-сервера, мы добавили новую библиотеку и в cli-конфигурацию, поэтому можно проверить работоспособность из командной строки.

php -v
PHP Warning:  PHP Startup: Unable to load dynamic library '/usr/lib/php5/20131226/tarantool.so' - /usr/lib/php5/20131226/tarantool.so: undefined symbol: tarantool_schema_destroy in Unknown on line 0
PHP 5.6.29-0+deb8u1 (cli) (built: Dec 13 2016 16:02:08)

На момент написания статьи модуль в репозитории PEAR содержал ошибку, поэтому остается лишь путь джедая — сборка драйвера из исходных кодов.

pecl uninstall Tarantool-PHP/Tarantool-beta
cd ~
git clone https://github.com/tarantool/tarantool-php.git
cd tarantool-php
phpize
./configure
make
make install

Загрузка данных


Создадим первый файл и назовём его test.php, в нём мы проверим работу нашей базы данных.

<?php
$tarantool = new Tarantool('localhost', 3311, 'good', 'secret');
try {
    $tarantool->ping();
} catch (Exception $e) {
    echo "Exception: ", $e->getMessage(), "\n";
}
?> 

Запускаем из командной строки php config.php и проверяем, как сработало. Если настроить неправильно, то получим сообщение об ошибке. Проверьте!

Теперь можно написать парсер, который соберёт данные с нужного нам сайта. Мы будем исследовать tlgrm.ru/stickers. Сначала загрузим таблицу pack, в которой у нас лежит перепись стикер-паков. Вот так выглядит команда insert в командной строке tarantool:

box.space.packs:upsert({'key1',0}, {{'=',2,0}})

Эта команда добавляет новый ключ “key1” (в поле 1) и значение 0 (в поле 2). Если запись существует, то обновляется для этой же записи (знак =) в поле 2 значением 0. Как мы помним, в поле 2 у нас содержится рейтинг, который мы первоначально в 0. Команду upsert удобно использовать для многократного запуска парсера при отладке, чтобы не удалять каждый раз внесенные данные. PHP-вариант команды будет выглядеть так:

$tarantool->upsert('packs', array ($pack,0), array (
    array(
        "field" => 1,
            "op" => "=",
            "arg" => 0
        )
   ));

Ай-ай! В PHP нумерация полей с 0, а в Lua c 1. Поэтому "field" => 1 из массива PHP соответствует записи {'=',2,0} в Lua. Везде, где массивы начинаются с нуля, текущие коннекторы работают так же. Это поведение было изменено начиная с версии 1.6. Читая примеры в интернете, обращайте внимание на версию Тарантула! Эта статья написана по версии 1.7, а про версию 1.5 разработчики просят не вспоминать вовсе.

Запись для стикера мы вносим при помощи встроенной процедуры auto_increment, которая автоматически увеличивает первичный индекс. Команда Тарантула:

box.space.stickers:auto_increment({0,'pack2','sticker2'})

PHP:

$tarantool->call('box.space.stickers:auto_increment', array(
    array(0,$pack, $i . '.png', $url, 0, 0)
));

Итак, скрипт написан. Запускаем его — вжух, и магия! Теперь у нас есть база данных с 16 000 записей!

Пишем программу


Для начала сделаем простейший роутер запросов, как это обычно делается в PHP:

# Get routes from request
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : '';
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : '';

switch ($route) {
    case '/good':
        action_good();
        break;
    case '/bad':
        action_bad();
        break;
    case '/ugly':
        action_ugly();
        break;
    case '/about':
        action_about();
        break;
    default:
        if (!empty($vote_plus) && !empty($vote_minus)) {
           sleep (1);
           do_post($vote_plus, $vote_minus);
        }
        action_main();
}

Отметим наличие двух переменных $vote_plus и $vote_minus, которые будут передаваться в POST-запросе при голосовании за одну или другую картинку. Дело в том, что, зная имя и путь файла очень легко накрутить голосование ботами, а нам это не нужно. Поэтому мы будем для страницы голосования генерировать пару уникальных токенов, по одному на каждую картинку. После голосования токен будет удаляться, делая невозможным повторное использование голоса.

Поскольку в PHP до выхода версии 7.0 дела с криптобезопасными функциями обстоят печально, то очень помогу богатые возможности Тарантула по работе с криптографией.

Для начала в функции action_main инициируем генератор случайных чисел криптобезопасным (т.е. действительно случайным) seed:

    $r = $tarantool->evaluate(
        "digest = require('digest')
        return (digest.urandom(4))"
    );

    $seed = unpack('L', $r[0])[1];
    srand($seed);

Функция $tarantool->evaluate() используется непосредственно для запуска кода Lua без возни с созданием хранимой процедуры. Затем два раза вызываем функцию create_random_vote(), которая выберет случайный элемент в пространстве и создаст URL для картинки и токенов.

function create_random_vote()
function create_random_vote() {
# Get random sticker id

    global $tarantool;
    $tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
    $id = $tuple[0][0];
    $url = $tuple[0][4];

# Create random sticker token
     $token = $tarantool->evaluate(
        "digest = require('digest')
        return ( digest.md5_hex(digest.urandom(32)))"
    )[0];

$time = time();

# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
    $tarantool->insert('secret', array ($token, $time, $id, $url));

    return array (
        $url,
        $token
    );
}


Тут использовались ещё две функции: $tarantool->call() для вызова встроенных процедур и $tarantol->insert() для вставки новой записи.

Приведём пример процедуры обновления голосования, которая обновляет рейтинг записи:

function update_votes($id, $plus, $minus)
function update_votes($id, $plus, $minus) {
    global $tarantool;

###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
   $tarantool->update("stickers", $id,
        array (
            array(
                "field" => 5,
                "op" => "+",
                "arg" => $plus
            ),
            array(
                "field" => 6,
                "op" => "+",
                "arg" => $minus
            )
        )
    );
}


Полный список методов класса Tarantool смотрите в документации tarantool-php.

Обратите внимание на параметр "op" => "=", который означает, что происходит замена поля в существующем кортеже. Также есть параметр +,- и некоторые другие операции. Они выполняют очень важную задачу. Обычно для замены значения в базе данных мы сначала читаем какое-то поле, затем изменяем его. Чтобы сохранить консистентность данных, приходится блокировать доступ к таблице и использовать транзакции. В Тарантуле же, благодаря его архитектуре, команды update и upsert срабатывают атомарно внутри процесса сервера без блокировки базы данных. Это позволяет строить чертовски быстрые системы!

Код файла index.php на момент написания статьи находится под спойлером:

index.php
<?php

# Init database
$tarantool = new Tarantool('localhost', 3301, 'good', 'bad');

try {
        $tarantool->ping();
} catch (Exception $e) {
        echo "Exception: ", $e->getMessage(), "\n";
}

const MIN_VOTES = 20;           // Number of votes to show the ugly
const UPDATE_PLUS = 1;          // Increment for positive update
const UPDATE_MINUS = -1;        // Increment for negative update
const NO_UPDATE = 0;
const COOKIE = 'uuid';          // Cookie name
const HIDDEN = '/img/Question.svg';// Picture for hidden element

# Get routes from request
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : '';
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : '';

# Get cookie from request or create new value
$cookie = isset($_COOKIE[COOKIE]) ? $_COOKIE[COOKIE] : update_user('');

switch ($route) {
    case '/good':
        action_good();
        break;
    case '/bad':
        action_bad();
        break;
    case '/ugly':
        action_ugly($cookie);
        break;
    case '/about':
        action_about();
        break;
    default:
# This is post request:
        if (!empty($vote_plus) && !empty($vote_minus)) {
           sleep (1);

           $cookie = update_user($cookie);
           do_post($vote_plus, $vote_minus);

        }
        setcookie(COOKIE, $cookie, time() + (86400 * 30), "/");
        action_main();
}

exit();

function action_main() {
    global $tarantool;

# Get crypto safe random seed from Tarantool LUA module
# https://tarantool.org/doc/reference/reference_lua/digest.html
    $r = $tarantool->evaluate(
        "digest = require('digest')
        return (digest.urandom(4))"
    );

    $seed = unpack('L', $r[0])[1];
    srand($seed);

    list ($left_url, $left_token_plus) = create_random_vote();
    list ($right_url, $right_token_plus) = create_random_vote();

    $left_token_minus = $right_token_plus;
    $right_token_minus = $left_token_plus;

    update_stats(UPDATE_PLUS, NO_UPDATE);

    $title = 'Хорошие и плохие стикеры Telegram';
    include_once('main.html');
}

function action_good() {
    $title = 'ТОП лучших стикеров Telegram';
    $top = get_top(10,Tarantool::ITERATOR_LE);
    $active_good ='class="active"';
    $active_bad ='';

    include_once('top.html');
}

function action_bad () {
    $title = 'ТОП худших стикеров Telegram';
    $active_bad ='class="active"';
    $active_good ='';
    $top = get_top(10,Tarantool::ITERATOR_GE);
# Hide the ugly
    $top[0][4] = HIDDEN;
    include_once('top.html');
}

function action_ugly($user) {
    $title = 'Худший стикер Telegram';
    $top = get_top(1,Tarantool::ITERATOR_GE);
    $votes = get_session($user);
# Hide the ugly until getting enough votes
    if ($votes < MIN_VOTES) {
        $ugly_message = "Голосуй " . MIN_VOTES . " раз чтобы увидеть результат<br>";
        $ugly_message .= "Осталось " . (MIN_VOTES - $votes) . " голосований";
        $ugly_img = HIDDEN;
    } else {
        $ugly_img = $top[0][4];
    }
    include_once('ugly.html');
}

function action_about() {
    $title = 'Как это сделано?';
    list($stickers, $shows, $votes, $visitors) = get_server_stats();
    include_once('about.html');
}

function do_post($vote_plus, $vote_minus) {
    global $tarantool;

    $tuple_plus = $tarantool->select("secret", $vote_plus);
    $tuple_minus = $tarantool->select("secret", $vote_minus);

    $id_plus = $tuple_plus[0][2];
    $id_minus = $tuple_minus[0][2];

# Clean up used tokens
    if (!empty($vote_plus) && !empty($vote_minus)) {
        $tarantool->delete("secret", $vote_plus);
        $tarantool->delete("secret", $vote_minus);
    }

# Get actual tuple data
    if (!empty($id_plus) && !empty($id_minus)) {

        $raiting = +1;
        update_rating($id_plus, $raiting);

        $raiting = -1;
        update_rating($id_minus, $raiting);

        update_votes($id_plus,  UPDATE_PLUS, NO_UPDATE);
        update_votes($id_minus, NO_UPDATE,  UPDATE_MINUS);

        update_stats(NO_UPDATE, UPDATE_PLUS);
    }
}

function create_random_vote() {
# Get random sticker id

    global $tarantool;
    $tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
    $id = $tuple[0][0];
    $url = $tuple[0][4];

# Create random sticker token
     $token = $tarantool->evaluate(
        "digest = require('digest')
        return ( digest.md5_hex(digest.urandom(32)))"
    )[0];

$time = time();

# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
    $tarantool->insert('secret', array ($token, $time, $id, $url));

    return array (
        $url,
        $token
    );
}

function update_rating($id, $update) {
    global $tarantool;

#################################################
#
# box.space.stickers:update(7856, {{'+', 2, 10}})
#
#################################################
   $tarantool->update("stickers", $id, array (
        array(
            "field" => 1,
            "op" => "+",
            "arg" => $update
        )
    ));
}

function update_votes($id, $plus, $minus) {
    global $tarantool;

###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
   $tarantool->update("stickers", $id,
        array (
            array(
                "field" => 5,
                "op" => "+",
                "arg" => $plus
            ),
            array(
                "field" => 6,
                "op" => "+",
                "arg" => $minus
            )
        )
    );
}

function update_user($cookie) {
    global $tarantool;

# Create uuid if first time user
    if (empty($cookie)) {

##################################
#
# uuid = require('uuid')
# uuid()
#
##################################
        $uuid = $tarantool->evaluate(
           "uuid = require('uuid')
            return (uuid.str())"
        )[0];
    } else {
       $uuid = $cookie;
    }
$time = time();
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
$agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';

# Create session or update user stat inside
###########################################################
#
# box.space.sessions:upsert({'111222333', 123456, 0, 'ip', 'agent'},
# {{'=', 2, 1}, {'+', 3, 1}, {'=', 4, 'ip'}, {'=', 5, 'agent'}})
#
###########################################################
  # Please check https://github.com/tarantool/tarantool-php/issues/111
   $tarantool->upsert("sessions", array($uuid, $time, 0, $ip, $agent),
        array (
            array(
                "field" => 1
                "op" => "=",
                "arg" => $time
            ),
            array(
                "field" => 2
                "op" => "+",
                "arg" => 1
            ),
            array(
                "field" => 3
                "op" => "=",
                "arg" => $ip
            ),
            array(
                "field" => 4,
                "op" => "=",
                "arg" => $agent
            )
        )
    );
    return($uuid);
}

function update_stats($vote, $click) {
    global $tarantool;

########################################################
#
# box.space.server:update(1, {{'+', 3, 1}, {'+', 4, 1}})
#
########################################################
   $tarantool->update("server",1,
        array (
            array(
                "field" => 2,
                "op" => "+",
                "arg" => $vote
            ),
            array(
                "field" => 3,
                "op" => "+",
                "arg" => $click
            )
        )
    );
}

function get_session($sid) {
    global $tarantool;

##########################################
#
# box.space.sessions:select('id')
#
#########################################
if (strlen($sid) > 16) {
    return $tarantool->select("sessions", $sid)[0][2];
    } else {
        return 0;
    }
}

function get_top($limit, $iterator) {
    global $tarantool;

######################################################################################
#
# box.space.stickers.index.secondary:select({primary}, {iterator = box.index.GE, offset=0, limit=10})
#
######################################################################################
    $result = $tarantool->select("stickers", null, 'secondary', $limit, 0, $iterator);
    return $result;
}
function get_server_stats() {
    global $tarantool;

    $time = time() - 30*86400; // one month before
    $stickers = $tarantool->call('box.space.stickers:count')[0][0];

    $tuple = $tarantool->select('server',1);

    $shows = $tuple[0][2];
    $votes = $tuple[0][3];

    $visitors = $tarantool->call('box.space.sessions.index.secondary:count',
       array($time, array('iterator' => Tarantool::ITERATOR_GE))
    )[0][0];

# $shows, $votes, $visitors) = get_server_stats();

    return array($stickers, $shows, $votes, $visitors);
}
?>


Осталось сделать HTML-шаблоны, которые будут выводить данные с картинками для голосования на сайт и страницы рейтингов.

Пример кода для картинок с голосованием
<!-- НАЧАЛО ГОЛОСОВАНИЯ -->
<div class="voting container">
  <div class="voting-zone">
<!-- ПЕРВЫЙ КОНТЕЙНЕР С КАРТИНКОЙ -->
    <div class="sticker" onclick="myFunction()">
    <form name="voteFormLeft" id="idForm" method ="POST" action="/" >
      <input class= "pic1" id="left_url" type="image" src="<?php echo $left_url?>" alt="Vote left" >
      <input type="hidden" name="vote_plus" value="<?php echo $left_token_plus?>">
      <input type="hidden" name="vote_minus" value="<?php echo $left_token_minus?>">
    </form>
  </div>
<!-- ВТОРОЙ КОНТЕЙНЕР С КАРТИНКОЙ -->
<div class="sticker" onclick="myFunction()">
    <form name="voteFormRight" id="idForm" method ="POST" action="/">
      <input class= "pic2" id="right_url" type="image" src="<?php echo $right_url?>" alt="Vote right" >
      <input type="hidden" name="vote_plus" value="<?php echo $right_token_plus?>" >
      <input type="hidden" name="vote_minus" value="<?php echo $right_token_minus?>" >
    </form>
</div>
  </div>
</div>
<!-- КОНЕЦ ГОЛОСОВАНИЯ -->


Кому-нибудь удалось повстречать в жизни дизайнера-верстальщика, который бы идеально отформатировал HTML-код?

Из вышеприведённого кода нетрудно догадаться, что мы выдали для левой и правой картинки одинаковую пару токенов, только поменяли местам vote_plus и vote_minus. (Обычно такими фразами автор как бы подчеркивает своё интеллектуальное превосходство над читателями). Таким образом, на какую бы картинку не кликнул пользователь, она получит плюс, а её конкурент — минус. Неудачник с каждым разом получает всё больше минусов и валится всё ниже в преисподнюю. Туда ему и дорога ХА – ХА – ХА!

Читателю, который добрался до конца статьи, вытерпел троллинг автора и не раз вскипал от его глупости, полагается заслуженная награда. Что может быть интереснее, чем реальный работающий пример, который можно потыкать и покликать? Приходи и голосуй за стикеры Telegram на сайт ugly.begetan.me, чтобы узнать, какой же из них самый уродский. Поковыряй мышкой наш русский хайлоад, собранный из NGNIX, PHP-FPM и Tarantool. И не забудь показать ссылку своей подружке, ведь необходимо достаточно много голосов, чтобы получить статистически-достоверную картину голосования.
Поделиться с друзьями
-->

Комментарии (11)


  1. oxidmod
    06.02.2017 17:02

    Немного не по теме, но все же спрошу. А не идет ли гдето в недрах mail.ru разработка чегото похожего на Doctrine ODM для тарантула? Хотелось бы попробовать, но останавливает необходимость писать поверх клиента свой костыль-орм


    1. danikin
      06.02.2017 17:48
      +1

      Мы пока размышляем на эту тему. Как вариант предлагаем вам попробовать написать на Lua прямо внутри Tarantool, используя его не только как СУБД, но и как сервер приложений.


    1. nekufa
      06.02.2017 18:16
      +4

      Мы для себя написали простую обёртку и используем её в продакшне.
      Посмотреть можно здесь: https://github.com/tarantool-php/mapper


  1. xotta6bl4
    06.02.2017 19:41
    +1

    А где нагрузочное тестирование?


  1. Begetan
    06.02.2017 22:22
    +3

    В былые времена, хабраюзеры рвали нагрузкой любой сайт. Сейчас и сайты крепче и юзеры спокойнее.

    Какие будут идеи кроме банального wrk на страницы top?


  1. Alexeyco
    07.02.2017 10:41
    +1

    А давно из PHP убрали лямбды и ООП?


    1. Begetan
      07.02.2017 19:34

      На Java, например, нужно писать строго в парадигме ООП, на php можно по-всякому. На самом цель статьи в том, что бы показать возможность работы с Тарантулом даже в простых скриптах. Отсюда и выбор инструмента.


  1. Alexeyco
    07.02.2017 10:49

    Еще сразу вопрос — а как преодолевается ограничение на кол-во записей в 65к в тупле?


    1. kostja
      07.02.2017 12:09

      Есть ограничение на 65k спейсов (таблиц). Количество полей в тапле не ограничено.


  1. shitware
    07.02.2017 18:21

    Было бы круто увидеть tarantool-csharp!