Hallo habr In diesem Artikel möchte ich eine Bibliothek für Java wie ClusterJ
, die es sehr einfach macht, mit der MySQL NDBCLUSTER
Engine aus Java
Code zu arbeiten, einer API auf hoher Ebene, deren Konzept JPA
und Hibernate
ähnelt.
Im Rahmen dieses Artikels erstellen wir eine einfache Anwendung auf SpringBoot
und erstellen einen Starter mit ClusterJ
an Bord für die bequeme Verwendung in Anwendungen mit ClusterJ
. Wir werden einfache Tests mit JUnit5
und JUnit5
schreiben, die die grundlegende Verwendung der API zeigen.
Ich werde auch über einige Mängel sprechen, denen ich bei der Arbeit mit ihr begegnen musste.
Wen kümmert es, willkommen bei Katze.
Einleitung
MySQL NDB Cluster
aktiv bei der Arbeit verwendet. In einem der Projekte bestand die Aufgabe aus Gründen der Geschwindigkeit darin, die ClusterJ
Bibliothek anstelle der üblichen JDBC
, die in ihrer API JPA
sehr ähnlich ist und tatsächlich einen Wrapper über die Bibliothek libndbclient.so
, die sie über JNI
.
Für diejenigen, die sich nicht auskennen, ist MySQL NDB Cluster eine hoch zugängliche und redundante MySQL-Version, die für eine verteilte Computerumgebung angepasst ist, die die NDB
Speicher- NDB
( NDBCLUSTER
) für die Arbeit in einem Cluster verwendet. Ich möchte hier nicht im Detail darauf eingehen, Sie können hier und hier mehr lesen
Es gibt zwei Möglichkeiten, mit dieser Datenbank aus Java-Code zu arbeiten:
- Standard über
JDBC
und SQL
Abfragen - Über
ClusterJ
für leistungsstarken Datenzugriff in der MySQL Cluster
.

ClusterJ basiert auf 4 Schlüsselkonzepten:
SessionFactory
- ein Analogon zum Verbindungspool, mit dem eine Sitzung SessionFactory
. Jede Instanz des Clusters muss über eine eigene SessionFactory verfügen.Session
- ist eine direkte Verbindung zu einem MySQL
Cluster.Domain Object
- Eine mit Anmerkungen versehene Schnittstelle, die eine Zuordnung einer Tabelle zu Java
Code darstellt, ähnlich wie JPA
.Transaction
- ist eine atomare Arbeitseinheit. Zu jedem Zeitpunkt wird in einer Sitzung eine Transaktion ausgeführt. Jede Operation (Empfangen, Einfügen, Aktualisieren, Löschen) wird in einer neuen Transaktion ausgeführt.
ClusterJ-Einschränkungen:
- Mangel an JOINs
- Es gibt keine Möglichkeit, eine Tabelle und Indizes zu erstellen. Verwenden Sie dazu
JDBC
. - Kein verzögertes Laden (
Lazy
). Der gesamte Datensatz wird auf einmal heruntergeladen. - In Domänenobjekten ist es nicht möglich, Beziehungen zwischen Tabellen zu definieren. Die Ähnlichkeit von
OneToMany
, ManyToOne
, ManyToMany
fehlt vollständig.
Übe. Reden ist billig. Zeig mir den Code.
Nun, genug Theorie, lasst uns zur Praxis übergehen.
Das erste Problem ist das Fehlen von ClusterJ
im zentralen Maven-Repository. Installieren Sie die Bibliothek mit Stiften im lokalen Repository. Es ist klar, dass es für immer in Nexus
oder einem Artifactory
liegen sollte, aber für unser Beispiel ist dies unnötig.
Gehen Sie also hierher und wählen Sie Ihr Betriebssystem. Wenn Sie ein Linux
ähnliches Betriebssystem verwenden, laden Sie das Paket mit dem Namen mysql-cluster-community-java
herunter und installieren Sie dieses rpm / deb-Paket. Wenn Sie Windows
, laden Sie das vollständige mysql-cluster-gp
Archiv herunter.
Auf die eine oder andere Weise haben wir eine JAR-Datei der Form: clusterj-{version}.jar
. Wir setzen es durch maven
:
mvn install:install-file -DgroupId=com.mysql.ndb -DartifactId=clusterj -Dversion={version} -Dpackaging=jar -Dfile=clusterj-{version}.jar -DgeneratePom=true
Wir benötigen auch die libndbclient
Bibliothek, eine Reihe von C++
Funktionen für die Arbeit mit der NDB API
, die ClusterJ
über die JNI
aufruft. Unter Windows
diese Bibliothek (.dll) im Archiv ndbclient_{version}
mysql-cluster-gp
Linux
Sie das Paket ndbclient_{version}
herunterladen.
Erstellen Sie als Nächstes ein Projekt. Wir werden SpringBoot
, JUnit5
+ TestContainers
für Tests verwenden.
Die endgültige Struktur des Projekts Das Projekt besteht aus zwei Modulen:
clusterj-spring-boot-starter
ist ein Starter, der ClusterJ
sowie eine Konfiguration enthält. Dank dieses Starters können wir die Verbindung zu MySQL NDB
in unserer Datei appliation.yml
wie folgt beschreiben:
clusterj: connectString: localhost:1186 dataBaseName: NDB_DB
Danach erstellt SpringBoot
für uns die für die Verbindung erforderliche SessionFactory
Factory.
clusterj-app
ist die Anwendung selbst, die unser Starter verwenden wird. Lassen Sie uns näher darauf eingehen.
Um zu beginnen, müssen wir ein Domänenmodell wie JPA
erstellen. Nur in diesem Fall müssen wir dies in Form einer Schnittstelle tun, deren Implementierung zur clusterj
:
import com.mysql.clusterj.annotation.Column; import com.mysql.clusterj.annotation.PersistenceCapable; import com.mysql.clusterj.annotation.PrimaryKey; @PersistenceCapable(table = "user") public interface User { @PrimaryKey int getId(); void setId(int id); @Column(name = "firstName") String getFirstName(); void setFirstName(String firstName); @Column(name = "lastName") String getLastName(); void setLastName(String lastName); }
Es gibt sofort ein Problem. Die Annotation PersistenceCapable
den Namen des Schemas oder der Datenbank angeben, in der sich die Tabelle befindet. Dies funktioniert jedoch nicht. Absolut. In ClusterJ
dies nicht implementiert. Daher sollten sich alle Tabellen, die über ClusterJ
, im selben Schema befinden, was zu einem Speicherauszug von Tabellen führt, die sich logischerweise in verschiedenen Schemas befinden sollten.
Versuchen wir nun, diese Schnittstelle zu verwenden. Dazu schreiben wir einen einfachen Test.
Um MySQL Cluster
nicht zu installieren, verwenden wir die wunderbare Bibliothek zum Testen der Integration von TestContainers und Docker . Da wir JUnit5 verwenden, werden wir eine einfache Extension
schreiben:
Quellcode der Erweiterung import com.github.dockerjava.api.model.Network; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.extension.Extension; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; import java.time.Duration; import java.util.stream.Stream; @Slf4j class MySQLClusterTcExtension implements Extension { private static final String MYSQL_USER = "sys"; private static final String MYSQL_PASSWORD = "qwerty"; private static final String CLUSTERJ_DATABASE = "NDB_DB"; private static Network.Ipam getIpam() { Network.Ipam ipam = new Network.Ipam(); ipam.withDriver("default"); Network.Ipam.Config config = new Network.Ipam.Config(); config.withSubnet("192.168.0.0/16"); ipam.withConfig(config); return ipam; } private static org.testcontainers.containers.Network network = org.testcontainers.containers.Network.builder() .createNetworkCmdModifier(createNetworkCmd -> createNetworkCmd.withIpam(getIpam())) .build(); private static GenericContainer ndbMgmd = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.2")) .withCommand("ndb_mgmd") .withExposedPorts(1186) .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(150))); private static GenericContainer ndbd1 = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.3")) .withCommand("ndbd"); private static GenericContainer ndbMysqld = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withCommand("mysqld") .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.10")) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .waitingFor(Wait.forListeningPort()) .withEnv(ImmutableMap.of("MYSQL_DATABASE", CLUSTERJ_DATABASE, "MYSQL_USER", MYSQL_USER, "MYSQL_PASSWORD", MYSQL_PASSWORD)) .withExposedPorts(3306) .waitingFor(Wait.forListeningPort()); static { log.info("Start MySQL Cluster testcontainers extension...\n"); Stream.of(ndbMgmd, ndbd1, ndbMysqld).forEach(GenericContainer::start); String ndbUrl = ndbMgmd.getContainerIpAddress() + ":" + ndbMgmd.getMappedPort(1186); String mysqlUrl = ndbMysqld.getContainerIpAddress() + ":" + ndbMysqld.getMappedPort(3306); String mysqlConnectionString = "jdbc:mysql://" + mysqlUrl + "/" + CLUSTERJ_DATABASE + "?useUnicode=true" + "&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false"; System.setProperty("clusterj.connectString", ndbUrl); System.setProperty("clusterj.dataBaseName", CLUSTERJ_DATABASE); System.setProperty("spring.datasource.username", MYSQL_USER); System.setProperty("spring.datasource.password", MYSQL_PASSWORD); System.setProperty("spring.datasource.url", mysqlConnectionString); } }
In dieser Erweiterung erhöhen wir den Steuerknoten des Clusters, ein Datum für den Knoten und den MySQL
Knoten. Danach legen wir die entsprechenden Verbindungseinstellungen für SpringBoot fest, genau die, die wir in der automatischen Starterkonfiguration beschrieben haben:
System.setProperty("clusterj.connectString", ndbUrl); System.setProperty("clusterj.dataBaseName", CLUSTERJ_DATABASE); System.setProperty("spring.datasource.username", MYSQL_USER); System.setProperty("spring.datasource.password", MYSQL_PASSWORD); System.setProperty("spring.datasource.url", mysqlConnectionString);
Als Nächstes schreiben wir eine Anmerkung, mit der wir Container in Tests deklarativ anheben können. Hier ist alles sehr einfach, wir verwenden unsere Erweiterung:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith(MySQLClusterTcExtension.class) public @interface EnableMySQLClusterContainer { }
Zum Schluss schreiben wir den Test:
@Test void shouldGetUserViaClusterJ() { User newUser = session.newInstance(User.class); newUser.setId(1); newUser.setFirstName("John"); newUser.setLastName("Jonson"); session.persist(newUser); User userFromDb = session.find(User.class, 1); assertAll( () -> assertEquals(userFromDb.getId(), 1), () -> assertEquals(userFromDb.getFirstName(), "John"), () -> assertEquals(userFromDb.getLastName(), "Jonson")); }
Dieser Test zeigt, wie wir den Datensatz per Primärschlüssel erhalten können. Diese Abfrage entspricht der SQL
Abfrage:
SELECT * FROM user WHERE id = 1;
Lassen Sie uns einen weiteren Test mit komplexerer Logik durchführen:
@Test void queryBuilderTest() { QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class);
QueryBuilder
verwendet, um komplexe Abfragen mit equal
QueryBuilder
zu erstellen. In diesem Test ziehen wir alle Benutzer heraus, deren Nachname = Jonson ist. Diese Abfrage entspricht der folgenden SQL
:
SELECT * FROM user WHERE lastName = 'Jonson';
Auch hier ist ein Problem aufgetreten. Abfrage des Formulars kann nicht erstellt werden:
SELECT * FROM user WHERE (lastName = 'Jonson' and firstName = 'John') or id = 2;
Diese Funktion ist derzeit nicht implementiert. Sie können den Test sehen: andOrNotImplemented
.
Vollständiges Testbeispiel @SpringBootTest @ExtendWith(SpringExtension.class) @EnableAutoConfiguration @EnableMySQLClusterContainer class NdbClusterJTest { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private SessionFactory sessionFactory; private Session session; @BeforeEach void setUp() { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `user` (id INT NOT NULL PRIMARY KEY," + " firstName VARCHAR(64) DEFAULT NULL," + " lastName VARCHAR(64) DEFAULT NULL) ENGINE=NDBCLUSTER;"); session = sessionFactory.getSession(); } @Test void shouldGetUserViaClusterJ() { User newUser = session.newInstance(User.class); newUser.setId(1); newUser.setFirstName("John"); newUser.setLastName("Jonson"); session.persist(newUser); User userFromDb = session.find(User.class, 1); assertAll( () -> assertEquals(userFromDb.getId(), 1), () -> assertEquals(userFromDb.getFirstName(), "John"), () -> assertEquals(userFromDb.getLastName(), "Jonson")); } @Test void queryBuilderTest() { User newUser1 = session.newInstance(User.class); newUser1.setId(1); newUser1.setFirstName("John"); newUser1.setLastName("Jonson"); User newUser2 = session.newInstance(User.class); newUser2.setId(2); newUser2.setFirstName("Alex"); newUser2.setLastName("Jonson"); session.persist(newUser1); session.persist(newUser2); QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class);
Dank unserer Anmerkung @EnableMySQLClusterContainer
haben wir die Details der Vorbereitung der Umgebung für Tests @EnableMySQLClusterContainer
. Dank unseres Starters können wir SessionFactory einfach in unseren Test einfügen und für unsere Anforderungen verwenden, ohne uns Gedanken darüber machen zu müssen, dass es manuell erstellt werden muss.
All dies konzentriert uns darauf, die Geschäftslogik der Tests zu schreiben und nicht die bedienende Infrastruktur.
Ich möchte auch darauf achten, dass Sie eine Anwendung ClusterJ
müssen, die ClusterJ
mit dem folgenden Parameter verwendet:
-Djava.library.path=/usr/lib/x86_64-linux-gnu/
Hier wird der Pfad zu libndbclient.so
. Ohne es wird nichts funktionieren.
Fazit
Für mich ist ClusterJ
gute Sache in Systemen, die für die Geschwindigkeit des Datenzugriffs von entscheidender Bedeutung sind, aber kleinere Mängel und Einschränkungen beeinträchtigen den Gesamteindruck. Wenn Sie die Möglichkeit haben zu wählen und sich nicht um die Geschwindigkeit des Zugriffs kümmern, ist es meiner Meinung nach besser, JDBC
zu verwenden.
Der Artikel hat nicht in Betracht gezogen, mit Transaktionen und Sperren zu arbeiten, und so stellte sich heraus, dass es ziemlich viel war.
Das war's, Happy Coding!
Nützliche Links:
Der gesamte Code mit dem Projekt liegt hier
Seite herunterladen
Informationen zu ClusterJ
Arbeiten Sie mit Java und NDB Cluster
Pro MySQL NDB-Clusterbuch
Mehr über MySQL NDB Cluster hier und hier
Noch mehr Testfälle im MySQL
Repository .