Eine Spring Data Anwendung mit einem Apache Cassandra Backend

Eine Spring Data Anwendung mit einem Apache Cassandra Backend ist recht einfach zu erstellen, da Spring Data hierfür ein eigenes Subprojekt etabliert hat. Weiterhin gibt es selbstverständlich einen Embedded Cassandra Server als Bestandteil von Spring Boot. Ein externer Server ist kann über einen Docker-Container realisiert werden, das offizielle Docker-Image liegt auf dem Docker Hub.  

Cassandra im Docker-Container

Für eine einfache Test-Anwendung wird eine einzelne Cassandra-Instanz über den Aufruf 

docker run --rm --name cassandra -p 9142:9142 cassandra

gestartet. Damit läuft ein einzelner Cassandra-Knoten.

Nach dem Starten des Containers kann eine Konsole im Container geöffnet werden, mit deren Hilfe die Cassandra Shell cqlsh geöffnet werden kann:

docker exec --it cassandra /bin/bash

cqlsh

Damit kann dann für das folgende Beispiel nötige KeySpace sowie die Tabelle angelegt werden:

CREATE KEYSPACE PUBLISHING_KEYSPACE
WITH REPLICATION = {
'class': 'SimpleStrategy',
'replication_factor': 1
};

CREATE TABLE PUBLISHING(
publisher_name text,
title text,
isbn text,
price double,
pages int,
available boolean,
primary key(publisher_name, title, pages)
);

 

Die Spring Boot Anwendung

Wie üblich ist der Einstiegspunkt in die Spring Boot-Anwendung die pom.xml. Diese definiert neben dem Spring Boot Parent den Starter für Cassandra:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.javacream.training</groupId>
<artifactId>org.javacream.training.apache.cassandra.springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>org.javacream.training.apache.cassandra.springboot</name>
<description>Cassandra project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Damit ist die Konfiguration der Anbindung an den im Docker-Container laufenden Datenbankserver einfach:

@EnableCassandraRepositories
@Configuration
public class PublishingConfiguration

}

und die application.properties:

spring.data.cassandra.port=9142
spring.data.cassandra.keyspace-name=PUBLISHING_KEYSPACE

Entity und Repository

Die in der Cassandra-Datenbank abzulegende Datenstruktur wird über Annotationen gemapped:

@Table
public class Book {

@PrimaryKeyColumn(name = "publisher_name", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
private String publisherName;
@PrimaryKeyColumn(name = "pages", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
private int pages;
private double price;
private String isbn;
private boolean available;
@PrimaryKeyColumn(name = "title", ordinal = 1, type = PrimaryKeyType.CLUSTERED)
private String title;
//getter, setter, hashCode, equals, toString

}

Das Repository selbst ist dann eine einfache Schnittstellen-Erweiterung des CassandraRepository:

@Repository
public interface PublishingRepository extends CassandraRepository<Book, String>{

}

Die Anwendung

Die Anwendung selber schreibt nun nur 4 Bücher in die Datenbank:

@Component
public class PublishingApplication {

@Autowired private PublishingRepository publishingRepository;

@PostConstruct public void startApplication() {
Book cassandraInAction1 = new Book("Manning", 100, 19.99, "ISBN-1", false, "Cassandra in Action");
publishingRepository.save(cassandraInAction1);
Book cassandraInAction2 = new Book("Manning", 200, 29.99, "ISBN-2", true, "Cassandra in Action");
publishingRepository.save(cassandraInAction2);
Book cassandraInAction3 = new Book("Manning", 500, 39.99, "ISBN-3", true, "Cassandra in Action");
publishingRepository.save(cassandraInAction3);
Book java = new Book("Manning", 5000, 9.99, "ISBN-4", true, "Java");
publishingRepository.save(java);
}
}

Die geschriebenen Daten können nun einfach über die CQL-Shell abgefragt werden:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' ;

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 100 | False | ISBN-1 | 19.99
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99
Manning | Cassandra in Action | 500 | True | ISBN-3 | 39.99
Manning | Java | 5000 | True | ISBN-4 | 9.99

(4 rows)

Oder mit einer zusätzlichen Selektion auf den Titel:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' and title = 'Cassandra in Action';

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 100 | False | ISBN-1 | 19.99
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99
Manning | Cassandra in Action | 500 | True | ISBN-3 | 39.99

(3 rows)

Und schließlich noch auf die Seiten:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' and title = 'Cassandra in Action' and pages = 200;

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99

(1 rows)

Die folgende Abfrage führt nun jedoch zu keinem sinnvollen Ergebnis:

cqlsh:publishing_keyspace> select * from publishing where pages = 200;
InvalidRequest: Error from server: code=2200 [Invalid query] message="PRIMARY KEY column "pages" cannot be restricted as preceding column "title" is not restricted"

 


Seminare zum Thema

Weiterlesen

Build-Tools für CI/CD

Die Build-Tools für CI/CD, also für die Umsetzung einer  Continuous Integration/Delivery/Deployment-Strategie haben sich in den letzten Jahren als gesetzte Produkte etabliert. Heutzutage haben Software-Projekte keinen konzeptuellen Aufwand mehr, eine komplette Build-Chain aufzusetzen. Es geht eigentlich “nur” noch um die gewünschte Ausprägung und eine Evaluierung der geeigneten Produkte. 

Die Werkzeuge

In einer klassisch aufgesetzten Build-Chain werden für die unterschiedlichen Aufgaben unterschiedliche Systeme eingesetzt:

  1. Das Source Code Management System (SCM) enthält alle Sourcen des Software-Projekts. Also natürlich die eigentlichen Quellcodes, Konfigurationsdateien und weitere Ressourcen. Daneben enthält es aber auch noch die Definition des Build-Prozesses.
  2. Ein Build-Tool erzeugt aus den Quellcodes die gebaute Anwendung bzw. “Artefakte”. Dabei werden Abhängigkeiten, auch transitiv, zu anderen Artefakten aufgelöst.
  3. Die Entwickler-Rechner haben Zugriff auf das SCM und legen darin ihre Quellen ab. Zusätzlich ist das Build-Tool installiert, so dass ein Build-Prozess lokal ausgeführt werden kann.
  4. Eine Build-Maschine holt sich vom SCM die Quellcodes sowie den Build-Prozess und baut daraus die Anwendung. 
  5. Das Artefakt-Repository hält alle Artefakte in einer hierarchischen Ablage inklusive Versionsnummer vor. Artefakte werden vom Build-Tool stets vom Artefakt-Repository geladen. Aber auch alle nachrangigen Prozesse, also beispielsweise Anwendertests oder die Produktionsplattform bedienen sich des Artefakt-Repositories. 

Als konkrete Produkte seien hier erwähnt:

  1. GIT in Kombination mit einem Git-Server wir GitHub. GitLab oder BitBucket als SCM, 
  2. Als Build-Werkzeug z.B. im Java-Umfeld Apache Maven oder Gradle. Für Web-Anwendungen ist npm sicherlich immer eine sinnvolle Möglichkeit.
  3. Als Build-Maschine ist Jenkins in vielen Unternehmen gesetzt. 
  4. Nexus und Artifactory sind gebräuchliche Repository Manager.

Selbstverständlich stellen diese Produkte nur eine subjektive Auswahl dar.

Gesamtbild

Das folgende Gesamtbild veranschaulicht das Zusammenspiel der Komponenten:

Die Build-Tools im Zusammenspiel

 


Seminare zum Thema

Weiterlesen

Apache Cassandra

Teil 1: Das Datenbanksystem

Apache Cassandra ist ein horizontal skalierendes, Master-loses Datenbanksystem, das durch einen Ring-Cluster realisiert wird. Doch was genau bedeutet dies?

Der Cluster

  • Wie bereits in einem vorherigen Artikel einer allgemeinen Einführung zum Thema NoSQL beschrieben, bedeutet horizontale Skalierung, dass beim Auftreten eines Engpasses bezüglich CPU, Speicher oder Storage einfach ein weiterer Knoten in einen Cluster hinzugefügt werden kann. Dieser kann nach kürzester Zeit Last übernehmen und somit den Engpass beheben.
    Ein horizontaler Cluster

     

  • In einem Master-losen Ring-Cluster sind alle Knoten des Systems gleichberechtigt: Jeder Knoten kann Anfragen eines Clients entgegen nehmen und entweder direkt selber verarbeiten oder an einen anderen Knoten delegieren. Solange von einem Client aus auch nur ein Knoten im Netz sichtbar ist, wird der Cluster funktionieren und antworten.

 

 

Der Ring-Cluster

 

Ob diese Antwort konsistent ist, hängt von der internen Kommunikation der Knoten im Ring ab. Dies ist die Aussage des bekannten CAP-Theorems.  

Replikation

Im Ring-Cluster von Cassandra wird ein Datensatz auf jeweils einem Knoten gehalten. Um bei einem Ausfall eines Cluster-Knotens Informations-Verlust zu verhindern, werden Datensätze auf mehrere Knoten repliziert. Dies wird durch den Replikationsfaktor RF beschrieben. So werden im folgenden Bild die Daten mit RF=3 abgelegt:

 

 

BASE

Schreibt ein Client Daten in den Ring-Cluster und liest beispielsweise ein anderer Client kurz danach die Daten, so können Inkonsistenzen auftreten. Es dauert eine gewisse Zeit, bis alle beteiligten Knoten des Rings alle Datenänderungen mitbekommen haben. Ist so ein Zeitfenster der Inkonsistenz fachlich möglich, so spricht man von einer BASE-Architektur, in der das E für die “Eventual Consistency” steht. “Eventual” ist hier selbstverständlich mit “letztendlich” zu übersetzen; letztendlich wird ein Ring-Cluster einen konsistenten Zustand erreichen.  

Tunable Consistency

Wird fachlich Konsistenz gefordert, so kann dies in einem Ring-Cluster dadurch erreicht werden, dass die Antwort des Clients so lange verzögert wird, bis die Daten auf allen replizierten Knoten angekommen sind. 

Eine nahe liegende Lösung wäre also, bei einer Schreib-Operation W zu verlangen, dass RF Knoten die Daten geschrieben haben. 

Dann kann ein Lesevorgang von einem beliebigen Knoten ausgeführt werden.

Also insgesamt:

W = RF und R = 1

Aber auch “anders herum” wird Konsistenz erreicht:

W = 1 und R = RF

Auf diese Art und Weise kann ausbalanciert werden, ob Wert auf schnelle Schreib- oder auf schnelle Lese-Vorgänge gelegt werden soll. Dies wird als  “Tunable Consistency” bezeichnet.

Beide Verfahren haben jedoch den Nachteil, dass bei einem Partitionsfehler, also einem Ausfall der Kommunikation zwischen Knoten, der gesamte Ring-Cluster in einem Standby-Modus geschaltet werden muss und keine Antwort liefern kann. Dies kann vermieden werden, in dem für Schreib und Lesevorgänge das Quorum benutzt wird:

W = R = (RF + 1)/2

Auch hier ist Konsistenz garantiert, allerdings muss nur noch in die Majorität der Knoten geschrieben werden.


Seminare zum Thema

Weiterlesen

Datenmodellierung für Apache Cassandra

Cassandra ist eine Datenbank, die nicht eindeutig in die 5 Kategorien der NoSQL-Datenbanksysteme eingeordnet werden kann: Einesteils ist sie Spalten-orientiert, andererseits wird für den effizienten Zugriff auf Daten ein “Partition Key” benötigt, also ein eindeutiger Primärschlüssel. Dies entspricht damit eher einem Key-Value-Store. Die Datenmodellierung für Apache Cassandra muss diese Besonderheiten berücksichtigen, um eine effiziente Datenverarbeitung ermöglichen zu können.

CQL – Die Cassandra Query Language

Um die Datenmodellierung mit Cassandra zu erlernen scheint es in erster Näherung sinnvoll, sich mit der Abfragesprache CQL zu beschäftigen. Diese Sprache wird wie SQL sowohl zur Struktur-Definition als auch für lesende und schreibende Zugriffe benutzt. 

Ein erster naiver Eindruck von CQL scheint nun zu zeigen, dass CQL und SQL sehr viel Analogien aufweisen. Es gibt ein CREATE TABLE ebenso wie ein SELECT FROM mit einer fast identischen Parametrisierung. 

Allerdings zeigt schon eine erste konkrete Umsetzung, dass diese Analogie äußerst beschränkt ist.

So muss zu allererst ein so genannter Keyspace eingerichtet werden, der in jedem Falle einen Replikationsfaktor definiert.

CREATE KEYSPACE PUBLISHING
WITH REPLICATION = {
'class': 'SimpleStrategy',
'replication_factor': 1
};

 

Dies ist in der Ring-Cluster-Architektur von Cassandra begründet, die eine Ausfallsicherheit durch Daten-Replikation über Knoten hinweg realisiert.

Das Anlegen einer Tabelle erfolgt dann scheinbar analog zu SQL. So wird im Folgenden eine Tabelle PUBLISHING angelegt, deren Intention es sein soll, einem Verleger Bücher zuzuordnen:

CREATE TABLE PUBLISHING(
publisher_name text,
title text,
isbn text,
price double,
pages int,
available boolean,
primary key(publisher_name, title)
);

Aber eine etwas genauere Analyse zeigt, dass diese Tabelle einen komplett anderen Aufbau hat als bei einem relationalen Modell. So würde ja in einem relationalen Datenbanksystem wohl eine Tabelle PUBLISHERS über einen Fremdschlüssel mit der Tabelle BOOKS verknüpft werden. 

Was ist nun aber der Sinn der obigen Tabellen-Definition:

  1. Der angegebene Primärschlüssel ist komplett etwas anderes. Der erste Teil, hier der publisher_name, ist der so genannte Partition Key. Cassandra verteilt die Datensätze an Hand des Partition Keys auf die Knoten des Clusters. Damit müssen effiziente Abfragen stets mit einer einleitenden WHERE <PARTITION_KEY> = <VALUE> eingeleitet werden. Ohne diese zentrale Selektion muss der Cluster die Abfrage auf allen Knoten des Rings ausführen, also eine Art “Full Table Scan” ausführen, was nicht sonderlich effizient ist.
  2. Der zweite Teil des Primary Key ist eine Liste von so genannten “Clustering Columns”.  Nur mit Hilfe einer Clustering Column kann ergänzend zum Partition Key die Abfrage erweitert werden. So kann in obigem Beispiel die Where-Klausel geschrieben werden als WHERE pulisher_name = 'Manning' and title = 'Cassandra in Action'. Eine weitere Selektion, also beispielsweise AND price = 19.99 ist nicht möglich. 

Die zweite obige Anmerkung zeigt den Spalten-orientierten Charakter von Cassandra. Die Clustering Column title ist nämlich ein Key in einer Map, dessen Value dann den zusätzlichen Buch-Informationen entspricht. Dieses Value wird von Cassandra aber als unstrukturiertes Dokument angenommen und wird deshalb in effizienten Abfragen nicht benutzt. 

Was wäre aber die Konsequenz, wenn eine weitere Clustering Column, also beispielsweise die pages mit in den Primary Key aufgenommen werden würden: 

primary key(publisher_name, title)

Fangen wir mit der Spalten-orientierten Arbeitsweise an: Die über den Partition Key definierte Map enthält als Schlüssel beispielsweise den Titel  'Cassandra in Action' . Das zugehörige Value ist nun aber wiederum eine Map! Und diese enthält für jeden Key-Eintrag der Seiten wiederum die restlichen Informationen des Buchs. Oder anders formuliert: Die PUBLISHING-Tabelle enthält für jeden Buch-Titel eine Liste von Seiten, die dann jeweils die ISBN, den Preis sowie die Verfügbarkeit enthält. 

Eine Abfrage der Form WHERE publisher_name = 'Manning' AND title = 'Cassandra in Action' AND pages = 500 ist vollkommen korrekt, ebenso title = 'Cassandra in Action'. Falsch hingegen wäre  WHERE publisher_name = 'Manning' AND pages = 500: Hier wird in der Navigation über die verschachtelten Maps eine Ebene ausgelassen, so dass wiederum eine sehr ineffiziente Abfrage resultieren würde.

Query First

Wir sehen damit, dass bei der Datenmodellierung mit Cassandra der Ausgangspunkt nicht das statische Datenmodell sein muss, sondern die auf den Datenbestand auszuführenden Abfragen. Dies wird als “Query First”-Ansatz bezeichnet.

Notwendige Konsequenz von Query First ist eine komplette Denormalisierung des Datenbestandes. Sollen verschiedene Kombinationen von Abfrage-Kriterien unterstützt werden so muss in Cassandra jeweils eine eigene Tabelle angelegt werden, so dass Daten redundant abgelegt werden müssen. 

Moderne Cassandra-Versionen

Die durch den “Query First”-Ansatz entstandenen Daten-Redundanzen sind für viele Modellierer schwer zu akzeptieren. Die Cassandra-Community hat deshalb in den letzten Jahren immer mehr Features und Optimierungen eingeführt, die dieses Problem lösen sollen. So können beispielsweise durch Indizes sehr wohl auch Abfragen bis in die Ebene der Value-Dokumente durchgeführt werden. Weiterhin führen Abfragen ohne die Angaben des Partition Key “nur” zu einer Warnung. Trotzdem müssen für einen sinnvollen Einsatz des Apache Cassandra-Datenbanksystems auch in den neuesten Versionen die technische Grundlagen sicher beherrscht und berücksichtigt werden. Sonst ist eine Ergänzung oder sogar Ablösung eines relationalen Datenbanksystems durch Cassandra sinnlos bzw. sogar  nachteilig. 


Seminare zum Thema

Weiterlesen

Garbage Collection

Garbage Collection und Java Virtual Machine

Alle erzeugten Objekte werden im Heap-Speicher der JVM (Java Virtual Machine) abgelegt. Da aber dieser Speicherbereich nicht unendlich groß ist, müssen die nicht mehr benötigten Objekte wieder entfernt werden. Diese Aufgabe könnte uns als Entwickler obliegen. Doch wir haben Glück: Java kennt eine automatische Speicherbereinigung, die Garbage Collection (Übersetzt: Müllabfuhr). Alle nicht referenzierten Objekte werden aus dem Heap-Speicher entfernt.
Trotzdem kann es durch unachtsame Programmierung zu Speicherlecks (Memory Leaks) kommen. Wenn eine nicht mehr benötigte Referenz noch ein Objekt, dies kann auch eine sehr große Collection sein, referenziert, wird dieses Objekt eben nicht aus dem Speicher entfernt.

Beispiel

Anhand eines kleinen Beispiels möchte ich die Garbage Collection erläutern:

public class Person {

  private String name;

  private Person partner;

  public Person( String name ) { this.name = name; }

  public Person getPartner() { return partner;       }

  public void setPartner(Person partner) { this.partner = partner; }

  public static void main(String[] args) {

    Person [ ] personen = { new Person( "Margit"), new Person("Hans" ) };

  }

}

Anschließend wollen wir unserer Person Margit einen Partner zuweisen.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

}

Was passiert aber, wenn wir das Array an der Stelle 0 mit null initialisieren?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

           

  personen[0] = null;

}

Arbeitsweise

Der Garbage Collector wird ausgehend von unserer Klasse Person auf dem Stack die main-Methode untersuchen. Welche Objekte können von hier erreicht werden? Über die lokale Variable personen wird das Array erreicht. An der Stelle 1 im Array wird die Person Hans referenziert und somit werden weder das Array noch die Person Hans aus dem Heap-Speicher entfernt. Der Speicher der Person Margit ist ohne Referenzierung und wird somit freigegeben.

Wie sieht es im nächsten Fall aus?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

}

Nun initialisieren wir die lokale Variable personen mit null.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  personen = null;

}

Jetzt sieht die Sache etwas anders aus. Der Garbage Collector beginnt in der main-Methode und findet die null-Referenz in der lokalen Variablen personen, Somit ist von hier kein Objekt erreichbar. Obwohl Margit und auch Hans jeweils referenziert sind, werden sie aus dem Heap-Speicher entfernt, da sie aus der main-Methode unerreichbar sind.

 

Im nächsten Beispiel sehen wir eine zusätzliche lokale Variable misterX. Wenn nun die lokale Variable personen mit null initialisiert wird, entfernt der Garbage Collector nur das Array, nicht aber die Personen Margit und Hans, da es noch eine Referenz misterX auf die Person Margit gibt.

In der Praxis sind dies oft nicht mehr benötigte oder vergessene Referenzen, so dass die Garbage Collection diese Objekte im Speicher belässt. Man spricht hier von Memory Leaks oder Speicherlecks. Dies könnte man hier durch Dereferenzieren der lokalen Variablen misterX leicht verhindern.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit") , new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  Person misterX = personen[ 0 ].getPartner( );

            

  personen = null;

}

Bevor der Garbage Collector ein Objekt aus dem Speicher entfernt, wird immer noch die Methode finalize( ) durchlaufen. Diese ist in der Klasse Object implementiert und kann überschrieben werden. Da der Entwickler aber weder weiß wann die Garbage Collection läuft, noch ob sie überhaupt läuft, ist diese Methode mit Vorsicht zu genießen. Seit JAVA 9 ist sie deprecated gekennzeichnet, weil viele Programmierer sie fehlerhaft eingesetzt haben.

Zusammenfassung

Dies war eine kurze einfache Darstellung der Garbage Collection. Alle nicht mehr erreichbaren Objekte ausgehend von der Root-Klasse und der main werden automatisch in parallelen Threads aus dem Heap-Speicher entfernt. Der Zeitpunkt ist für uns Entwickler nicht vorhersehbar.

In unserem Seminar Java Erweiterungen II (03323) wird das Thema mit schwachen (Weak Reference) und starken Referenzen vertieft.

 

Seminar zum Thema

 

Bildnachweis: Photo by Melodi2 at Morguefile.com

Weiterlesen