Contents

CockroachDB 简介

1. 简介

本教程是在 Java 中使用 CockroachDB 的介绍性指南。

我们将解释关键特性、如何配置本地集群以及如何监控它,以及关于如何使用 Java 连接服务器并与服务器交互的实用指南。

让我们首先定义它是什么。

2. CockroachDB

CockroachDB 是一个分布式 SQL 数据库,构建在事务性和一致的键值存储之上。

用 Go 编写并且完全开源,**其主要设计目标是支持 ACID 事务、水平可扩展性和可生存性。**有了这些设计目标,它的目标是容忍从单个磁盘故障到整个数据中心崩溃的所有情况,同时将延迟中断降至最低,并且无需人工干预。

因此,**CockroachDB 可以被认为是一个非常适合需要可靠、可用和正确数据的应用程序的解决方案,无论规模如何。**但是,当非常低的延迟读取和写入至关重要时,它不是首选。

2.1. 主要特征

让我们继续探索 CockroachDB 的一些关键方面:

  • SQL API 和 PostgreSQL 兼容性——用于结构化、操作和查询数据
  • ACID 事务——支持分布式事务并提供强一致性
  • 云就绪– 设计用于在云中或本地解决方案上运行,可在不同云提供商之间轻松迁移,而不会中断任何服务
  • 水平扩展——增加容量就像将新节点指向正在运行的集群一样简单,操作员开销最小
  • 复制——复制数据以获得可用性并保证副本之间的一致性
  • 自动修复——只要大多数副本仍然可用于短期故障,就可以无缝继续,而对于长期故障,使用未受影响的副本作为源自动重新平衡丢失节点的副本

3. 配置 CockroachDB

安装 CockroachDB 后,我们可以启动本地集群的第一个节点:

cockroach start --insecure --host=localhost;

出于演示目的,我们使用insecure属性,使通信未加密,无需指定证书位置。

至此,我们的本地集群已启动并运行。只有一个节点,我们已经可以连接到它并运行,但是为了更好地利用 CockroachDB 的自动复制、重新平衡和容错,我们将添加另外两个节点

cockroach start --insecure --store=node2 \
  --host=localhost --port=26258 --http-port=8081 \
  --join=localhost:26257;
cockroach start --insecure --store=node3 \
  --host=localhost --port=26259 --http-port=8082 \
  --join=localhost:26257;

对于另外两个节点,我们使用join标志将新节点连接到集群,指定第一个节点的地址和端口,在我们的例子中是 localhost:26257 。本地集群上的每个节点都需要唯一的storeporthttp-port值。

在配置 CockroachDB 的分布式集群时,每个节点将位于不同的机器上,因此可以避免指定portstorehttp-port ,因为默认值就足够了。此外,在将其他节点加入集群时,应使用第一个节点的实际 IP。

3.1. 配置数据库和用户

一旦我们的集群启动并运行,通过 CockroachDB 提供的 SQL 控制台,我们需要创建我们的数据库和一个用户。 首先,让我们启动 SQL 控制台:

cockroach sql --insecure;

现在,让我们创建我们的testdb数据库,创建一个用户并向用户添加授权,以便能够执行 CRUD 操作:

CREATE DATABASE testdb;
CREATE USER user17 with password 'qwerty';
GRANT ALL ON DATABASE testdb TO user17;

如果我们想验证数据库是否创建正确,我们可以列出当前节点中创建的所有数据库:

SHOW DATABASES;

最后,如果我们想验证 CockroachDB 的自动复制功能,我们可以在其他两个节点之一上检查数据库是否创建正确。为此,我们必须在使用 SQL 控制台时表达port标志:

cockroach sql --insecure --port=26258;

4. 监控 CockroachDB

现在我们已经启动了本地集群并创建了数据库,我们可以使用 CockroachDB Admin UI 监控它们

/uploads/cockroachdb_java/1.png

这个管理 UI 与 CockroachDB 捆绑在一起,只要集群启动并运行,就可以通过http://localhost:8080 访问。特别是,它提供了有关集群和数据库配置的详细信息,并通过监控以下指标帮助我们优化集群性能

  • 集群健康——关于集群健康的基本指标
  • 运行时指标——关于节点计数、CPU 时间和内存使用情况的指标
  • SQL 性能——关于 SQL 连接、查询和事务的指标
  • 复制详细信息——关于如何跨集群复制数据的指标
  • 节点详细信息- 活动、死亡和退役节点的详细信息
  • 数据库详细信息- 有关集群中系统和用户数据库的详细信息

5.项目设置

鉴于我们正在运行的 CockroachDB 本地集群,为了能够连接到它,我们必须在 pom.xml 中添加一个额外的依赖项

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.1.4</version>
</dependency>

或者,对于 Gradle 项目:

compile 'org.postgresql:postgresql:42.1.4'

6. 使用 CockroachDB

现在我们已经清楚了我们正在使用什么并且一切都设置正确,让我们开始使用它。

由于 PostgreSQL 的兼容性,可以直接与 JDBC 连接或使用 ORM,例如 Hibernate。在我们的例子中,我们将使用 JDBC 与数据库进行交互。

为简单起见,我们将遵循基本的 CRUD 操作,因为它们是最好的开始。

让我们从连接到数据库开始。

6.1. 连接 CockroachDB

要打开与数据库的连接,我们可以使用DriverManager类的*getConnection()*方法。此方法需要连接 URL 字符串参数、用户名和密码:

Connection con = DriverManager.getConnection(
  "jdbc:postgresql://localhost:26257/testdb", "user17", "qwerty"
);

6.2. 创建表

通过工作连接,我们可以开始创建我们将用于所有 CRUD 操作的articles表:

String TABLE_NAME = "articles";
StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ")
  .append(TABLE_NAME)
  .append("(id uuid PRIMARY KEY, ")
  .append("title string,")
  .append("author string)");
String query = sb.toString();
Statement stmt = connection.createStatement();
stmt.execute(query);

如果我们想验证表是否被正确创建,我们可以使用SHOW TABLES命令:

PreparedStatement preparedStatement = con.prepareStatement("SHOW TABLES");
ResultSet resultSet = preparedStatement.executeQuery();
List tables = new ArrayList<>();
while (resultSet.next()) {
    tables.add(resultSet.getString("Table"));
}
assertTrue(tables.stream().anyMatch(t -> t.equals(TABLE_NAME)));

让我们看看如何修改刚刚创建的表。

6.3. 更改表格

如果我们在创建表的过程中遗漏了一些列,或者因为我们稍后需要它们,我们可以轻松地添加它们:

StringBuilder sb = new StringBuilder("ALTER TABLE ").append(TABLE_NAME)
  .append(" ADD ")
  .append(columnName)
  .append(" ")
  .append(columnType);
String query = sb.toString();
Statement stmt = connection.createStatement();
stmt.execute(query);

更改表后,我们可以使用SHOW COLUMNS FROM命令验证是否添加了新列:

String query = "SHOW COLUMNS FROM " + TABLE_NAME;
PreparedStatement preparedStatement = con.prepareStatement(query);
ResultSet resultSet = preparedStatement.executeQuery();
List<String> columns = new ArrayList<>();
while (resultSet.next()) {
    columns.add(resultSet.getString("Field"));
}
assertTrue(columns.stream().anyMatch(c -> c.equals(columnName)));

6.4. 删除表

在处理表格时,有时我们需要删除它们,这可以通过几行代码轻松实现:

StringBuilder sb = new StringBuilder("DROP TABLE IF EXISTS ")
  .append(TABLE_NAME);
String query = sb.toString();
Statement stmt = connection.createStatement();
stmt.execute(query);

6.5. 插入数据

一旦我们明确了可以对表执行的操作,我们现在就可以开始处理数据了。我们可以开始定义Article类:

public class Article {
    private UUID id;
    private String title;
    private String author;
    // standard constructor/getters/setters
}

现在我们可以看到如何将Article添加到我们的articles表中:

StringBuilder sb = new StringBuilder("INSERT INTO ").append(TABLE_NAME)
  .append("(id, title, author) ")
  .append("VALUES (?,?,?)");
String query = sb.toString();
PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setString(1, article.getId().toString());
preparedStatement.setString(2, article.getTitle());
preparedStatement.setString(3, article.getAuthor());
preparedStatement.execute();

6.6. 读取数据

一旦数据存储在表中,我们想要读取这些数据,这很容易实现:

StringBuilder sb = new StringBuilder("SELECT * FROM ")
  .append(TABLE_NAME);
String query = sb.toString();
PreparedStatement preparedStatement = connection.prepareStatement(query);
ResultSet rs = preparedStatement.executeQuery();

但是,如果我们不想读取articles表中的所有数据而只想读取一篇Article,我们可以简单地更改构建PreparedStatement的方式:

StringBuilder sb = new StringBuilder("SELECT * FROM ").append(TABLE_NAME)
  .append(" WHERE title = ?");
String query = sb.toString();
PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setString(1, title);
ResultSet rs = preparedStatement.executeQuery();

6.7. 删除数据

最后但同样重要的是,如果我们想从表中删除数据,我们可以使用标准的DELETE FROM命令删除一组有限的记录:

StringBuilder sb = new StringBuilder("DELETE FROM ").append(TABLE_NAME)
  .append(" WHERE title = ?");
String query = sb.toString();
PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setString(1, title);
preparedStatement.execute();

或者我们可以使用TRUNCATE函数删除表中的所有记录:

StringBuilder sb = new StringBuilder("TRUNCATE TABLE ")
  .append(TABLE_NAME);
String query = sb.toString();
Statement stmt = connection.createStatement();
stmt.execute(query);

6.8. 处理事务

一旦连接到数据库,默认情况下,每个单独的 SQL 语句都被视为一个事务,并在其执行完成后立即自动提交。

但是,如果我们希望允许将两个或多个 SQL 语句组合到一个事务中,我们必须以编程方式控制该事务。

首先,我们需要通过将ConnectionautoCommit属性设置为false来禁用自动提交模式,然后使用*commit()rollback()*方法来控制事务。

让我们看看在进行多次插入时如何实现数据一致性:

try {
    con.setAutoCommit(false);
    UUID articleId = UUID.randomUUID();
    Article article = new Article(
      articleId, "Guide to CockroachDB in Java", "blogdemo"
    );
    articleRepository.insertArticle(article);
    article = new Article(
      articleId, "A Guide to MongoDB with Java", "blogdemo"
    );
    articleRepository.insertArticle(article); // Exception
    con.commit();
} catch (Exception e) {
    con.rollback();
} finally {
    con.setAutoCommit(true);
}

在这种情况下,由于违反了主键约束,在第二次插入时引发了异常,因此在articles表中没有插入任何文章。