哈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 存储库本身中还有更多测试用例。