Oracle Replication in der XE-Edition
Ich habe mir oft Gedanken über eine einfache Möglichkeit einer Replikation von Schemata in der Oracle-XE-Edition gemacht, zumal wir bei uns im Unternehmen eine kritische Anwendung basierend auf der XE-Edition haben, welche permanent verfügbar sein muss. Zusätzlich soll diese Anwendung nun auch in einem Tochterunternehmen im Ausland eingesetzt werden, und gleiche Datenbasis verwenden. Die Anwendung selber speichert unter anderem wichtige PDF-Dokumente der Flugzeugbranche deren Laden aus der Datenbank über die Anwendung eine ausreichende Bandbreite voraussetzt. Gerade im Bereich der Ausfallsicherheit und der dezentralen Vorhaltung des gleichen Datenbestandes bietet sich natürlich eine Replikation an. Leider habe ich im Netz keine für mich befriedigende Lösung, die preiswert ist und meinen Anforderungen gerecht wurde, gefunden.
Mein Ansatz
Aus diesem Grund entschied ich mich, eine eigene Lösung zu entwickeln, welche genau dieses ermöglicht. Hinzu kam natürlich nun die Frage, welche Technologie kann für eine derartige Lösung in Frage kommen und welches Konzept kann dieses bewerkstelligen? Die für mich naheliegende Lösung bestand in der Verwendung von Triggern. Dafür überlegte ich mir folgendes Konzept:
Auf die Applikationstabellen werden Trigger gelegt, die beim Hinzufügen, Ändern und Löschen den entsprechenden Datensatz in eine dafür vorgesehene Spiegeltabelle ablegen, um diese Aktion dann zu einem späteren Zeitpunkt auf die weiteren Schemata anwenden zu können. Zusätzlich erhält jede Applikationstabelle eine zusätzliche Spalte, in meinem Fall mit dem Namen GUID_REP, welche für jede Zeile eine GUID für die Wiedererkennung erzeugt, die die Eindeutigkeit der Datensätze bestimmt und über die die Replikation die korrespondierenden Sätze in den anderen Schemata des Replikationsverbundes findet.
Die Spiegeltabellen
Hierzu ein Beispiel für die Erweiterung der Anwendungstabellen:
CREATE TABLE "FJMAP"."TABAIRCRAFT"
( "ID" NUMBER(10,0),
"HOLDER" VARCHAR2(100 BYTE) NOT NULL ENABLE,
"REGISTRATION" VARCHAR2(30 BYTE) NOT NULL ENABLE,
"DESCRIPTION" VARCHAR2(150 BYTE),
"NOTES" VARCHAR2(4000 BYTE),
"DELETED" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE,
"CREATOR" VARCHAR2(100 BYTE),
"CREATEDATE" VARCHAR2(100 BYTE),
"MODIFIER" VARCHAR2(100 BYTE),
"MODIFYDATE" VARCHAR2(100 BYTE),
"NOTE" VARCHAR2(4000 BYTE),
"EXTMAIL" VARCHAR2(200 BYTE),
"INTMAIL" VARCHAR2(200 BYTE),
"SN" VARCHAR2(100 BYTE),
"TCH" VARCHAR2(30 BYTE),
"TYPE" VARCHAR2(30 BYTE),
"GUID_REP" VARCHAR2(100 BYTE) DEFAULT RAWTOHEX(SYS_GUID()) NOT NULL ENABLE
)
TABLESPACE "TBSLXMAP";
Die dazugehörige Spiegeltabelle hat den folgenden Aufbau:
CREATE TABLE "FJMAP"."TB#TABAIRCRAFT"
( "ID" NUMBER(10,0),
"HOLDER" VARCHAR2(100 BYTE),
"REGISTRATION" VARCHAR2(30 BYTE),
"DESCRIPTION" VARCHAR2(150 BYTE),
"NOTES" VARCHAR2(4000 BYTE),
"DELETED" NUMBER(1,0),
"CREATOR" VARCHAR2(100 BYTE),
"CREATEDATE" VARCHAR2(100 BYTE),
"MODIFIER" VARCHAR2(100 BYTE),
"MODIFYDATE" VARCHAR2(100 BYTE),
"NOTE" VARCHAR2(4000 BYTE),
"EXTMAIL" VARCHAR2(200 BYTE),
"INTMAIL" VARCHAR2(200 BYTE),
"SN" VARCHAR2(100 BYTE),
"TCH" VARCHAR2(30 BYTE),
"TYPE" VARCHAR2(30 BYTE),
"GUID_REP" VARCHAR2(100 BYTE),
"REPSEQID" NUMBER(10,0),
"REPACTION" VARCHAR2(2 BYTE),
"UPDCOLS" VARCHAR2(4000 BYTE)
)
TABLESPACE "TBSLXMAP" ;
CREATE INDEX "FJMAP"."IDXREP#TABAIRCRAFT"
ON "FJMAP"."TB#TABAIRCRAFT" ("GUID_REP")
TABLESPACE "TBSLXMAP" ;
Hierbei bestehen neben der Spalte GUID_REP für die Eindeutigkeit jeder Zeile noch die zusätzlichen Spalten REPSEQID, REPACTION und UPDCOLS. Die Spalte REPSEQID wird durch eine einzelne Sequenz gespeist, die für alle REPSEQID-Spalten der Spiegeltabellen zuständig ist. Die Spalte REPACTION beinhaltet die Aktion für die Zeile. Wird eine Zeile hinzugefügt, so erhält sie den Wert "I", beim Löschen wird der Wert "D" und beim Ändern der Wert "U" verwendet. Zum Schluss besitzt die Spiegeltabelle noch die Spalte UPDCOLS, die die Namen der geänderten Spalten in dem Format #|Spalte1|##|Spalte2|#....#|SpalteN|# besitzt, so dass zu sehen ist, welche Spalte für die Zeile eine Änderung erfahren hat.
Der Trigger
Das Einfügen der Datensatzänderungen übernimmt ein Compound-Trigger, der für die gezeigten Beispieltabellen folgendermaßen aussieht:
CREATE OR REPLACE TRIGGER TR#TABAIRCRAFT
FOR INSERT OR UPDATE OR DELETE
ON TABAIRCRAFT
COMPOUND TRIGGER
BEFORE EACH ROW IS
BEGIN
IF PCK_REPSTAT.V_REPSTAT=FALSE THEN
IF INSERTING THEN
:NEW.GUID_REP:=RAWTOHEX(SYS_GUID());
END IF;
END IF;
END BEFORE EACH ROW;
AFTER EACH ROW
IS
V_REPTIMESTAMP TIMESTAMP WITH LOCAL TIME ZONE:=CURRENT_TIMESTAMP;
V_REPID NUMBER :=SQREP#MASTERID.NEXTVAL;
V_UPDCOLS VARCHAR2(4000);
BEGIN
IF PCK_REPSTAT.V_REPSTAT=FALSE THEN
IF UPDATING('ID') THEN
V_UPDCOLS:=V_UPDCOLS || '#|ID|#';
END IF;
IF UPDATING('HOLDER') THEN
V_UPDCOLS:=V_UPDCOLS || '#|HOLDER|#';
END IF;
IF UPDATING('REGISTRATION') THEN
V_UPDCOLS:=V_UPDCOLS || '#|REGISTRATION|#';
END IF;
IF UPDATING('DESCRIPTION') THEN
V_UPDCOLS:=V_UPDCOLS || '#|DESCRIPTION|#';
END IF;
IF UPDATING('NOTES') THEN
V_UPDCOLS:=V_UPDCOLS || '#|NOTES|#';
END IF;
IF UPDATING('DELETED') THEN
V_UPDCOLS:=V_UPDCOLS || '#|DELETED|#';
END IF;
IF UPDATING('CREATOR') THEN
V_UPDCOLS:=V_UPDCOLS || '#|CREATOR|#';
END IF;
IF UPDATING('CREATEDATE') THEN
V_UPDCOLS:=V_UPDCOLS || '#|CREATEDATE|#';
END IF;
IF UPDATING('MODIFIER') THEN
V_UPDCOLS:=V_UPDCOLS || '#|MODIFIER|#';
END IF;
IF UPDATING('MODIFYDATE') THEN
V_UPDCOLS:=V_UPDCOLS || '#|MODIFYDATE|#';
END IF;
IF UPDATING('NOTE') THEN
V_UPDCOLS:=V_UPDCOLS || '#|NOTE|#';
END IF;
IF UPDATING('EXTMAIL') THEN
V_UPDCOLS:=V_UPDCOLS || '#|EXTMAIL|#';
END IF;
IF UPDATING('INTMAIL') THEN
V_UPDCOLS:=V_UPDCOLS || '#|INTMAIL|#';
END IF;
IF UPDATING('SN') THEN
V_UPDCOLS:=V_UPDCOLS || '#|SN|#';
END IF;
IF UPDATING('TCH') THEN
V_UPDCOLS:=V_UPDCOLS || '#|TCH|#';
END IF;
IF UPDATING('TYPE') THEN
V_UPDCOLS:=V_UPDCOLS || '#|TYPE|#';
END IF;
IF INSERTING THEN
INSERT
INTO TB#TABAIRCRAFT VALUES
(
:NEW.ID,
:NEW.HOLDER,
:NEW.REGISTRATION,
:NEW.DESCRIPTION,
:NEW.NOTES,
:NEW.DELETED,
:NEW.CREATOR,
:NEW.CREATEDATE,
:NEW.MODIFIER,
:NEW.MODIFYDATE,
:NEW.NOTE,
:NEW.EXTMAIL,
:NEW.INTMAIL,
:NEW.SN,
:NEW.TCH,
:NEW.TYPE,
:NEW.GUID_REP,
V_REPID,
'I',
NULL
);
INSERT
INTO TB#CENTRALMASTER VALUES
(
V_REPID,
'TB#TABAIRCRAFT',
'TABAIRCRAFT',
DBMS_TRANSACTION.LOCAL_TRANSACTION_ID,
:NEW.GUID_REP,
V_REPTIMESTAMP,
'I'
);
ELSIF DELETING THEN
INSERT
INTO TB#TABAIRCRAFT
(
GUID_REP,
REPSEQID,
REPACTION
)
VALUES
(
:OLD.GUID_REP,
V_REPID,
'D'
);
INSERT
INTO TB#CENTRALMASTER VALUES
(
V_REPID,
'TB#TABAIRCRAFT',
'TABAIRCRAFT',
DBMS_TRANSACTION.LOCAL_TRANSACTION_ID,
:OLD.GUID_REP,
V_REPTIMESTAMP,
'D'
);
ELSE
INSERT
INTO TB#TABAIRCRAFT VALUES
(
:NEW.ID,
:NEW.HOLDER,
:NEW.REGISTRATION,
:NEW.DESCRIPTION,
:NEW.NOTES,
:NEW.DELETED,
:NEW.CREATOR,
:NEW.CREATEDATE,
:NEW.MODIFIER,
:NEW.MODIFYDATE,
:NEW.NOTE,
:NEW.EXTMAIL,
:NEW.INTMAIL,
:NEW.SN,
:NEW.TCH,
:NEW.TYPE,
:NEW.GUID_REP,
V_REPID,
'U',
V_UPDCOLS
);
INSERT
INTO TB#CENTRALMASTER VALUES
(
V_REPID,
'TB#TABAIRCRAFT',
'TABAIRCRAFT',
DBMS_TRANSACTION.LOCAL_TRANSACTION_ID,
:NEW.GUID_REP,
V_REPTIMESTAMP,
'U'
);
END IF;
END IF;
END AFTER EACH ROW;
END;
Wichtige Bestandteile des Triggers habe ich in diesem Beispiel farblich markiert, auf die ich im Folgenden eingehen möchte. Der Trigger fragt bei jeder Ausführung die globale boolesche Variable V_REPSTAT des Package PCK_REPSTAT ab, welche für eine Ausführung FALSE sein muss. Warum das? Daten sollen ja zwischen Schemata repliziert werden, welches durch einen Replikationsjob mit der PL/SQL-Prozedur PRC_REPLICATION erfolgt. Der Trigger muss nun wissen: handelt es sich bei der Änderung im Schema um Änderungen direkt aus der Anwendung oder ändert der Replikationsjob die Daten. Wird die Änderung durch den Replikationsjob getätigt, darf der Trigger natürlich nicht erneut loslaufen, damit ein Ping-Pong-Effekt vermieden wird. Aus diesem Grund setzt der Replikationsjob diese Variable auf den Wert TRUE und signalisiert somit, dass der Trigger nicht für diese Änderungen loslaufen darf.
Wurden aber Änderungen (INSERT, UPDATE, DELETE) durch die Anwendung getätigt, so wird im ersten Schritt überprüft, ob ein neuer Datensatz hinzugefügt (INSERT) wurde. Ist dieses der Fall, wird für diesen Datensatz eine neue GUID generiert und der neue Datensatz mit dieser GUID in die Spiegeltabelle eingetragen. Bei einer Änderung (UPDATE) eines Datensatzes wird dieses ebenfalls vollzogen, zusätzlich wird im Trigger überprüft, welche Spalten geändert wurden und deren Name in die Spalte UPDCOLS der Spiegeltabelle in dem Format #|Spalte1|##|Spalte2|#....#|SpalteN|# gespeichert. Beim Löschen werden nur grundlegende Informationen, wie der Wert GUID_REP abgelegt, da diese für das Löschen dieses Datensatzes aus den anderen Schemata reicht.
Ein ganz wichtiger Aspekt ist bei meinem Ansatz die Verwendung einer zusätzlichen zentralen Tabelle eines jeden Schemas, welche allgemeine Informationen für die Datensatzänderungen beinhaltet. Wichtig ist bei der Replikation die Einhaltung der Reihenfolge der durchgeführten Modifikationen und der Gewährleistung der transaktionalen Konsistenz. Dies habe ich über diese zentrale Tabelle mit dem Namen TB#CENTRALMASTER versucht zu erreichen.
CREATE TABLE "FJMAP"."TB#CENTRALMASTER"
( "ID" NUMBER,
"TABLE_NAME_REP" VARCHAR2(100 BYTE),
"TABLE_NAME" VARCHAR2(100 BYTE),
"TRANSID" VARCHAR2(100 BYTE),
"GUID_REP" VARCHAR2(100 BYTE),
"REPTIMESTAMP" TIMESTAMP (6) WITH LOCAL TIME ZONE,
"REPACTION" VARCHAR2(2 BYTE)
) TABLESPACE "TBSLXMAP" ;
Neben den Einträgen für das Hinzufügen, Ändern und Löschen in den Spiegeltabellen durch den Trigger, fügt dieser zusätzlich für jeden Eintrag in die Spiegeltabelle einen Eintrag in die Zentraltabelle ein. Die Einträge zwischen der Zentraltabelle und der Spiegeltabelle sind über die Spalte ID der Zentraltabelle und der Spalte REPSEQID verknüpft. Diese Zentraltabelle beinhaltet unter anderem den Namen der Anwendungstabelle, den Namen der Spiegeltabelle, einen Zeitstempel der Änderung sowie eine Transaktions-ID. Für die transaktionale Konsistenz trägt der Trigger für jede Änderung die dazugehörige Transaktions-ID ein, um die Zusammengehörigkeit der Änderungen zu ermitteln. Diese Transaktions-ID wird über die Package-Funktion DBMS_TRANSACTION.LOCAL_TRANSACTION_ID ermittelt und ist innerhalb der Transaktion gleich. Nach dieser ID werden die zu replizierenden Änderungen später in Verbindung mit der Spalte REPSEQID und einem Zeitstempel geordnet und angewendet.
Die Replikation
Wie funktioniert nun die Replikation? Die Replikation wird durch eine PL/SQL-Prozedur gestartet, die in einer von mir ausgewählten Datenbank läuft. Hierfür verwendet die Prozedur Database-Links, die auf die anderen Schemata zeigen und die Namenskonvention LNK_REP0001…LNK_REPNNNN besitzen. Über die Database-Links sowie auf der lokalen Datenbank werden die durch die Trigger protokollierten Einträge über ein dynamisch erzeugtes SQL-Statement zusammengefasst und nach der Transaktion und zeitlicher Reihenfolge geordnet. Zusätzliche erhält jede Änderung eine Standort ID, so dass die Prozedur erkennt, auf welches Datenbankschema die Änderung anzuwenden ist. Bei drei Schemata im Replikationsverbund sähe das SQL-Statement für die Zusammenführung folgendermaßen aus:
SELECT A.ID,
A.TABLE_NAME_REP,
A.TABLE_NAME,
A.TRANSID,
A.GUID_REP,
A.REPACTION,
A.DEPT,
MIN(A.REPTIMESTAMP) OVER (PARTITION BY A.TRANSID) AS MINTIME
FROM
(
SELECT D0.*, 0 AS DEPT FROM TB#CENTRALMASTER D0
UNION ALL
SELECT D1.*, 1 AS DEPT FROM TB#CENTRALMASTER@LNKREP_00001 D1
UNION ALL
SELECT D2.*, 2 AS DEPT FROM TB#CENTRALMASTER@LNKREP_00002 D2
) A
ORDER BY MIN(A.REPTIMESTAMP) OVER (PARTITION BY A.TRANSID), DEPT, TRANSID, ID;
Nach der Identifikation der zu replizierenden Daten, werden diese über eine Schleife weiterverarbeitet, in dem anhand der Standortnummer jedes Satzes ermittelt wird, wo dieser hin zu replizieren ist. Um dieses zu bewerkstelligen, wird für jeden zu replizierenden Datensatz eine weitere Schleife über die Standortnummern durchgeführt. Dadurch wird der Schleifenzähler des Standortes mit der Standortnummer des zu replizierenden Satzes verglichen. Sind diese Werte unterschiedlich, so muss der Satz zum Standort des Schleifenzählers repliziert werden. Hierfür werden dann der Ziel- und der Quell-Database-Link in einer Textvariable abgelegt, sofern es sich nicht um die Hauptdatenbank, in der die Replikationsprozedur läuft, als Quelle oder Ziel handelt.
FOR I IN 0..P_MAXREPMEMBER
LOOP
IF I <> REC_REP_VALUES.DEPT
THEN
IF I>0
THEN
V_TARGETLINK:='@LNKREP_' || TRIM(TO_CHAR(I,'0000'));
ELSE
V_TARGETLINK:='';
END IF;
IF REC_REP_VALUES.DEPT >0
THEN
V_SOURCELINK:='@LNKREP_' ||
TRIM(TO_CHAR(REC_REP_VALUES.DEPT,'0000'));
ELSE
V_SOURCELINK:='';
END IF;
……
……
Des Weiteren wird über die Spalte REPACTION bestimmt, welche Aktion mit diesem Datensatz durchzuführen ist. Über eine CASE-Anweisung wird die auszuführende Aktion separiert und auf Basis dieser dynamisch eine geeignete Anweisung generiert. Für das Einfügen eines Datensatzes wird eine einfache INSERT INTO..SELECT-Anweisung verwendet, die den Datensatz von der Quelle in das Ziel kopiert.
CASE REC_REP_VALUES.REPACTION
WHEN 'I' THEN
……
……
V_SQLREP:='INSERT INTO ' || REC_REP_VALUES.TABLE_NAME || V_TARGETLINK ||
' SELECT * FROM ' || REC_REP_VALUES.TABLE_NAME || V_SOURCELINK ||
' WHERE GUID_REP=''' || REC_REP_VALUES.GUID_REP || '''';
……
……
EXECUTE IMMEDIATE V_SQLREP;
Das Löschen eines Datensatzes erfolgt nach dem gleichen Prinzip, in dem der Datensatz vom Ziel entfernt wird.
WHEN 'D' THEN
V_SQLREP:='DELETE FROM ' || REC_REP_VALUES.TABLE_NAME || V_TARGETLINK ||
' WHERE GUID_REP=''' || REC_REP_VALUES.GUID_REP || '''';
Das Ändern eines Datensatzes an den Zielen ist in meinem Ansatz etwas aufwändiger gestaltet und bedarf sicherlich noch ein wenig Optimierung – speziell was die Belastung des Systems betrifft. Mein Ansatz ist: Ich hole mir im ersten Schritt aus der Spalte UPDCOLS der Spiegeltabelle die Spaltenliste der geänderten Spalten des Datensatzes.
WHEN 'U' THEN
……
……
V_SQLREP:='SELECT UPDCOLS FROM ' || REC_REP_VALUES.TABLE_NAME_REP || V_SOURCELINK ||
' WHERE GUID_REP=:GID_REP AND REPSEQID=:REPSEQID';
……
……
EXECUTE IMMEDIATE V_SQLREP INTO V_UPDCOLS
USING REC_REP_VALUES.GUID_REP,REC_REP_VALUES.ID;
Im zweiten Schritt trenne ich die Spaltennamen über das Package DBMS_UTILITY mit der Prozedur COMMA_TO_TABLE, so dass ich diese über ein Array auslesen kann, womit ich dann dynamisch im letzten Schritt eine UPDATE-Anweisung generiere und auf Basis des Quelldatensatzes den Zieldatensatz aktualisiere. Jede Spalte wird so einzeln mit dem Zielwert aktualisiert.
V_UPDCOLS:=REPLACE(V_UPDCOLS,'|##|',',');
V_UPDCOLS:=REPLACE(V_UPDCOLS,'#|','');
V_UPDCOLS:=REPLACE(V_UPDCOLS,'|#','');
DBMS_UTILITY.COMMA_TO_TABLE(V_UPDCOLS, V_TABLEN, V_TAB);
……
……
FOR IC IN 1..V_TABLEN
LOOP
V_COLUMNNAME:=V_TAB(IC);
V_SQLREP := 'UPDATE ' || REC_REP_VALUES.TABLE_NAME || V_TARGETLINK ||
' SET ' || V_COLUMNNAME ||
'=(SELECT ' || V_COLUMNNAME ||
' FROM ' || REC_REP_VALUES.TABLE_NAME_REP || V_SOURCELINK ||
' WHERE GUID_REP=:GUID_REP1 AND REPSEQID=:REPSEQID)
WHERE GUID_REP=:GUID_REP2';
……
……
EXECUTE IMMEDIATE V_SQLREP
USING REC_REP_VALUES.GUID_REP, REC_REP_VALUES.ID, REC_REP_VALUES.GUID_REP;
END LOOP;
Nachdem der Replikationsvorgang für den Quelldatensatz abgeschlossen wurde, werden die Metadaten dieses Satzes aus der Zentral- und Spiegeltabelle entfernt.
V_SQLREP:='DELETE FROM TB#CENTRALMASTER' || V_SOURCELINK ||
' WHERE ID=:ID';
EXECUTE IMMEDIATE V_SQLREP USING REC_REP_VALUES.ID;
V_SQLREP:='DELETE FROM ' || REC_REP_VALUES.TABLE_NAME_REP || V_SOURCELINK ||
' WHERE REPSEQID=:ID';
EXECUTE IMMEDIATE V_SQLREP USING REC_REP_VALUES.ID;
Ein ganz wesentlicher Aspekt wurde bis zu diesem Zeitpunkt außer Acht gelassen: Wie werden Anweisungen einer Transaktion behandelt? Bei jedem neuen Schleifendurchgang bei der Verarbeitung der zur replizierenden Datensätze der Zentraltabelle wird die Transaktions-ID gelesen und mit der vorherigen verglichen. Hat diese sich geändert, wird sie in die Variable V_TRANSID geschrieben.
WHILE C_REP_VALUES%FOUND
LOOP
IF NVL(V_TRANSID,'-XX')!=REC_REP_VALUES.TRANSID
THEN
V_TRANSID:=REC_REP_VALUES.TRANSID;
END IF;
Bei jedem Ende eines Schleifenvorgangs wird der Inhalt der Variable V_TRANSID mit der nächsten Transaktions-ID verglichen. Hat diese sich geändert, wird die Transaktion mit COMMIT abgeschlossen.
FETCH C_REP_VALUES INTO REC_REP_VALUES;
IF C_REP_VALUES%NOTFOUND OR NVL(V_TRANSID,'-XX')!=REC_REP_VALUES.TRANSID
THEN
COMMIT;
END IF;
END LOOP;
Ebenso ist wichtig zu erwähnen, dass die Ausführung der Trigger für die zu replizierenden Datensätze verhindert werden muss. Hierfür wird bei Eingang in die Prozedur die Package-Variable PCK_REPSTAT.PRC_SETREPSTAT(TRUE); auf TRUE und bei Ausgang auf FALSE gesetzt.
Des Weiteren werden alle Replikationsfehler in die Tabelle TB#ERROR geschrieben. So lange aus einem bestimmten Grund ein Fehler bei der Replikation auftritt, werden keine weiteren Änderungen mehr weitergegeben, bzw. repliziert. Zur dauerhaften Protokollierung und zur Fehleranalyse kann der Replikationsprozedur PRC_REPLICATION der Parameter P_WRITELOG mit dem Wert 1 übergeben werden, welcher die zur Replikation verwendeten SQL-Anweisungen sowie die Metadaten der zu replizierenden Datensätze mitschreibt. Die Protokollierung übernimmt die lokale Prozedur PRC_WRITE_LOG der Replikationsprozedur PRC_REPLICATION und wird im Fehlerfall oder bei P_WRITELOG = 1 regelmäßig innerhalb der Prozedur aufgerufen. Hat die Spalte ERRCODE der Tabelle TB#ERROR für einen Protokolleintrag den Wert <> 0, so liegt ein Fehler vor, sonst handelt es sich um einen normalen Informationseintrag.
PROCEDURE PRC_WRITE_LOG(P_TABLE_NAME VARCHAR2,
P_TABLE_NAME_REP VARCHAR2,
P_SQLREP VARCHAR2,
P_TRANSID VARCHAR2 DEFAULT NULL,
P_GUID_REP VARCHAR2 DEFAULT NULL,
P_ERRM VARCHAR2 DEFAULT NULL,
P_ERRN NUMBER DEFAULT 0)
IS
BEGIN
INSERT INTO TB#ERROR
VALUES(
SQREP#ERRID.NEXTVAL,
SYSDATE,
P_TABLE_NAME,
P_TABLE_NAME_REP,
P_SQLREP,
P_TRANSID,
P_GUID_REP,
P_ERRM,
P_ERRN
);
END;
Implementierung
Nun kommt natürlich die Gretchenfrage. Wie kann das ganze implementiert werden? Eine Entwicklung für eine Vielzahl an Tabellen macht das ganze sehr zeitaufwändig und bindet zu viele Entwicklungsressourcen. Aus diesem Grund habe ich mir eine VB-Anwendung geschrieben, die die Implementierung übernimmt. Diese VB-Anwendung erlaubt es mir, für die Installation die zu replizierenden Tabellen auszuwählen, für welche dann die Replikationsobjekte erstellt werden sollen. Des Weiteren fügt diese Anwendung bei der Implementierung allen Tabellen die Spalte GUID_REP hinzu und befüllt diese für jeden Datensatz mit einem eigenen GUID-Wert.
Hierfür müssen die Verbindungsinformationen im Vorfeld der Implementierung zu den Oracle-Datenbanken angegeben werden, auf die das Schema kopiert und repliziert werden soll. Aus diesen Anmeldeinformationen werden während der Implementierung ebenfalls die Database-Links erstellt. Die Masterdatenbank ist die Datenbank die das Ausgangsschema beinhaltet und auf der später der Replikationsjob über die Prozedur PR_REPLICATION läuft. Ebenfalls müssen auf den Zieldatenbanken die Benutzer erstellt worden sein, die das zu replizierende Schema erhalten.
Nachdem die Objekte erstellt und die Schemaänderung erfolgt ist, wird das Schema automatisch per Database-Link-Datapumpimport auf die weiteren Server übertragen. Bevor die Implementierung startet wird noch eine Voraussetzungsüberprüfung durchgeführt, welche grob abklopft, ob eine Implementierung erfolgreich sein wird. Meine Empfehlung ist aber, den Benutzern, die das zu replizierende Schema erhalten, für die Installation DBA-Rechte zu geben. Nach der Installation können diese wieder entfernt werden.
Wichtige Überlegungen
In meinem Ansatz muss auf ein paar wichtige Probleme hingewiesen werden, die unter Umständen auftreten können. Zum einen muss auf Verwendung der Sequenzen geachtet werden. Wird die Replikation für eine Zweiwegreplikation verwendet, das heißt, es werden Änderungen an den Datenbeständen eines jeden zu replizierenden Schemas gemacht, die dann zusammenfließen, so kann es bei der Vergabe von Sequenzwerten zu Konflikten führen. Hier muss darauf geachtet werden, dass jedes Schema in dem Replikationsverbund seinen eigenen Nummernkreis besitzt. Ich habe dieses gelöst, indem ich den Sequenzwerten eine Niederlassungsnummer voranstelle. So wird der Wert 1 der Sequenz in der Niederlassung 1 der Wert 11 und der Wert 1 der Sequenz in der Niederlassung 2 21. Ein weiterer wichtiger Hinweis verbirgt sich in der hinzugefügten Spalte GUID_REP, die die Eindeutigkeit der Datensätze bestimmt und über die die Replikation die korrespondierenden Sätze in den anderen Schemata des Replikationsverbundes findet. Sollte beim Einfügen von Datensätzen über Ihre Anwendung die Spaltenliste des INSERT-Befehls fehlen, so schlägt dieser Befehl fehl. Dieses Problem werde ich wohl erst in der XE 12c lösen können, in der versteckte Spalten erstellt werden können.
Fazit
Eine Multi-Master-Replikation ohne die Enterprise-Edition einzurichten ist nicht wirklich trivial und erfordert eine Menge Hirnakrobatik, zumindest empfand ich die Lösungsfindung nicht ganz einfach. Mir ist leider auch nicht wirklich klar, warum die Oracle XE-Edition nicht wenigstens Oracle Streams besitzt oder irgendeinen Mechanismus für eine Replikation. Unabhängig davon besitze ich jetzt eine brauchbare Lösung - auch wenn diese leider nicht für jeden verwendbar ist, da sie ja doch ein paar grundlegende Einschränkungen besitzt. Ich warte jetzt mal auf die Oracle XE 12c und hoffe, dass man dort das Feature für das Verstecken von Spalten nutzen kann. Wenn das der Fall ist, dann ist eine große Hürde genommen. Alle, für die dieses Tool zur Implementierung interessant ist, dürfen sich gerne bei mir melden und ich sende es ihnen zu.
Publikationen
- Der Oracle DBA: Handbuch für die Administration der Oracle Database 12c: Von Mirko Hotzy, Andrea Held , et al.
- Das große Oracle Datenbank-Einsteigerbuch.: Marek Adar
- Ein strukturierter Einstieg in die Oracle-Datenbankadministration: Marek Adar
- Ein strukturierter Einstieg in Oracle Real Application Cluster 11g R2: Marek Adar
- Ein strukturierter Einstieg in die Oracle SQL und PL/SQL-Entwicklung: Marek Adar
- Recovery Manager Kompakt: Eine strukturierte Einführung in den Recovery Manager: Marek Adar