Наконец-то вместо уговоров подождать еще немного, на вопрос “Есть ли InterSystems GlobalsDB/Caché Extreme под Microsoft .Net?” можно ответить утвердительно. В новой версии Caché 2012.2 (Field Test) и GlobalsDB v2012.296 появилась поддержка этой платформы.
Попытаюсь в любимом для многих разработчиков на одной шестой суши стиле, то есть без чтения install notes и прочего, исследовать, что, собственно говоря, представляет дистрибутив GlobalsDB под Windows.
Качаем дистрибутив под Windows и .Net. Получаем globals_setup_2012.296.exe размером в 25 мегабайт. Запускается инсталлятор InstallShield. Запускал я его не в режиме администратора под Windows 7x64, работающий в VMWare Fusion под Mac OS X.
Установка прошла предсказуемо, в качестве домашней директории я указал C:usriscglobals. Для чистоты эксперимента надо сказать, что у меня уже стоит в этой операционке Caché.
Изучаю то, что поставилось. В usriscglobals три папки: bin, dev, mgr — стандартная для Caché структура. В bin лежат бинарники и либы самой СУБД, в mgr — конфигурация и файлы базы данных, плюс, что немаловажно — системный лог, в нашем случае globalsdb.log
Идем в .dev — папка с различным инструментарием для разработчика. Нас интересует .devdotnet, состоящая из bin, help и samples. В .bin лежит сборка InterSystems.GlobalsDB, в help замечаем chm аналог javadocs, в samples — проект Visual Studio c небольшим примером использования.
Жму на проект. Запускается моя Visual Studio 10 и начинает конвертацию проекта из 2008 в свой формат — разработчики InterSystems все еще сидят на восьмерке, хотя в новом релизе Caché есть уже сборки под .Net 4. Конвертация заканчивается успешно, запускаю Debug на исполнение — и о чудо, оно заработало!
Мое удивление вызвано тем, что в случае GlobalsDB для Java надо было запускать базу через .binglobalsdb start и для доступа к базе необходимо было взводить переменные окружения GLOBALS_HOME и PATH.
Исследую вопрос с помощью Sysinternals Process Explorer. Обнаруживаю шесть процессов cache.exe, стартовавших из C:usriscglobalsdb (это кстати и есть значение для GLOBALS_HOME). Смотрю на окружение моей застартованной Visual Studio — в нем есть уже GLOBALS_HOME и нужный путь в PATH — %GLOBALS_HOME%bin. Смотрю на список сервисов Windows — обнаруживаю GlobalsDB сервис. В общем инсталлятор позаботился о мелочах — хорошая тенденция. Кстати сказать, старт/стоп через команду %GLOBALS_HOME%binglobalsdb start или stop эквивалентен старту или стопу сервиса и наоборот.
Ну что же — консольное приложение работает, правда, пример скорее представляет собой набор тестов разработчика GlobalsDB, который методично проверял, что все перегрузки методов GlobalsDB API под .Net работают. Поэтому чтобы разобраться с API, создадим свой новый проект. Заодно посмотрим, что нужно сделать в VS, чтобы подцепить GlobalsDB.
Создаем новый проект под консольное приложение. Добавляем референс на InterSystems.GlobalsDB, который лежит в %GLOBALS_HOME%devdotnetbin. В моем случае это сборка C:usriscglobalsdevdotnetbinInterSystems.GlobalsDB.dll.
using System;
using System.Diagnostics;
using InterSystems.Globals;
namespace globals
{
class Program
{
static void Main(string[] args)
{
}
}
}
System.Diagnostics будем использовать для измерений скорости работы.
Теперь подсмотрим в примере, как делается соединение и перенесем в наш код:
static void Main(string[] args)
{
Connection connection = ConnectionContext.GetConnection()
connection.Close();
Console.ReadLine();
}
Запускаем. Получаем исключение An attempt was made to load a program with an incorrect format. Все понятно, мой проект по умолчанию собирает под x32. Идем в проектные Properties->Build и ставим Platform target в x64. Запускаем — все хорошо.
Идем дальше — попытаемся что-нибудь записать в базу. Это что-нибудь называется в GlobalsDB глобалами, или, используя полное название — global variables, то есть глобальные переменные Caché, которые в отличие от локальных, живущих в памяти конкретного процесса базы данных, живут на диске и таким образом доступны всем подключениям к базе данных. В GlobalsDb мы можем работать только с глобалами.
Глобалы каждый воспринимает в силу своей испорченности — кто-то называет их многомерными переменными, другие видят их как деревья или key-value хранилища, мне ближе представлять их как ассоциативные массивы или MultiMap. Если не предаваться фантазиям, то глобал — это атомарная структура хранения данных в Caché (например атомарной структурой для реляционок будет таблица), которая имеет имя, состоит из элементов, называемых узлами. Каждый узел состоит из левой части, которая называется сабскриптом (часто ее называют индексом или ключом) и правой части — собственно хранимыми данными, которые могут быть либо простым типом (числа, строки), либо списком, элементы которого опять же либо простые типы либо списки.
Примеры глобалов (псевдокод).
a. Для тех, кто думает, что это key value или коллекция с доступом по ключу:
test(1) = “some text or number limited to 3641144 bytes”
test(“any string limited to 500 bytes”) = 3.14
b. Для тех, кто думает, что это дерево:
test(1,”post”) = “root”
test(1,2,”post”) = “branch A”
test(1,3,”post”) = “branch B”
c. Пустой сабскрипт тоже сабскрипт:
test = 2
d. Похожее на табличное хранение данных с использованием списков:
orders(1)=list(1001,”20.12.2012”,3,,999.99)
orders(2)=list(1002,”04.12.2012”,4,,249.99)
Попробуем теперь сохранить данные в глобал. Для этого нужно указать имя глобала, создать новый узел глобала, указать для узла сабскрипт (ключ или индекс, как вы видите из примера) и данные, которые хранятся в этом узле. Для работы с узлами глобалов в GlobalsDB API используется объект класса NodeReference. Фактически это указатель на некоторый узел в глобале. Так как узел идентифицируется своим сабскриптом, то можно считать, что NodeReference это ссылка на некоторый сабскрипт. Ссылку на глобал, которая первоначально указывает на головной узел, мы получаем через соединение:
NodeReference testArray = connection.CreateNodeReference(«test»); //global name is test
Для начала используем простой метод NodeReference.Set(). В обобщенном виде его можно воспринимать как NodeReference.Set(data,subscript) и соответственно после его исполнения у нас появляется узел глобала в базе такого вида:
globalName(subscript) = data
Таким образом, код выглядит вот так:
static void Main(string[] args)
{
Connection connection = ConnectionContext.GetConnection();
NodeReference node = connection.CreateNodeReference("test");
node.Set("some text or number limited to 3641144 bytes",1);
node.Set(3.14, "any string or primitive type or several values limited to 500 bytes");
connection.Close();
Console.ReadLine();
}
Bыполняем. Получаем исключение: Must connect before calling Connection.CreateNodeReference(). Все верно, смотрим код примера — за установление соединения мы отвечаем сами, а не фабрика. Почему так? Не знаю. Тут можно заметить одно, что есть вопросы, связанные с многопотоковой работой с соединением и на будущее об этом важно помнить в случае необходимости. Добавляем установление соединения:
if (!connection.IsConnected()) connection.Connect();
Теперь все хорошо. Замечаем, что работают хинты для GlobalsDB API. Добавляем примеры на работу с многомерным сабскриптом и на предельные значения для сабскрипта и данных справа:
static void Main(string[] args)
{
Connection connection = ConnectionContext.GetConnection();
if (!connection.IsConnected()) connection.Connect();
NodeReference node = connection.CreateNodeReference("test");
node.Set("some text or number limited to 3641144 bytes",1);
node.Set(3.14, "any string limited to 500 bytes");
node.Set("root",new object[2] {1,"post"}) ;
node.Set("branch A",new object[3] {1,1,"post"}) ;
node.Set("branch B",new object[3] {1,2,"post"}) ;
node.Set(2);
node.Close();
node = connection.CreateNodeReference("limit");
String longestKey = new String('k', 500);
String longestValue = new String('v', 3641144);
node.Set(longestValue, longestKey);
connection.Close();
Console.ReadLine();
}
Как посмотреть, что произошла вставка узлов глобалов в базу? Можно воспользоваться одним из методов чтения NodeReference. Однако для отладки можно использовать такой прием, не документированный в GlobalsDB, но знакомый тем, кто работал с Caché (тем более, что в поставке GlobalsDB утилит администрирования пока нет). Итак, идем в %GLOBALS_HOME% и в командной строке выполняем:
C:usriscglobalsbin>cache -s ..mgr -U Data
DATA>_
В результате мы получаем терминальную сессию к базе данных. Что это такое? Это стандартный процесс базы данных, запущенный в терминальном режиме, который позволяет исполнять нам команды на языке Caché Object Script. Независимые процессы, работающие с базой данных — это одна из особенностей Caché, которая позволяет использовать ее как сервер приложений — запускать процессы, где будет работать, к примеру, пользовательский код приложения. В случае GlobalsDB таким процессом становится процесс CLR, в котором исполняется наш .Net код.
Как посмотреть созданные узлы? Для этого есть команды write (сокращенно w) и zwrite (zw). Пример:
DATA>zw ^test
^test=2
^test(1)="some text or number limited to 3641144 bytes"
^test(1,1,"post")="branch A"
^test(1,2,"post")="branch B"
^test(1,"post")="root"
^test("any string limited to 500 bytes")=3.1400000000000001243
Удаление узла или всего глобала — команда kill (сокращенно k):
DATA>kill ^test
Запись данных:
DATA>s ^test(1,"name") = "Athens"
Вставка в цикле:
DATA>for i=1:1:100 { s ^test(i) = i }
Выход из терминала — команда halt (сокращенно h):
DATA>h
C:usriscglobalsbin>
Замер времени:
DATA>s st=$zh for i=1:1:100000 { s ^test(i)=i} w $zh-st,!
.064444
Теперь попробуем вставить 100 000 записей из .Net и засечь время:
static public void testInserts(int loop)
{
Connection connection = ConnectionContext.GetConnection();
if (!connection.IsConnected()) connection.Connect();
NodeReference node = connection.CreateNodeReference("test");
node.Kill();
Stopwatch dbTimer = Stopwatch.StartNew();
for (int i = 0; i < loop; i++)
{
node.Set(i+" item", i);
}
dbTimer.Stop();
Console.WriteLine(dbTimer.ElapsedMilliseconds + " milliseconds to save " + loop + " items");
Console.WriteLine("It is " + dbTimer.Elapsed.Seconds + " seconds");
node.Close();
connection.Close();
}
Результаты для серии тестов:
for (int i = 100000; i <= 1100000; i += 200000) testInserts(i);
ConnectionContext.GetConnection().Close();
C:srcglobalsglobalsbinRelease>.globals.exe
430 ms to save 100000 items. 232558 records/second
1223 ms to save 300000 items. 245298 records/second
1958 ms to save 500000 items. 255362 records/second
2754 ms to save 700000 items. 254175 records/second
3559 ms to save 900000 items. 252880 records/second
4436 ms to save 1100000 items. 247971 records/second
Более 200 000 записей в секунду. Все это происходит c включенным журналированием и в транзакции. С другой стороны, данные носят совсем макетный характер. С третьей, это происходит под виртуалкой, у которой осталось 5 метров свободного дискового пространства. С четвертой, на таких скоростях все очень сильно зависит от конкретики — размеры записей и случайность ключа, интенсивность потока транзакций, возможность обрабатывать попакетно, необходимость в транзакциях и тд.
Для начала достаточно — GlobalsDB установлен, проект работает, сделаны общие выводы по быстродействию.
В дальнейшем разберем такие важные темы, как навигация по узлам глобалов и операции обхода узлов, работа со списками, транзакции, счетчики, блокировки. Отдельно рассмотрим интересный вопрос, как GlobalsDB ведет себя при работе с несколькими потоками.
Автор: 0leo