Начиная с версии 5.1 в MySQL реализована поддержка динамически подключаемых плагинов. А дистрибутив содержит примерный скелет кода под названием – example. Он описывает интерфейс и структуру базового обработчика – handler, копия которого создается отдельно для каждого соединения с БД. Также ему передаётся указатель на дескриптор таблицы TABLE *table и вспомогательный вектор TABLE_SHARE *share, используемый для синхронизации с другими обработчиками. Разработку плагина можно осуществлять по модульному принципу, реализуя только необходимые функции в первую очередь и закрывая заглушками более сложные операции.
Поскольку шаблон example описывает только интерфейс и не выполняет никаких операций, то в этом примере мы добавим в него реализацию CRUD-операций на основе одно-связного списка. Для этого необходимо написать всего 4 функции:
Начнём с того, что добавим заготовку для односвязного списка и несколько указателей — на голову, текущий, предыдущий и следующий элемент списка (в ha_smalldb.h):
Теперь перейдём к операции записи – write_row(uchar *buf). В качестве аргумента ей передаётся buf, который содержит в себе содержимое добавляемой строки во внутреннем представлении:
Поскольку мы будем сохранять данные в память без предварительной обработки, то можно просто скопировать содержимое строки целиком. Длину строки с метаданными можно получить из дескриптора таблицы table->s->reclength.
При чтении данных из таблицы используется последовательное сканирование с помощью связки — rnd_init и rnd_next. Оно используется если движок не поддерживает индексов или первичных ключей. Для начала в rnd_init() необходимо подготовить указатели.
Булевый аргумент scan сообщает о намерении БД произвольного (непоследовательного) чтения таблицы. Мы можем его спокойно проигнорировать, т.к. при попытке вызвать нереализованную команду rnd_pos(), БД получит в ответ HA_ERR_WRONG_COMMAND и догадается, что придётся читать попорядку.
После одиночного rnd_init() БД будет последовательно вызывать rnd_next(), пока не дойдет до конца таблицы — HA_ERR_END_OF_FILE. После каждого вызова память по адресу buf должна быть заполнена содержимым строки во внутреннем представлении.
И наконец завершим написанием delete_row() путём удаления текущего элемента из списка:
Вот и всё, теперь можно испытать мини-движок в деле.
Помимо INSERT и SELECT можно выполнять даже более сложные SQL-запросы, использующие операторы LIKE, INNER JOIN и пр., ведь оптимизация SQL – запросов реализована на уровне ядра БД до передачи управления движку, но её эффективность конечно же будет зависеть от поддерживаемого движком функционала.
Надеюсь этот материал позволит немного познакомиться с интерфейсом подключаемых модулей MySQL. Полностью код проекта можно скачать на github.
Для тех, кто хочет более серъезно изучить внутренности движков MySQL, но не рискует пока подступиться к InnoDB, советую обратить внимание на CSV или HEAP (Новое название — MEMORY). CSV добавляет возможность сохранения данных на диск в одноименном формате. А HEAP как следует из названия организует данные в кучу и реализует поддержку хэш-индексов. В обоих случаях приходится решать вспомогательную проблему высвобождения свободных участков памяти после удаления строк, которую нам удалось избежать за счёт использования односвязного списка.
Более подробное руководство о написании собственных движков можно найти в главе 22 официальной документации MySQL Internals Manual. Chapter 22. Writing a Custom Storage Engine.
P.S. И напоследок хочу порекомендовать утилиту ctags для тех, кто с ней не знаком. С её помощью довольно удобно изучать код, разбросанный по разным файлам, например она позволяет прыгнуть к определению функции или переменной по CTRL+]. Есть поддержка vim, emacs и других популярных редакторов. Здесь можно найти краткое руководство.
Поскольку шаблон example описывает только интерфейс и не выполняет никаких операций, то в этом примере мы добавим в него реализацию CRUD-операций на основе одно-связного списка. Для этого необходимо написать всего 4 функции:
- int rnd_init(bool scan);
- int rnd_next(uchar *buf);
- int write_row(uchar *buf);
- int delete_row(const uchar *buf);
Подготовка окружения
Шаблон example располагается в двух файлах ha_example.h и ha_example.cc в директории storage/example из исходного кода MySQL. В этом примере будет использован MySQL Community 5.5.35. Сначала скопируем example и переименуем в smalldb.
wget http://downloads.mysql.com/archives/get/file/mysql-5.5.35.tar.gz
tar -zxvf mysql-5.5.35.tar.gz
cd mysql-5.5.35
cp -rf storage/example storage/smalldb
cd storage/smalldb
sed -e 's/EXAMPLE/SMALLDB/g' -e 's/example/smalldb/g' ha_example.h > ha_smalldb.h
sed -e 's/EXAMPLE/SMALLDB/g' -e 's/example/smalldb/g' ha_example.cc > ha_smalldb.cc
sed -i 's/EXAMPLE/SMALLDB/g;s/example/smalldb/g' CMakeLists.txt
Начнём с того, что добавим заготовку для односвязного списка и несколько указателей — на голову, текущий, предыдущий и следующий элемент списка (в ha_smalldb.h):
class node{
public:
uchar* data; // Содержимое строки
node* next; // Указатель на следующую строку
node(){
data=NULL;
next=NULL;
}
~node(){
free(data);
}
};
node *first, *cur, *prev, *next;
int row_count, cur_pos;
void append_node(node* n){
if (first==NULL){
first=n;
}else{
node* iter=first;
while (iter->next!=NULL){iter=iter->next;}
iter->next=n;
}
}
Теперь перейдём к операции записи – write_row(uchar *buf). В качестве аргумента ей передаётся buf, который содержит в себе содержимое добавляемой строки во внутреннем представлении:
Поскольку мы будем сохранять данные в память без предварительной обработки, то можно просто скопировать содержимое строки целиком. Длину строки с метаданными можно получить из дескриптора таблицы table->s->reclength.
int ha_smalldb::write_row(uchar *buf)
{
DBUG_ENTER("ha_smalldb::write_row");
node* n=new node();
n->data = (uchar*) malloc(sizeof(uchar)*table->s->reclength);
memcpy(n->data,buf,table->s->reclength);
append_node(n);
row_count++;
DBUG_RETURN(0);
}
При чтении данных из таблицы используется последовательное сканирование с помощью связки — rnd_init и rnd_next. Оно используется если движок не поддерживает индексов или первичных ключей. Для начала в rnd_init() необходимо подготовить указатели.
int ha_smalldb::rnd_init(bool scan)
{
DBUG_ENTER("ha_smalldb::rnd_init");
cur=NULL;
prev=NULL;
next=first;
cur_pos=0;
DBUG_RETURN(0);
}
Булевый аргумент scan сообщает о намерении БД произвольного (непоследовательного) чтения таблицы. Мы можем его спокойно проигнорировать, т.к. при попытке вызвать нереализованную команду rnd_pos(), БД получит в ответ HA_ERR_WRONG_COMMAND и догадается, что придётся читать попорядку.
После одиночного rnd_init() БД будет последовательно вызывать rnd_next(), пока не дойдет до конца таблицы — HA_ERR_END_OF_FILE. После каждого вызова память по адресу buf должна быть заполнена содержимым строки во внутреннем представлении.
int ha_smalldb::rnd_next(uchar *buf)
{
int rc;
DBUG_ENTER("ha_smalldb::rnd_next");
MYSQL_READ_ROW_START(table_share->db.str, table_share->table_name.str, TRUE);
if (next!=NULL){
prev=cur;
cur=next;
next=cur->next;
memcpy(buf,cur->data,table->s->reclength);
cur_pos++;
rc=0;
}else{
rc= HA_ERR_END_OF_FILE;
}
MYSQL_READ_ROW_DONE(rc);
DBUG_RETURN(rc);
}
И наконец завершим написанием delete_row() путём удаления текущего элемента из списка:
int ha_smalldb::delete_row(const uchar *buf)
{
DBUG_ENTER("ha_smalldb::delete_row");
if (cur!=first){
free(cur);
prev->next=next;
}else{
free(cur);
cur=NULL;
first=next;
}
row_count--;
DBUG_RETURN(0);
}
Вот и всё, теперь можно испытать мини-движок в деле.
Установка
В MySQL 5.5 для сборки кода используется cmake. Для установки достаточно скомпилировать .so файл и скопировать его в директорию с плагинами установленной версии MySQL. Узнать её можно с помощью команды — SHOW VARIABLES LIKE "%plugin_dir%";
Если сама MySQL не установлена или установлена другая версия, то необходимо дополнительно установить БД с помощью make install.
Если сама MySQL не установлена или установлена другая версия, то необходимо дополнительно установить БД с помощью make install.
#Из корневой директории mysql-5.5.35:
cmake .
cd storage/smalldb
make
cp ha_smalldb.so /opt/mysql/server-5.5.35/lib/plugin/
#Из консоли MySQL:
INSTALL PLUGIN smalldb SONAME "ha_smalldb.so";
CREATE DATABASE small;
USE small;
CREATE TABLE tbl (a VARCHAR(255), b VARCHAR(255)) ENGINE=smalldb;
INSERT INTO tbl values ("Hello","Habr");
SELECT * FROM tbl;
Помимо INSERT и SELECT можно выполнять даже более сложные SQL-запросы, использующие операторы LIKE, INNER JOIN и пр., ведь оптимизация SQL – запросов реализована на уровне ядра БД до передачи управления движку, но её эффективность конечно же будет зависеть от поддерживаемого движком функционала.
Надеюсь этот материал позволит немного познакомиться с интерфейсом подключаемых модулей MySQL. Полностью код проекта можно скачать на github.
Для тех, кто хочет более серъезно изучить внутренности движков MySQL, но не рискует пока подступиться к InnoDB, советую обратить внимание на CSV или HEAP (Новое название — MEMORY). CSV добавляет возможность сохранения данных на диск в одноименном формате. А HEAP как следует из названия организует данные в кучу и реализует поддержку хэш-индексов. В обоих случаях приходится решать вспомогательную проблему высвобождения свободных участков памяти после удаления строк, которую нам удалось избежать за счёт использования односвязного списка.
Более подробное руководство о написании собственных движков можно найти в главе 22 официальной документации MySQL Internals Manual. Chapter 22. Writing a Custom Storage Engine.
P.S. И напоследок хочу порекомендовать утилиту ctags для тех, кто с ней не знаком. С её помощью довольно удобно изучать код, разбросанный по разным файлам, например она позволяет прыгнуть к определению функции или переменной по CTRL+]. Есть поддержка vim, emacs и других популярных редакторов. Здесь можно найти краткое руководство.