哈Ha! 在本文中,我想考虑一个Java库,例如ClusterJ
,它使使用Java
代码的MySQL NDBCLUSTER
引擎变得非常容易,该引擎的概念类似于JPA
和Hibernate
,是高级API。
在本文的框架中,我们将在SpringBoot
上创建一个简单的应用程序,并使用ClusterJ
创建一个入门ClusterJ
,以便在使用自动配置的应用程序中方便使用。 我们将使用JUnit5
和TestContainers
编写简单的测试,这些测试将显示API的基本用法。
我还将讨论在与她合作的过程中我必须面对的几个缺点。
谁在乎,欢迎来猫。
引言
为了提高速度, MySQL NDB Cluster
在工作中和一个项目中MySQL NDB Cluster
积极使用,其任务是使用ClusterJ
库而不是通常的JDBC
,后者的API与JPA
非常相似,并且实际上是libndbclient.so
库的包装器,它是通过JNI
使用的。
对于那些不了解的人,MySQL NDB Cluster是一个高度可访问且冗余的MySQL版本,适用于使用NDB
存储NDB
( NDBCLUSTER
)在群集中运行的分布式计算环境。 我不想在这里详细介绍,您可以在这里和这里阅读更多内容
使用该数据库的Java代码有两种方法:
- 标准,通过
JDBC
和SQL
查询 - 通过
ClusterJ
,用于MySQL Cluster
中的高性能数据访问。

ClusterJ基于4个关键概念构建:
SessionFactory
连接池的类似物,用于获取会话。 群集的每个实例都必须具有自己的SessionFactory。Session
-是与MySQL
群集的直接连接。Domain Object
-带注释的接口,表示表到Java
代码的映射,类似于JPA
。Transaction
-是工作的原子单元。 在任何给定时间,在一个会话中,将执行一个事务。 任何操作(接收,插入,更新,删除)都在新事务中执行。
ClusterJ的局限性:
- 缺乏加盟
- 无法创建表和索引。 为此,请使用
JDBC
。 - 没有延迟加载(
Lazy
)。 整个记录一次下载。 - 在域对象中,无法定义表之间的关系。 完全没有
OneToMany
, ManyToOne
, ManyToMany
的相似性。
练习 谈话很便宜。 给我看代码。
好了,理论足够多,让我们继续练习。
面临的第一个问题是中央Maven存储库中缺少ClusterJ
。 用笔将库安装在本地存储库中。 很明显,它应该位于Nexus
或某些Artifactory
,但对于我们的示例而言,这是不必要的。
因此,请转到此处并选择您的操作系统。 如果您使用的是类似Linux
操作系统,请下载名为mysql-cluster-community-java
软件包并安装此rpm / deb软件包。 如果您使用Windows
,请下载完整的mysql-cluster-gp
存档。
一种或另一种方式,我们将具有以下形式的jar文件: clusterj-{version}.jar
。 我们把它通过了maven
:
mvn install:install-file -DgroupId=com.mysql.ndb -DartifactId=clusterj -Dversion={version} -Dpackaging=jar -Dfile=clusterj-{version}.jar -DgeneratePom=true
我们还需要libndbclient
库,它是一组C++
函数,用于处理ClusterJ
通过JNI
调用的NDB API
。 对于Windows
此库(.dll)位于mysql-cluster-gp
归档文件中,对于Linux
您需要下载ndbclient_{version}
软件包。
接下来,创建一个项目。 我们将使用SpringBoot
, JUnit5
+ TestContainers
进行测试。
该项目包含两个模块:
clusterj-spring-boot-starter
是一个包含ClusterJ
以及atoconfiguration的启动器。 感谢这个入门者,我们可以在appliation.yml
文件中描述与MySQL NDB
的连接,如下所示:
clusterj: connectString: localhost:1186 dataBaseName: NDB_DB
之后, SpringBoot
将为我们创建连接所需的SessionFactory
工厂。
clusterj-app
是应用程序本身,我们的入门人员将使用它。 让我们更详细地讨论它。
首先,我们需要创建一个域模型,例如JPA
。 仅在这种情况下,我们需要以接口的形式执行此操作,在clusterj
将由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); }
马上有问题。 PersistenceCapable
批注可以指定表所在的模式或数据库的名称,但这不起作用。 绝对是 在ClusterJ
这没有实现。 因此,所有通过ClusterJ
工作的表都应在同一模式中,这将导致表转储,从逻辑ClusterJ
,表应在不同的模式中。
现在,让我们尝试使用此界面。 为此,我们编写了一个简单的测试。
为了不麻烦安装MySQL Cluster
,我们将使用出色的库对TestContainers和Docker进行集成测试。 由于我们使用的是JUnit5,我们将编写一个简单的Extension
:
扩展源代码 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); } }
在此扩展中,我们提出了群集的控制节点,该节点的一个日期以及MySQL
节点。 之后,我们设置适当的连接设置以供SpringBoot使用,就像我们在启动程序自动配置中描述的那样:
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);
接下来,我们编写一个注释,使我们可以在测试中声明性地引发容器。 这里的一切都很简单,我们使用扩展程序:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith(MySQLClusterTcExtension.class) public @interface EnableMySQLClusterContainer { }
最后,我们编写测试:
@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")); }
此测试显示了如何通过主键获取记录。 此查询等效于SQL
查询:
SELECT * FROM user WHERE id = 1;
让我们用更复杂的逻辑做另一个测试:
@Test void queryBuilderTest() { QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class);
QueryBuilder
用于以in
, where
, equal
QueryBuilder
( like
QueryBuilder
构建复杂的查询。 在此测试中,我们提取了姓氏= Jonson的所有用户。 此查询等效于以下SQL
:
SELECT * FROM user WHERE lastName = 'Jonson';
这里也遇到了问题。 无法组成以下形式的查询:
SELECT * FROM user WHERE (lastName = 'Jonson' and firstName = 'John') or id = 2;
当前未实现此功能。 您可以看到测试: andOrNotImplemented
。
完整的测试示例 @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);
感谢我们的注释@EnableMySQLClusterContainer
,我们隐藏了准备测试环境的详细信息。 同样,由于我们的入门者,我们可以简单地将SessionFactory注入我们的测试中,并将其用于我们的需求,而不必担心需要手动创建它的事实。
所有这些使我们专注于编写测试的业务逻辑,而不是服务基础结构。
我还想注意一个事实,您需要运行一个将ClusterJ
与参数一起使用的应用程序:
-Djava.library.path=/usr/lib/x86_64-linux-gnu/
其中显示了libndbclient.so
的路径。 没有它,什么都不会起作用。
结论
对于我来说,在那些对数据访问速度至关重要的系统中, ClusterJ
一件好事,但细微的缺陷和限制破坏了整体印象。 如果您有机会选择并且不关心访问速度,那么我认为使用JDBC
更好。
本文没有考虑使用事务和锁,因此结果很多。
就是这样,快乐编码!
有用的链接:
该项目的所有代码都位于此处
下载页面
有关ClusterJ的信息
使用Java和NDB集群
Pro MySQL NDB丛书
在此处和此处了解有关MySQL NDB群集的更多信息
MySQL
存储库本身中还有更多测试用例。