GlobalsDB — универсальная NoSQL база данных. Часть 2

от автора

Часть 1.

Моделируем 4 вида NoSQL-баз с помощью GlobalsDB

Перед тем как мы начнём моделировать различные виды NoSQL-баз, давайте взглянем на глобалы чуть более детатально и определим некоторые термины, которые будем использовать позднее.

При сохранении данных в элементе глобала используются 3 компонента:

  • Имя глобала
  • Индексы (ноль, один или несколько). Они могут быть текстовыми или численными.
  • Значение (которое, собственно, и хранится в элементе глобала). Оно м.б. текстовым или численным

Эти три компонента часто записываются как N-арная реляционная переменная следующим образом:

globalName[subscript1, subscript2, ..subscriptN] = value 

Это комбинация имени, индексов и значения известна как элемент глобала (Global Node) и является единицей хранения. Глобал состоит из множества его элементов, а база данных состоит из множества глобалов.

Важным свойством глобалов является то, что одиночный глобал может содержать узлы с различным числом индексов, например:

myGlobal["a"] = 123 myGlobal["b", "c1"] = "foo" myGlobal["b", "c2"] = "foo2" myGlobal["d", "e1", "f1"] = "bar1" myGlobal["d", "e1", "f2"] = "bar2" myGlobal["d", "e2", "f1"] = "bar1" myGlobal["d", "e2", "f2"] = "bar2" myGlobal["d", "e2", "f3"] = "bar3" 

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

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

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

Также в рамках БД нет явной схемы или структуры данных, которые хранятся в глобалах. Способ хранения данных в глобалах определяется на уровне приложения.

Элементы в глобалах создаются командой set. Точный синтаксис этой команды зависит от используемого API.

Так, для Node.JS API мы можем двумя способами создавать элементы один и тот же элемент глобала.

Асинхронно:

var globalNode = {   global:"myGlobal",   subscripts: ["b","c3"],   data: "Rob" }; db.set(globalNode, function(error,results) { // etc }); 

или синхронно:

var globalNode = {   global:"myGlobal",   subscripts: ["b","c3"],   data: "Rob" }; db.set(globalNode); 

Одним из интересных свойств GlobalsDB является то, можно широко использовать синхронное программирование без вреда для производительности, что упрощает работу с ней и позволяет использовать ОО-синтаксис в полном объёме внутри JavaScript.

Это возможно из-за уникальной производительности GlobalsDB: она работает в связке c NodeJS как подлинкованный процесс в оперативной памяти (в отличие от многих других NoSQL-баз, которые работают через различные сетевые сокеты), и в сочетании с глубокой оптимизацией, которая, как уже было озвучено, даёт производительность как у баз в памяти в RAM. Подобного сочетания свойств нет у самых популярных NoSQL-баз.

Примечание переводчика: Однако если нужна именно сетевая NoSQL-база, то сделать её на том же Node.JS + GlobalsDB нетрудно — достаточно написать API на основе JSON, например.

Вызов вышеописанной команды приведёт к вставке элемента в нашу иерархию и дерево приобретёт новый вид:

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

1) Хранилище Ключ/Значение

Реализация хранилища типа Ключ/Хначение на глобалах элементарна. Мы создаём его используя следующую структуру:

keyValueStore[key] = value 

Например:

telephone["211-555-9012"] = "James, George" telephone["617-555-1414"] = "Tweed, Rob" 

В виде иерархического дерева эта структура выглядит так:

Всё, хранилище типа Ключ/Значение реализовано. Однако с помощью глобалов мы можем пойти дальше и сохранять несколько аттрибутов для одного ключа. Например, так:

Telephone[phoneNumber, "name"] == value Telephone[phoneNumber, "address"] == value 

Пример с конкретными данными:

telephone["211-555-9012", "name"] = "James, George" telephone["211-555-9012", "address"] = "5308, 12th Avenue, Brooklyn" telephone["617-555-1414", "name"] = "Tweed, Rob" telephone["617-555-1414", "address"] = "112 Beacon Street, Boston" 

Мы создали иерархическое дерево, которое выглядит так:

Вот код на Node.JS API для создания первой записи в этом улучшенном хранилище типа ключ/значение:

var gnode = {   global: "telephone",   subscripts: ["617-555-1414", "name"],   data: "Tweed, Rob" }; db.set(gnode); 

NoSQL-базы обычно не представляют автоматических способов для индексирования данных. Однако в БД на глобалах, если вам нужен доступ к данным по альтернативному ключу, то достаточно создать второй глобал, в котором в качестве ключа может выступать любое поле.

Например, если нам нужен доступ к данным по полю name, мы должны создать индексный глобал и обновлять его вместе с глобалом telephone. Проектирование и добавление индексов полностью лежит на вас, как разработчике, но это очень просто.

Для поддержки индекса по полю name каждый раз при добавлении записей в глобал telephone мы будем создавать элемент в глобале nameIndex:

nameIndex[name, phoneNumber] = "" 

nameIndex["James, George", "211-555-9012"] = "" nameIndex["Tweed, Rob", "617-555-1414"] = "" 

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

На диаграмме показаны глобал с телефонными данными и индексный глобал. Пунктирной линией показаны неявные отношения между индексом и основным глобалом:

Индексный глобал позволяет нам получить доступ к данным по имени, в то время как основной глобал предоставляет доступ по телефонному номеру.

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

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

Если нам нужно создать телефонный справочник из наших данных, то мы можем последовательно обойти элементы глобала nameIndex и получить адреса из глобала telephone с помощью метода get().

Метод-итератор для обхода — это функция order. Вот пример на Node.js API:

gnode = {   global: "nameIndex",   subscripts:["James, George"] }; var nextName = db.order(gnode).result; 

Этот код должен вернуть индекс элемента, следующего за элементом с индексом «James, George» в данном глобале. То есть:

nextName = "Tweed, Rob" 

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

Принципиально мы рассмотрели все пути использования глобалов в качестве простых хранилищ типа Ключ/Значение. Кстати, наше хранилище может быть перепроектировано для использования всего лишь одного глобала и для данных, и для индексов. Для этого нужно добавить ещё один индекс первого уровня, например так:

telephone["data", phoneNumber, "address"] = address telephone["data", phoneNumber, "name"] = name telephone["nameIndex", name, phoneNumber] = "" 

Поскольку физическая реализация глобалов скрыта от нас абстрактным уровнем, мы можем создавать структуры на глобалах в точности соответствующими нашим нуждам. Однако, если хранилища типа ключ/значение начинают расти до огромных размеров, то необходимо учитывать как та или иная структура способствует или препятствует администрированию БД (резервное копирование и восстановление, максимальный размер БД, распределение между различными шард-серверами). Учёт этих факторов может повлиять на решение о том, хранить ли все данные в одном или создать несколько глобалов.

Другие типы хранилищ Ключ/Значение

Если мы взглянем на такое Ключ/Значение-хранилище как Redis, то увидим, что оно предлагает несколько других способов хранения данных. Каждый из этих способов может быть очень просто реализован на глобалах.

Списки

Списки в Redis являются связанными. Можно поместить значения в список и извлечь значения из списка, получить подсписок и т.п.

Модель подобного списка на глобалах очень проста. Например, можно использовать следующую структуру:

list[listName, "firstNode"] = nodeNo list[listName, "lastNode"] = nodeNo list[listName, "node", nodeNo, "value"] = value list[listName, "node", nodeNo, "nextNode"] = nextNodeNo list[listName, "node", nodeNo, "previousNode"] = prevNodeNo 

Например, связанный список под названием myList, содержащий последовательность значений:

  • Rob
  • George
  • John

может быть представлен как:

list["myList", "firstNode"] = 5 list["myList", "lastNode"] = 2 list["myList", "nodeCounter"] = 5 list["myList", "node", 2, "previousNode"] = 4 list["myList", "node", 2, "value"] = "John" list["myList", "node", 4, "nextNode"] = 2 list["myList", "node", 4, "previousNode"] = 5 list["myList", "node", 4, "value"] = "George" list["myList", "node", 5, "nextNode"] = 4 list["myList", "node", 5, "value"] = "Rob" 

или графически:

На этом рисунке видна разреженная природа глобалов. Номер элемента списка — это последовательное целое число. Элемент 5 на данный момент первый элемент в списке, поэтому он имеет аттрибут nextNode в котором хранится следующий элемент списка и не имеет аттрибута previousNode для предыдущего элемента списка.

Средний элемент списка под номером 4 имеет аттрибуты для хранения номерров предыдущего и последующего элементов.

Каждая операция, которая изменяет связанный список (вставляет, извлекает, удаляет, укорачивает и т.п.), должна изменить несколько элементов внутри этого списка, например:

  • сбросить указатель на первый или последний узел
  • добавить или удалить значение элемента
  • установить корректные значения следующего и предыдущего элементов, чтобы вставить или удалить элемент из списка

Например, чтобы вставить новое имя «Chris» в начало списка мы должны изменить глобал, где хранится список так:

list["myList", "firstNode"] = 6 list["myList", "lastNode"] = 2 list["myList", "nodeCounter"] = 6 list["myList", "node", 2, "previousNode"] = 4 list["myList", "node", 2, "value"] = "John" list["myList", "node", 4, "nextNode"] = 2 list["myList", "node", 4, "previousNode"] = 5 list["myList", "node", 4, "value"] = "George" list["myList", "node", 5, "nextNode"] = 4 list["myList", "node", 5, "previousNode"] = 6 list["myList", "node", 5, "value"] = "Rob" list["myList", "node", 6, "nextNode"] = 5 list["myList", "node", 6, "value"] = "Chris" 

Графическая схема изменений (то что изменилось подсвечено):

Для обхода списка мы должны начать с первого элемента и рекурсивно переходить от элемента к элементу по номеру в поле nextNode, до тех пор пока у очередного элемента мы не найдём данного поля:

Чтобы найти число элементов в списке мы можем выполнить его обход, или, для максимальной производительности, хранить это число в отдельном элементе глобала и обновлять при изменении списка:

List["myList", "count"] = noOfNodes 

Понятно, что мы должны написать эти операции как методы, чтобы легко и корректно манипулировать элементами в списке, но это весьма простая задача.

Множества (Sets)

Множества в Redis это неупорядоченный набор строк. Мы можем легко смоделировать их на глобалах:

theSet[setName, elementValue] = "" 

Вы можете заметить, что это в точности совпадает со способом задания индексов, рассмотренным ранее. А вот так можно добавить элемент в множество:

Set: theSet["mySet", "Rob"] = "" 

Удаление элемента из множества:

Kill: theSet["mySet", "Rob"] 

Для определения вхождения элемента в множество мы можем использовать команду data. Она вернёт 1, если элемент входит в множество, и 0, если нет.

Data: theSet["mySet", "Rob"] → 1 Data: theSet["mySet", "Robxxx"] → 0 

В Node.js API мы можем использовать метод data

gnode = {   global: "theset",   subscripts: ["mySet", "Rob"] }; var exists = db.data(gnode).defined; 

В этом примере переменная exists получит значение 1.

Мы можем использовать упорядоченность элементов глобала, чтобы отобразить члены множества в алфавитной последовательности.

При использовании глобалов нет никакой значимой разницы при моделировании Redis-множеств set и zset.

Хэши.

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

Hash[hashName, value] = "" 

2) Табличные (или колоночные) хранилища

Табличные или колоночные NoSQL-базы такие как BigTable, Cassandra и Amazon SimpleDB позволяют сохранять данные в разреженных таблицах, подразумевая что каждая строка может содержать значения в некоторых, но необязательно во всех, столбцах.

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

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

columnStore[columnName, rowId] = value 

Пример с конкретными данными:

user["dateOfBirth", 3] = "1987-01-23" user["email", 1] = "rob@foo.com" user["email", 2] = "george@foo.com" user["name", 1] = "Rob" user["name", 2] = "George" user["name", 3] = "John" user["state", 1] = "MA" user["state", 2] = "NY" user["telephone", 2] = "211-555-4121" 

В виде диаграммы:

Опять нам пригодилась разреженная природа глобалов. Вышеописанный глобал представляет следующую таблицу:

name telephone email dateOfBirth state
1 Rob rob@foo.com MA
2 George 211-555-4121 george@foo.com NY
3 John 1987-01-23

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

userIndex["byRow", rowId, columnName] = "" userIndex["byValue", value, columnName, rowId] = "" 

3) Документо-ориентированное хранилище

Документо-ориентированные NoSQL БД такие как CouchDB и MongoDB хранят множество пар ключ/значение и вложенные множества множеств различных аттрибутов.

Обычно используются JSON или JSON-подобные структуры для представления «документов» хранящихся в этих БД. GlobalsDB автоматически отображает JSON-документы или объекты в глобалы.

Для примера рассмотрим JSON-документ:

{key:"value"} 

Он может быть смоделирован на глобалах как:

Document["key"] = "value" 

В GlobalsDB мы мы можем создать этот документ так:

var json = {   node: {     global: "Document",     subscripts: []   },   object: {     key: "value"   } }; db.update(json, 'object'); 

И получить его из БД так:

var json = db.retrieve({global: 'Document'},"object"); console.log("Document = " + JSON.stringify(json.object)); 

Давайте возьмём более сложный документ:

{this:{looks:{very:"cool"}}} 

Его можно представить следующим элементом глобала:

Document["this", "looks", "very"] = "cool" 

А создать его можно так:

var json = {   node: {     global: "Document",     subscripts: []   },   object: {     this: {       looks: {         very: "cool"       }     }   } }; db.update(json, "object"); 

А что насчёт массива?

["this","is","cool"] 

Он может быть представлен так:

document[1] = "this" document[2] = "is" document[3] = "cool" 

GlobalsDB для создания и получения отображения данных на глобалы использует объекты, а не массивы. Поэтому для сохранения массива мы напишем так:

var json = {   node: {     global: "Document",     subscripts: []   },   object: {     1: "this",     2: "is",     3: "cool"   } }; db.update(json, "object"); 

Для получения массива из БД:

var json = db.retrieve({global: "Document"}, "object"); console.log("Document = " + JSON.stringify(json.object)); Document = {"1":"this", "2":"is", "3":"cool"} 

Приведём более сложный JSON-документ:

{   "age": "26",   "contact": {     "address": {       "city": "Boston",       "street": "112 Beacon Street"     },     "cell": "617-555-1761",     "email": "rob@foo.com",     "telephone": "617-555-1212"   },   "knows": {     "1": "George",     "2": "John",     "3": "Chris"   },   "medications": {     "1": {       "dose": "5mg",       "drug": "Zolmitripan"     },     "2": {       "dose": "500mg",       "drug": "Paracetemol"     }   },   "name": "Rob",   "sex": "Male" } 

Он будет отображён на глобалы так:

person["age"] = 26 person["contact", "address", "city"] = "Boston" person["contact", "address", "street"] = "112 Beacon Street" person["contact", "cell"] = "617-555-1761" person["contact", "eMail"] = "rob@foo.com" person["contact", "telephone"] = "617-555-1212" person["knows", 1] = "George" person["knows", 2] = "John" person["knows", 3] = "Chris" person["medications", 1, "drug"] = "Zolmitripan" person["medications", 1, "dose"] = "5mg" person["medications", 2, "drug"] = "Paracetamol" person["medications", 2, "dose"] = "500mg" person["name"] = "Rob" person["sex"] = "Male" 

Или графически:

Мы можем создать этот документ в GlobalsDB так:

var json = {   node: {     global: "person",     subscripts: []   },   object: {     name: "Rob",     age: 26,     knows: {       1: "George",       2: "John",       3: "Chris"     },     medications: {       1: {         drug: "Zolmitripan",         dose: "5mg"       },       2: {         drug: "Paracetemol",         dose: "500mg"       }     },     contact: {       email: "rob@foo.com",       address: {         street: "112 Beacon Street",         city: "Boston"       },       telephone: "617-555-1212",       cell: "617-555-1761"     },     sex: "Male"   } }; db.update(json, "object"); 

И получить так:

var json = db.retrieve({global: "person"}, "object").object; 

4) Графовые базы данных

NoSQL-базы такие как Neo4j используются для представления сложных сетей взаимосвязей в терминах узлов и связей между ними (т.н. рёбер) с помощью пар ключ/значение связывающих узлы и рёбра.

Классическое использование графовой БД это представление социального графа. Давайте рассмотрим следующий пример:

В этом примере стрелками показано какие пользователи знают (know) о других. С помощью глобалов он может быть представлен так:

person[personId, "knows", personId] = "" person[personId, "knows", personId, key) = value person[personId, "name"] = name 

Время состояния «knows» («знает») может быть вычислено из временного штампа, который сохраняется при первичном создании связи, например так:

person[1, "knows", 2] = "" person[1, "knows", 2, "disclosure"] = "public" person[1, "knows", 2, "timestamp"] = "2008-08-16T12:23:01Z" person[1, "knows", 7] = "" person[1, "name"] = "Rob" person[2, "name"] = "John" person[7, "knows", 2] = "" person[7, "knows", 2, "disclosure"] = "public" person[7, "knows", 2, "timestamp"] = "2009-12-16T10:06:44Z" person[7, "name"] = "George" 

или графически (красные точечные пунктирные линии показывают отношения «know» между пользователями в этой модели):

Если говорить об общем случае графовой БД, модель будет представлять узлы и рёбра (edge) между ними. Примерно так:

node[nodeType, nodeId] = "" node[nodeType, nodeId, attribute] = attributeValue edge[edgeType, fromNodeId, toNodeId] = "" edge[edgeType, fromNodeId, toNodeId, attribute] = attributeValue edgeReverse[edgeType, toNodeId, fromNodeId] = "" 

Таким образом, разреженная природа и гибкость глобалов позволяет очень органично и просто определять сложные графовые БД.

5) Модели других баз данных

Моделирование на глобалах не ограничивается только NoSQL-моделями данных. Они также могут быть использованы для моделирования:

  • XML DOM/Native XML-баз данных. GlobalsDB отлично подходит для работы с сохраняемыми XML DOM файлами. XML-документ, по существу, это граф, который представляет узлы различных типов и отношения между ними (например firstChild, lastChild, nextSibling, parent и т.п.). В сущности это позволяет GlobalsDB выступать в роли Native XML-базы данных. Модуль для Node.js ewdDOM — одна легковесных реализаций такой БД.
  • Реляционные таблицы. Caché моделирует реляционные таблицы на глобалах, таким образом что можно использовать стандартные SQL-запросы. Т.е. GlobalsDB можно рассматривать как основу для NoSQL-движка, а в Caché добавлены возможности NOSQL (т.е Not-Only SQL — не только SQL) базы данных.
  • Объектная БД. Caché моделирует на глобалах объекты, а также предоставляет прямое отображение между объектами и реляционными таблицами. Наверное, теперь вы понимаете как это реализовано.

В отличие от хорошо известных NoSQL-баз, GlobalsDB это не жестко-специализированная БД. Она одновременно имеет множество свойств. Так GlobalsDB может поддерживать любые из вышеописанных типов баз данных. И даже одновременно, если потребуется.

Это похоже как будто у вас есть Redis, CouchDB, SimpleDB, Neo4j и Native XML БД запущенные в одной базе данных и в одно и тоже время!

Если вы заинтересованы в NoSQL-базе работающей с Node.JS (а также .NET, Java), вам необходимо взглянуть в сторону GlobalsDB. Это воистину Универсальная NoSQL база данных!

Заключение

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

Секретный соус — конечно, реализация. Если она сделана корректно, применены мудрые проектные решения, то достигнутая производительность поразительна.

Благодарность
Эта статья представляет собой адаптацию статьи Роба Твида и Джорджа Джеймса «Универсальная NoSql база данных на основе проверенной и протестированной технологии» (2010).

ссылка на оригинал статьи http://habrahabr.ru/company/intersystems/blog/185472/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *