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 。本地集群上的每个节点都需要唯一的store、port和http-port值。
在配置 CockroachDB 的分布式集群时,每个节点将位于不同的机器上,因此可以避免指定port、store和http-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 监控它们:
这个管理 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 语句组合到一个事务中,我们必须以编程方式控制该事务。
首先,我们需要通过将Connection的autoCommit属性设置为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表中没有插入任何文章。