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

GlobalsDBЧасть 1.

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

Будем реализовывать схемы хранения как в Redis, memcached, Cassandra, Neo4, SimpleDB, MongoDB

Перед тем как мы начнём моделировать различные виды 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).

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

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