S neustále se zrychlujícím rozvojem vědy a techniky roste i množství dat, které je nutné zpracovávat pokud možno v reálném čase a často se stává, že určité projekty vyžadují výkon, který přesahuje hranice běžných databází. Takovým projektem je například projekt Evropské kosmické agentury (ESA) Gaia, jenž má za úkol pořizovat soupis vesmírných objektů vyskytujících se v naší galaxii a zaznamenávat jejich polohu, pohyb a změny jasu. Technologie, která byla vybrána pro tento projekt, se nazývá Caché eXTreme a účelem článku je seznámit s ní čtenáře.

Caché eXtreme je sada technologií firmy InterSystems, které zpřístupňují vlastnosti vysoce výkonné objektové databáze prostředkům programovacího jazyka Java k vytváření XTP (eXtreme Transaction Processing) aplikací. Pro dosažení maximálního výkonu používá Caché eXTreme in-proces verzi databáze Caché, implementovanou pomocí Java JNI (Java Nativ Interface) a Caché call-in API.

Samotný framework se skládá z následujících komponent:

  • API vrstvy eXTreme Globals nabízí přímý přístup k uloženým datům s maximální flexibilitou a rychlostí.

  • eXTreme Event Persistence (XEP) umožňuje projekci jednoduchých objektů Javy do XTP persistentních událostí, které lze extrémně rychle uložit a zpracovávat. Jedná se o odlehčené API pro tzv. „low-latency objects“ (objekty s krátkou dobou přístupu).

  • eXTreme Dynamic Object (XDO) API poskytuje objektově orientovaný přístup k datům, která jsou uložena v databázi Caché. Přístup k datům je dynamický v tom smyslu, že třídy popisující uložené objekty nemusejí být známy v době kompilace zdrojového kódu Javy a zároveň neprobíhá ani žádné generování kódu.

Výše vyjmenované komponenty jsou navrženy tak, aby byly použitelné s platformami, jako jsou např. JCACHE, OSGi, CEP nebo JMS:

Cache

 

Instalace a nastavení prostředí

Aby bylo možné otestovat a plně využít prostředky Caché eXTreme, je potřeba si nainstalovat Caché verze 2010.2 a vyšší, s volbou zabezpečení „minimal“ nebo „normal“, a Java JDK 1.6 a vyšší. Pro jednotlivá prostředí je potřeba nastavit příslušné proměnné prostředí. V následujícím seznamu je uveden seznam proměnných a jejich nastavení pro prostředí Windows:

  • GLOBALS_HOME cesta do složky, kde je nainstalováno Caché

  • PATH musí obsahovat cestu do GLOBALS_HOME/bin

  • CLASSPATH musí obsahovat plnou cestu k níže uvedeným jar archivům, které jsou standardně uloženy ve složce GLOBALS_HOME\dev\java\lib\JDK16\:

    • CacheDB.jar, CecheeXTreme.jar, CacheJDBC.jar

Seznam proměnných prostředí a jejich nastavení pro platformu UNIX (Linux) je možné nalézt v dokumentaci Caché.

Globály a Caché

Jádro databáze Caché představuje mimořádně efektivní stroj vícerozměrných dat. Tyto vícerozměrné struktury (vícerozměrná pole), známé pod názvem „globály“, se používají zvláště tehdy, jedná-li se o neobvyklé nebo velmi specializované struktury dat nebo je-li požadovaný nejvyšší možný výkon.

API rozhraní vrstvy Globals, které je plně dokumentováno, umožňuje aplikacím psaných v Javě manipulovat s globály, a je plně optimalizované pro extrémně rychlé ukládání a čtení několika základních datových typů – (int,long,double,byte[],String a ValueList). Toto rozhraní je využíváno i vrstvou XDO pro rychlý přístup k datům uloženým jako objekty Caché.

Globál v Caché, obdobně jako jiná řídká pole, je stromová datová struktura. Základní koncept globálů může být ilustrován na analogii ke jmenné konvenci balíčků v Javě. Podobně jako třídy v Javě, jsou prvky vícerozměrných polí jednoznačně identifikovány jmenným prostorem obsahujícím libovolný počet identifikátorů. Na všechny jmenné prostory ve struktuře globálů se odkazujeme jako na „uzly“. Uzel musí mít buď potomka, nebo obsahovat data, nebo oboje. Za název globálu bývá považován identifikátor kořenového uzlu a je považován i za identifikátor celého globálu, všechny ostatní identifikátory jmenného prostoru jsou považovány za indexy. Úplnému jmennému prostoru uzlu (název globálu plus index) se říká „adresa uzlu“. V notaci Caché je adresa uzlu reprezentována znakem „stříška“ (^), následovaným názvem globálu a čárkou odděleným seznamem indexů, který je uzavřen v kulatých závorkách.

Data do globálu mohou byt ukládána s libovolným počtem indexů. Indexy navíc nemají definován typ, a mohou tedy obsahovat data libovolného typu. Jeden index může být celé číslo, např. 34, zatímco dalším indexem je smysluplný název, např. PoložkaŘádku. To platí dokonce i na téže úrovni indexu.

Uvažujme aplikaci pro inventuru stavu zásob, která poskytuje např. informace o druhu, velikosti, barvě a vzoru. Globál obsahující tyto informace může mít následující strukturu:


^Zasoba(<polozka>,<velikost>,<barva>,<vzor>) = <množství>

 

Globál pak může být naplněn konkrétními daty takto:

 

^Zasoba("cvičební úbor",4,"modrá","květinový")=3

 

Díky přístupu přes datový uzel v této struktuře je velmi jednoduché určit, zda je na skladě cvičební úbor velikosti 4 s květinovým vzorem. Požaduje-li zákazník cvičební úbor velikosti 4 a není si jist barvou či vzorem, je velmi jednoduché zobrazit jejich úplný seznam tak, že cyklicky projdeme všechna data, která jsou uložena v uzlech:

 

^Zasoba("cvičební úbor",4)

 

Všechny datové uzly v uvedeném příkladu byly stejné povahy (uchovávaly množství) a všechny byly uloženy na stejné úrovni indexování (čtyři indexy) s podobnými indexy (třetí index byl vždy text vyjadřující barvu). Ale nemusí tomu tak být. Datové uzly mohou mít odlišný jak počet, tak typ indexů a mohou obsahovat rozdílné typy dat. Níže je uveden příklad složitějšího globálu s daty faktury, obsahující různé typy dat uložené na různých úrovních indexování:


^Faktura(<c faktury>,"Zákazník") = <Informace o zákazníkovi>
^Faktura(<c faktury>,"Datum") = <Datum faktury>
^Faktura(<c faktury>,"Položky") = <Počet položek faktury>
^Faktura(<c faktury>,"Položky",1,"Číslo") = <objednací číslo první položky>
^Faktura(<c faktury>,"Položky",1,"Množství") = <množství první položky>
^Faktura(<c faktury>,"Položky",1,"Cena") = <cena první položky>
^Faktura(<c faktury>,"Položky",2,"Číslo") = <objednací číslo druhé položky>

 

Do datového uzlu se často ukládá pouze jeden datový prvek, např. datum nebo množství. Někdy je výhodné ukládat více datových prvků společně jako jeden samostatný datový uzel. To je vhodné u sady souvisejících údajů, které se často zpracovávají společně. Snížením počtu přístupů do databáze může dojít ke zlepšení výkonu, zvláště při zpracování v síti. Ve výše uvedené faktuře např. každá položka obsahuje objednací číslo zboží, množství a cenu uložené zvlášť v samostatných uzlech. Tyto údaje ale mohou být také uloženy jako seznam prvků v jednom uzlu:

 

^Faktura(<c faktury>,"PoložkyŘádku",<cislo polozky>).

 

Pro vysokou propustnost systémů s tisíci uživateli je rozhodující omezení počtu konfliktů soupeřících procesů. K největším konfliktům dochází mezi transakcemi, které požadují přístup ke stejným datům. Procesy Caché nezamykají během aktualizace dat celé stránky. Naproti tomu transakce vyžadují časté čtení nebo aktualizaci malých množství dat, a proto stačí zamykání dat na logické úrovni. Možné konflikty v databázi jsou dále snižovány pomocí atomizovaných operací sčítání a odčítání, které nevyžadují zamykání. (Tyto operace se používají především u čítačů, které přiřazují identifikační čísla, a pro modifikaci statistických čítačů.)

Než se podrobněji podíváme na některé části API rozhraní vrstvy eXTreme Globals, ukažme si, jak může vypadat velice jednoduchý program, který vytvoří v databázi globál s dvěma uzly a poté oba uzly načte do paměti. Během vytváření globálu a jeho načtením do paměti, se vždy vytvoří a poté ukončí spojení s databází, aby se prověřila persistence globálu v databázi. Zde je zdrojový kód aplikace:

 

package gds.sga;

import com.intersys.globals.*;

 

class SimpleGlobals {

public static void main(String[] args) {

Connection myConn = ConnectionContext.getConnection();

 

try {

// Connection 1 is used to create a new global array with two nodes

myConn.connect("User","_SYSTEM", "SYS");

NodeReference nodeRef1 = myConn.createNodeReference("myGlobal");

nodeRef1.set("Hello world"); // create root node ^myGlobal

nodeRef1.appendSubscript("sub1"); //point to node address ^myGlobal("sub1")

nodeRef1.set("This is a subscripted node"); // create subnode ^myGlobal("sub1")

nodeRef1.close();

myConn.close();

 

// Connection 2 is used to read both existing nodes

myConn.connect("User","_SYSTEM","SYS");

NodeReference nodeRef2 = myConn.createNodeReference("myGlobal");

System.out.println("Value of ^myGlobal is " + nodeRef2.getString());

nodeRef2.appendSubscript("sub1"); // point to subnode

System.out.println("Value of ^myGlobal(\"sub1\") is " + nodeRef2.getString());

nodeRef2.setSubscriptCount(0); // point to root node

// nodeRef2.kill(); // delete entire array

nodeRef2.close();

myConn.close();

}

catch (GlobalsException e) { System.out.println(e.getMessage()); }

} // end Main()

} // end class SimpleGlobals

 

Výsledek kompilace a spuštění výše uvedeného příkladu z příkazové řádky můžeme vidět na obrázku níže.

 

cache1_2

 

V portálu Caché se pak můžeme přesvědčit, že doopravdy došlo k vytvoření globálu „^myGlobal“:

 

cache1_3

 

Základním nástrojem pro vytváření, mazání a odkazování se na globály v databázi je instance třídy „NodeRefence“. Metody této třídy poskytují veškeré prostředky pro práci s globály. Podívejme se na některé z těchto metod.

Metoda „set()“ přiřadí nebo změní hodnotu referencovaného globálu. Pokud uzel nemá hodnotu nebo neexistuje, metoda vytvoří nový persistentní uzel a přiřadí mu požadovanou hodnotu. Tato hodnota může být typu int, long, double, byte[], String nebo ValueList.

Data a kód jsou v systému Caché ukládány v souborech s názvem CACHE.DAT (v jednom adresáři může být pouze jeden takový soubor). Každý takový soubor obsahuje množství globálů (vícerozměrných polí). Název globálu musí být v daném souboru jedinečný, ale může se opakovat v jiných souborech. Tyto soubory lze volně považovat za databáze. Místo udávání určitého databázového souboru používají procesy Caché při přístupu k datům tzv. názvový prostor. Je to logická mapa názvů, ve které jsou mapovány názvy vícerozměrných polí globálů a kódu do databáze. Pokud je databáze přesunuta z jedné diskové jednotky na jinou nebo na jiný počítač, je třeba upravit pouze mapu názvového prostoru. Vlastní aplikace se nezmění.

Objekt obsahující odkaz na globál operuje standardně v názvovém prostoru uvedeném při navázání spojení se systémem, je však možné současně operovat i ve více názvových prostorech. Jako příklad si uveďme úryvek kódu, který vytvoří současně dva globály ve dvou různých názvových prostorech, „User“ a „Samples“. Oba globály se mohou a budou jmenovat stejně, neboť názvové prostory jsou mapovány do dvou různých databází:

 

try {

// Create a node in each namespace

NodeReference nodeRef = myConn.createNodeReference("myGlobal");

 

myConn.setNamespace("User");

nodeRef.set("This node is in namespace " + myConn.getNamespace());

 

myConn.setNamespace("Samples");

nodeRef.set("This node is in namespace " + myConn.getNamespace());

 

// Access and print the value of each node

myConn.setNamespace("User");

System.out.println(nodeRef.getString());// Prints: "This node is in namespace User"

 

myConn.setNamespace("Samples");

System.out.println(nodeRef.getString());// Prints: "This node is in namespace Samples"

 

myConn.close();

nodeRef.close();

}

catch (GlobalsException e) { /* message */ }

 

 

Níže uvedený útržek kódu ilustruje, jakým způsobem se API rozhraní vrstvy eXTreme Globals vypořádává s vytvářením jednotlivých uzlů ve stromové struktuře globálu:

 

// nodeRef initially points to root node ^myGlobal, but changes three times

nodeRef.appendSubscript("x"); // nodeRef points to ^myGlobal("x")

nodeRef.appendSubscript("y"); // nodeRef points to ^myGlobal("x","y")

nodeRef.set(3.14); // create persistent ^myGlobal("x","y") = 3.14

nodeRef.setSubscriptCount(0); // nodeRef points to root node ^myGlobal again

 

V případě odstraňování uzlů globálu předpokládejme, že je v databázi uložen globál s následující strukturou:

 

^myGlobal(1)

^myGlobal("two","a")

^myGlobal("two","c","third")

^myGlobal(3.1)

^myGlobal(3.1,"x",25)

 

Předpokládejme dále, že objekt „nodeRef“ obsahuje referenci na kořenový uzel „^myGlobal“, pak metody „kill()“ a „killNode()“ jsou implementovány níže uvedeným způsobem:

 

nodeRef.killNode(3.1); // kill only ^myGlobal(3.1)

nodeRef.kill("two"); // kill ^myGlobal("two") and all of its subnodes

nodeRef.kill(); // kill all nodes in global array ^myGlobal

 

Abychom získali představu, jak lze procházet jednotlivé uzly globálu, je nutné vědět, jak jsou uzly globálu v databázi uspořádány. Neexistuje žádné pravidlo, které by vyžadovalo, v jakém pořadí je nutné jednotlivé uzly do globálu ukládat, neboť globál je reprezentován stromovou strukturou, hierarchie uzlů je automaticky generována, kdykoli dojde k přidání persistentního uzlu do globálu. Například globál se může skládat ze tří persistentních uzlů:

^myGlobal(„a“,“23“), ^myGlobal(„a“,“ x“), ^myGlobal(„b“). Tyto uzly je možné přidávat do globálu v libovolném pořadí a výsledná struktura globálu v databázi bude vždy takováto:

 

^myGlobal (korenovy uzel bez prirazene hodnoty)

^myGlobal("a") (uroven 1 uzel bez prirazene hodnoty)

^myGlobal("a","23") = <nejaka hodnota>

^myGlobal("a"," x") = <nejaka hodnota> (druhy index obsahuje a „ „ znak mezera!)

^myGlobal("b") = <nejaka hodnota>

 

Pořadí uzlů je dáno úrovní vnoření uzlu a použitým způsobem třídění znaků (collation order) v databázi.

API rozhraní nabízí tři základní metody třídy „NodeReference“, které je možné použít k procházení jednotlivými uzly globálu: „nextSubscript()“, „previousSubscript()“, „exists()“, „hasSubnodes()“.

Níže uvedený úryvek kódu demonstruje jeden z možných způsobů procházení uzly globálu „^myNames“, který obsahuje tato data:

 

^myNames (valueless root node)

^myNames("dogs") (valueless level 1 node)

^myNames("dogs","Balto") = 6

^myNames("dogs","Hachiko") = 8

^myNames("dogs","Lassie") = 9

^myNames("dogs","Lassie","Timmy") = 10

^myNames("dogs","Whitefang") = 7

^myNames("people") (valueless level 1 node)^myNames("people","Anna") = 2

^myNames("people","Julia") = 4

^myNames("people","Misha") = 5

^myNames("people","Ruri") = 3

^myNames("people","Vlad") = 1

 

// Search for first child node of ^myNames("people") in ascending collation order

nodeRef.appendSubscript("");

String subscr = nodeRef.nextSubscript();

if (subscr.equals("")) nodeRef.setSubscriptCount(nodeRef.setSubscriptCount-1);

 

// Access all level 2 nodes under ^myNames("people") in ascending order

System.out.print("Ascend from below first subscript: ");

while (!subscr.equals("")) {

nodeRef.setSubscript(nodeRef.getSubscriptCount(), subscr);

if (nodeRef.exists()) { // if this node has data

System.out.print("\"" + subscr + "\"=" + (nodeRef.getInt()) + " ");

}

subscr = nodeRef.nextSubscript();

};

 

Výše uvedené příklady se dotkly celého API modulu eXTreme Globals velice povrchně, jen abychom získali představu o tom, o co se opírají vrstvy eXTreme Event Persistence (XEP) a eXTreme Dynamic Object (XDO). Podrobnější informace je možné nalézt v dokumentaci Caché.

V další části článku se budeme věnovat modulu Caché eXTreme Event Persitence, který umožňuje pracovat s projekcí jednoduchých objektů Javy jako s persistentními událostmi.