2019-02-21 Cassandra权威指南(2版)8章 【译】

要开始构建我们的应用程序,我们将使用驱动程序的API连接到我们的集群。
在Java驱动程序中,它由com.datastax.driver.core.Cluster和Session类表示。
Cluster类是驱动程序的主要入口点。
它使用构建器模式支持流式的API。
例如,以下行创建与本地主机上运行的节点的连接:

Cluster cluster = Cluster.builder().
addContactPoint("127.0.0.1").build();

这一个语句代表创建集群所需的最低信息:一个联络点。
我们还可以指定多个联络点。
联络点类似于Cassandra种子节的概念,用于连接到同一群集中的其他节点。

创建自定义Cluster初始化程序
Cluster.Builder类实现了一个名为Cluster.Initializer的接口。
这允许我们使用静态方法Cluster.buildFrom(Initializer初始化程序)插入不同的机制来初始化Cluster。
例如,如果我们想要从配置文件加载连接信息,这可能很有用。
我们可以在群集上配置其他几个选项,例如度量标准,默认查询选项以及重新连接,重试和推测执行的策略。
在我们看一些其他与连接相关的选项后,我们将在后面的章节中研究这些选项中的每一个:协议版本,压缩和身份验证。

DataStax Java Driver

Protocol version

默认情况下,驱动使用第一个连接上的节点所支持的协议版本。
虽然在大多数情况下是没有问题的,但是如果您使用的是基于旧版本Cassandra的集群,你可能需要重写。
你可以通过从com.datastax传递所需的值选择协议版本。

Compression

利用CQL本地协议所支持的压缩选项,驱动提供你的客户端和Cassandra节点间的压缩信息的选项。
启用压缩可以减少驱动程序消耗的网络带宽,但代价是客户端和服务器的CPU使用量会增加。
目前可使用的压缩算法有两种LZ4和SNAPPY,它们在com.datastax.driver.core.ProtocolOptions中被定义。
compression默认值是NONE,但是可以通过调用Cluster.Builder.withCompression() 操作来重写。

Authentication and encryption

认证与加密
驱动提供可插入的身份验证机制,可用于支持简单的用户名/密码登录,或与其他身份验证系统集成。
默认情况下,不执行身份验证。
您可以通过将com.datastax.driver.core.AuthProvider接口(如PlainTextAuthProvider)的实现传递给Cluster.Builder.with AuthProvider()操作来选择身份验证提供程序。
驱动程序还可以加密与服务器的通信以确保隐私。
客户端 - 服务器加密选项由每个节点在其cassandra.yaml文件中指定。
驱动程序分别匹配并执行每个节点指定的加密设置。

我们将在第13章中从客户端和服务器角度更详细地检查身份验证,授权和加密。

Sessions and Connection Pooling

会话和连接池

在我们创建Cluster实例之后,还没有连接到任何Cassandra节点直到我们通过调用init()方法来初始化:
cluster.init();
当调用这个方法后,驱动连接到其中一个已配置的联络点以获取有关群集的元数据。
如果没有联络点可以获得,这个操作会抛出一个NoHostAvailableException异常。
如果认证失败的话,则抛出 AuthenticationException异常。我们将在13章讨论更多关于认证的细节。
一旦我们初始化了我们的Cluster对象,我们需要建立一个会话来制定我们的查询。
我们可以通过调用Cluster.connect()中的某个操作来获得一个com.datastax.driver.core.Session对象。
你可以可选地提供要连接的键空间(keyspace)的名称,就像我们在如下示例中连接到hotel键空间一样:

Session session = cluster.connect("hotel"); 

也有一个没有参数的connect()重载方法,它会创建一个能够被多个keyspace使用的会话。
如果你选择没有参数的方法,你必须使用适当的键空间名称限定查询中的每个表引用。
注意,并不严格要求显式调用Cluster.init(),因为当我们调用connect()时也会在后台调用它。
每个会话管理着到一个Cassandra集群的连接,会话用于使用Cassandra本地协议来执行查询和控制操作。
会话包含每个主机的TCP连接池。

由于会话维护多个节点的连接池,因此它是一个相对重量级的对象。在大多数情况下,你需要创建一个会话,并在整个应用程序中重复使用它,而不是不断地构建和拆除Sessions。 另一个可接受的选择是,如果你的应用程序正在访问多个键空间,那么可以每个键空间创建一个Session。

由于CQL本地协议是异步的,因此每个连接允许多个同时请求(并发请求);协议v2中最多为128个并发请求,而v3和v4允许最多32,768个并发请求。
由于并发请求数量巨大,因此要求每个节点的连接控制在较少数量。 实际上,默认值是每个节点一个连接。
驱动支持基于每个连接的请求数来调整连接数目。
这些连接池设置可通过PoolingOptions类进行配置,该类设置用于本地和远程主机的最大和最小(或“核心”)连接数。
如果核心值和最大值不同,则驱动程序会根据客户端发出的请求数量增大或减小每个节点的连接池大小。
每个连接的最小和最大请求阈值的设置用于确定何时创建新连接,以及何时可以回收未充分利用的连接。还有一个缓冲期可以防止连续建立和拆除连接。

当使用ClusterBuilder.withPoolingOptions()创建Cluster时,可以设置PoolingOptions,或者当Cluster被创建后使用Cluster.getConfiguration().getPoolingOptions()进行操作。
以下是一个创建Cluster的示例,该Cluster将远程节点的最大连接数限制为一个:

PoolingOptions poolingOptions = new PoolingOptions().
setMaxConnectionsPerHost(HostDistance.REMOTE, 1);

Cluster cluster = Cluster.builder().
addContactPoint("127.0.0.1").
withPoolingOptions(poolingOptions).build();

驱动程序提供连接心跳,用于确保不会因为干预网络设备而过早关闭连接。
默认为30秒,但可以使用PoolingOptions.setHeartbeatIntervalSeconds()操作覆盖。
但是,这仅适用于设置值后建立的连接,因此你需要在创建群集时对其进行配置。

Statements

到目前为止,我们只配置了与集群的连接,并且尚未执行任何读取或写入操作。
要开始做一些真正的应用程序工作,我们将使用com.datastax.driver.core.Statement类及其各种子类创建和执行语句。
Statement是一个具有多个实现的抽象类,包括SimpleStatement,PreparedStatement,BoundStatement,BatchStatement和BuiltStatement。
创建和执行语句的最简单方法是使用表示语句的字符串作为参数调用Session.execute()操作。
这是一个语句的示例,它将返回hotels表的全部内容:

session.execute("SELECT * from hotel.hotels");

此语句在单个方法调用中创建并执行查询。
实际上,在大型数据库中执行此语句,会成为非常昂贵的查询,但它确实是一个适合的示例用来展示一个非常简单的查询。

Expensive queries are database queries that run slowly and/or spend a significant amount of their execution time reading and writing to disk.

我们需要构建的大多数查询都会更复杂,因为我们将有指定的搜索条件或特定的值要插入。
我们当然可以使用Java的各种字符串实用程序来手动构建查询的语法,但这当然容易出错。
如果我们不小心清理来自终端用户的字符串,它甚至可能会将我们的应用程序暴露给注入攻击。

Simple statement

值得庆幸的是,我们不必把事情弄得那么复杂。
Java驱动程序提供SimpleStatement类以帮助构造参数化语句。
事实证明,我们之前看到的execute()操作实际上是一种创建SimpleStatement的便捷方法。

让我们尝试通过Session对象来创建SimpleStatement来构建查询。
这是一个语句的示例,它将在我们的hotels表中插入一行,我们可以执行:

SimpleStatement hotelInsert = 
session.newSimpleStatement
(
 "INSERT INTO hotels (hotel_id, name,phone) VALUES (?, ?, ?)", "AZ123", "Super Hotel at WestWorld", "1-888-999-9999"
);
session.execute(hotelInsert);

调用中的第一个参数是查询的基本语法,指示我们感兴趣的表(hotels)和列。
问号用于表示我们将提供值的参数。
我们使用简单的字符串来保存酒店ID(hotel ID),姓名(name)和电话号码(phone number)的值。
如果我们正确创建了语句(statement),插入将成功执行(并且静默)。
现在让我们创建另一个语句来回读刚刚插入的行:

SimpleStatement hotelSelect = session.newSimpleStatement("SELECT * FROM hotels WHERE id=?", "AZ123");
ResultSet hotelSelectResult = session.execute(hotelSelect);

同样,我们使用参数化来为我们的搜索提供ID。
这次,当我们执行查询时,我们需要接收从execute() 方法返回的ResultSet。
我们可以迭代结果集返回的行,如下所示:

for (Row row : hotelSelectResult) {
   System.out.format("hotel_id: %s, name: %s, phone: %s\n",
                       row.getString("hotel_id"), row.getString("name"), row.getString("phone"));
}

这段代码使用ResultSet.iterator()选项在结果集中的行上获取迭代器,并在每行上循环,打印出所需的列值。
注意,我们使用特殊访问器来获取每列的值,具体取决于所需的类型 - 在本例中为Row.getString()。正如我们所料,这将打印出来结果如下:

hotel_id: AZ123, name: Super Hotel at WestWorld, phone: 1-888-999-9999

Using a Custom Codec

使用自定义编解码器

正如我们已经指出的那样,我们需要知道在与ResultSet中的Rows交互时我们请求的列的类型。
如果我们使用Row.getString()请求id列,我们将收到CodecNotFoundException异常,表明驱动程序不知道如何将CQL类型uuid映射到java.lang.String。
这里发生的是驱动程序维护Java和CQL类型之间的默认映射列表,称为编解码器,它用于在应用程序和Cassandra之间来回转换。
驱动程序提供了一种通过继承(扩展)类com.datastax.driver.core.TypeCodec来添加其他映射的方法。
并将其注册到由Cluster管理的CodecRegistry:

cluster.getConfiguration().getCodecRegistry().register(myCustomCodec)

自定义编解码器机制非常灵活,如以下用例所示:
• Mapping to alternate date/time formats (e.g., Joda time for pre-Java 8 users)
• Mapping string data to/from formats such as XML and JSON
• Mapping lists, sets, and maps to various Java collection types
你可以在示例com.cassandraguide.clients.SimpleStatementExample中找到使用SimpleStatement的示例代码。

Asynchronous execution

异步执行

Session.execute()操作是同步的,这意味着它会阻塞直到获得结果或发生错误,例如网络超时。
驱动程序还提供异步executeAsync()操作以支持与Cassandra的非阻塞交互。
这些非阻塞请求可以使并行发送多个查询更加简单,从而提高客户端应用程序的性能。
让我们从之前的操作开始并修改它以使用异步操作:

ResultSetFuture result = session.executeAsync(statement);

结果是ResultSetFuture类型,它是java.util.concurrent.Future接口的实现。
Future是一种Java泛型类型,用于捕获异步操作的结果。
可以检查每个Future以查看操作是否已完成,然后根据绑定类型进行查询操作。
还有阻塞等待结果的wait()操作。
如果调用者对操作结果不再感兴趣,也可以取消Future。
Future类是实现异步编程模式的有用工具,但需要阻塞或轮询等待操作完成。
为了解决这个缺点,Java驱动程序利用了Google的Guava框架中的ListenableFuture接口。
ListenableFuture接口扩展了Future,并添加了一个addListener()操作,该方法允许客户端注册当Future完成后的回调方法。
回调方法在由驱动程序管理的线程中调用,因此让该方法快速完成是很重要,以避免占用驱动程序资源。ResultSetFuture绑定到ResultSet类型。

其它异步操作 除了Session.executeAsync()操作外,驱动程序支持其他几个异步操作,包括Cluster.closeAsync(),Session.prepareAsync()和几个对象映射器上的操作。

Prepared statement

预编译语句

虽然SimpleStatements对于创建 ad hoc 查询非常有用,但大多数应用程序倾向于重复执行同一组查询。PreparedStatement旨在更有效地处理这些查询。
语句的结构一次性发送到节点进行准备,并返回该语句的句柄。
要使用预编译语句(prepared statement),只需要发送句柄和参数。

An Ad-Hoc Query is a query that cannot be determined prior to the moment the query is issued. It is created in order to get information when need arises and it consists of dynamically constructed SQL which is usually constructed by desktop-resident query tools.
Ad-Hoc Query是在发出查询之前无法确定的查询。 它的创建是为了在需要时获取信息,它由动态构造的SQL组成,通常由桌面驻留查询工具构建。

在构建应用程序时,通常会创建PreparedStatements以读取数据,这些数据对应于您在数据模型中派生的每种访问模式,以及其他用于将数据写入表以支持这些访问模式的数据。
让我们使用Session.prepare() 操作创建一些PreparedStatements来表示与以前相同的酒店查询:

PreparedStatement hotelInsertPrepared = 
       session.prepare("INSERT INTO hotels (hotel_id, name, phone) VALUES (?, ?, ?)");
PreparedStatement hotelSelectPrepared = session.prepare("SELECT * FROM hotels WHERE hotel_id=?");

请注意,PreparedStatement使用我们之前用于SimpleStatement的相同参数化语法。
但是,一个关键的区别是PreparedStatement不是Statement的子类型。
这可以防止尝试将未绑定的PreparedStatement传递给会话执行的错误。
在我们开始之前,让我们退后一步,讨论Session.prepare() 操作幕后发生的事情。
驱动程序将PreparedStatement的内容传递给一个Cassandra节点,并获取到关于该语句的唯一标识符。
创建BoundStatement时会引用此唯一标识符。
如果你很好奇,你可以通过调用PreparedStatement.getPreparedID()实际看到这个引用。

你可以将PreparedStatement视为用于创建查询的模板。
除了指定查询的格式之外,我们还可以在PreparedStatement上设置其他属性,这些属性将用作其用于创建的语句的默认值,包括默认一致性级别,重试策略和跟踪。
除了提高效率之外,PreparedStatements还通过将CQL的查询逻辑与数据分离来提高安全性。
这提供了针对试图将命令嵌入到数据字段中以便获得未经授权的访问的注入攻击的防护。

Bound statement

现在我们的PreparedStatement可供我们用来创建查询。
为了使用PreparedStatement,我们通过调用bind()操作将它与实际值绑定。
例如,我们可以绑定我们之前创建的SELECT语句,如下所示:

BoundStatement hotelSelectBound = hotelSelectPrepared.bind("AZ123");

我们在这里使用的bind()操作允许我们提供PreparedStatement中每个变量匹配的值。
可以提供前n个绑定值,在这种情况下,剩余值必须在执行之前单独绑定声明。
还有一个不带参数版本的bind(),在这种情况下,所有参数必须单独绑定。
BoundStatement提供了几个set() 操作,可用于绑定不同类型的值。
例如,我们可以从上面获取INSERT预处理语句,并使用setString()操作绑定name和phone:

BoundStatement hotelInsertBound = hotelInsertPrepared.bind("AZ123");
hotelInsertBound.setString("name", "Super Hotel at WestWorld");
hotelInsertBound.setString("phone", "1-888-999-9999");

一旦我们绑定了所有值,我们就使用Session.execute()执行BoundStatement。
如果我们绑定值中的任何一个失败了,如果正在使用协议v4(Cassandra 3.0或更高版本),它们将在服务器端被忽略。
旧协议版本的驱动程序则是,如果存在任何未绑定的值,则抛出IllegalStateException异常。
你可以在示例com.cassandraguide.clients.PreparedStatementExample中找到使用PreparedStatement和BoundStatement的示例代码。

Built statement and the Query Builder

构建语句和查询生成器

驱动程序还提供了com.datastax.driver.core.querybuilder.QueryBuilder类,该类为构建查询提供了流式API(fluent-style API)。
这适用于查询结构存在差异的情况(例如可选参数),在那些情况下,很难使用PreparedStatements。
与PreparedStatement类似,它也提供一些防止注入攻击的保护。
我们使用一个简单构造函数,传入Cluster对象,来构造一个QueryBuilder:

QueryBuilder queryBuilder = new QueryBuilder(cluster);

让我们在使用QueryBuilder之前再审视一下查询,看看它是如何工作的。
首先,我们将构建一个CQL INSERT查询:

BuiltStatement hotelInsertBuilt =
queryBuilder.insertInto("hotels")
.value("hotel_id", "AZ123")
.value("name", "Super Hotel at WestWorld")
.value("phone", "1-888-999-9999");

第一个操作调用QueryBuilder.insertInto() 操作为hotels表创建一个Insert语句。
如果需要,我们可以使用Insert.using() 向我们的语句添加CQL USING子句,但在这里,我们选择开始向查询添加值。
在我们添加值时,Insert.value() 操作继续返回Insert语句。
生成的Insert可以像使用Session.execute() 或executeAsync() 的任何其他Statement一样执行。
CQL SELECT命令的构造也类似:

BuiltStatement hotelSelectBuilt = queryBuilder.select()
.all()
.from("hotels")
.where(eq("hotel_id", "AZ123"));

对于此查询,我们调用QueryBuilder.select()来创建Select语句。
我们使用Select.all()操作来选择所有列,尽管我们也可以使用column()操作来选择特定的列。
我们通过Select.where()操作添加一个CQL WHERE子句,该操作接收Clause类的一个实例。
我们使用QueryBuilder提供的静态操作创建Clauses。
在这种情况下,我们使用eq()操作来检查与ID的相等性。
要访问这些静态操作,我们需要在Java源文件中添加其他import语句,例如:

import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;

有关使用QueryBuilder和BuiltStatement的完整代码示例,请参阅com.cassandraguide.clients.QueryBuilderExample类。

Object mapper

我们已经探索了几种使用驱动程序创建和执行查询语句的技术。
我们最后还将探索一种技术,它提供了更多的抽象。
Java驱动程序提供了一个对象映射器,使您可以专注于开发域模型(或API上使用的数据类型)并与之交互。
对象映射器使用源代码中的注释,用于将Java类映射到表或用户定义类型(UDTs)。

对象映射API作为与cassandra-driver-mapping.jar文件中驱动程序其余部分的单独库提供,因此您需要包含此额外的Maven依赖项才能在项目中使用Mapper:


com.datastax.cassandra
cassandra-driver-mapping
3.0.0

例如,让我们创建并注释与我们的hotels表对应的Hotel域模型类:

import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
@Table(keyspace = "hotel", name = "hotels")
public class Hotel {
                       @PartitionKey
                       private String id;
                       @Column (name = "name")
                       private String name;
                        @Column (name = "phone")
                       private String phone;
                       @Column (name = "address")
                       private String address;
                       @Column (name = "pois")
                       private Set pointsOfInterest;
                       // constructors, get/set methods, hashcode, equals
}

现在我们使用com.datastax.driver.mapping.MappingManager附加到Session并为我们的带注释的域模型类创建一个Mapper:

MappingManager mappingManager = new MappingManager(session);
Mapper hotelMapper = MappingManager.mapper(Hotel.class);

我们假设Hotel类有一个简单的构造函数,只需要一个UUID,name和phone number,我们将使用它来创建一个我们可以使用对象映射器保存的简单hotel:

Hotel hotel = new Hotel("AZ123", "Super Hotel at WestWorld”,"1-888-999-9999");
hotelMapper.save(hotel);

为了达到执行CQL INSERT或UPDATE同样的效果,我们只需要执行Mapper.save() 操作,这些对Cassandra来说是完全相同的操作。
Mapper代表我们构建并执行语句。
要检索对象,我们使用Mapper.get()操作,传入一个与分区键元素匹配的参数列表:

Hotel retrievedHotel = hotelMapper.get(hotelId);

删除对象的语法类似:

hotelMapper.delete(hotelId);

与save()操作一样,get()和delete()完全帮我们处理好了驱动程序执行语句的细节。
还有saveAsync(),getAsync()和deleteAsync()操作,它们使用我们前面讨论过的ListenableFuture接口支持异步执行。
如果你希望能够在执行查询之前配置查询,Mapper上有返回语句的操作:saveQuery(),getQuery()和deleteQuery()。
对象映射器是一个有用的工具,用于抽象与代码交互的一些细节,特别是如果您有现有的域模型。
如果您的域模型包含引用其他类的类,则可以使用@UDT注释将引用的类注释为用户定义的类型。
对象映射器使用带注释的类型递归处理对象。

Achilles:高级对象映射器 DuyHai Doan为Java开发了一个高级对象映射器叫Achilles。 Achilles为更高级的功能提供支持,比如复杂的键映射,轻量级事务,用户定义的函数等。您可以访问https://github.com/doanduyhai/Achilles查看。

Policies

策略
Java驱动程序提供了几个策略接口,可用于调整驱动程序的行为。
其中包括负载平衡,重试请求和群集中节点的连接管理的策略。

Load balancing policy

负载均衡策略
正如我们在第6章中了解到的,可以对集群中的任何节点进行查询,然后将其称为该查询的协调器节点(coordinator node)。
根据查询的内容,协调器可以与其他节点通信以满足查询。
如果客户端要在同一节点上管理其所有查询,则会在群集上产生不平衡负载,尤其是在其他客户端执行相同操作的情况下。
为了解决这个问题,驱动程序提供了一个可插拔的机制来平衡多个节点之间的查询负载。
通过com.datastax.driver.core.policies.LoadBalancing策略接口的选择来实现负载平衡。

每个LoadBalancingPolicy必须提供distance()操作,以根据HostDistance枚举将群集中的每个节点分类为本地,远程或忽略。
驱动程序更喜欢与本地节点的交互,并且相对远程节点,保持与本地节点更多的连接。
另一个关键操作是newQueryPlan(),它按照应该查询的顺序返回节点列表。
LoadBalancingPolicy接口还包含用于在添加或删除节点、节点在上线或下线时通知策略的操作。
这些操作有助于策略避免在查询计划中包含关闭或删除的节点。

该驱动程序提供了两种基本的负载平衡实现:RoundRobin Policy(默认)和DCAwareRoundRobinPolicy。
RoundRobinPolicy以重复模式跨集群中的节点分配请求以分散处理负载。
DCAwareRoundRobinPolicy类似,但将其查询计划集中在本地数据中心的节点上。
此策略可以在远程数据中心中添加可配置数量的节点来查询计划,但远程节点将始终优先于本地节点。
可以显式标识本地数据中心,也可以允许驱动程序自动发现它。
第二种模式是令牌感知,其使用分区密钥的令牌值以选择作为所需数据的副本的节点,从而最小化必须查询的节点的数量。
这是通过使用TokenAwarePolicy包装所选策略来实现的。
LoadBalancingPolicy在构建时在群集上设置。
例如,以下语句将初始化群集以具有令牌感知并优先选择本地数据中心中的节点:

Cluster.builder().withLoadBalancingPolicy(new TokenAwarePolicy(new DCAwareRoundRobinPolicy.Builder().build());
Retry policy
重试策略

当Cassandra节点发生故障或无法访问时,驱动程序会自动并透明地尝试其他节点并在后台安排重新连接到死亡(dead)节点。
由于网络条件的临时更改也会使节点显示为脱机,因此驱动程序还提供了一种机制来重试因网络相关错误而失败的查询。
这消除了在客户端代码中编写重试逻辑的需要。
驱动程序根据com.datastax.driver.core.RetryPolicy接口提供的实现重试失败的查询。
onReadTimeout(),onWrite Timeout()和onUnavailable()操作分别定义了与网络相关的异常ReadTimeoutException,WriteTimeoutException或UnavailableException而导致查询失败时应采取的行为。

DataStax Java驱动程序异常
可以由Java驱动程序生成的各种异常和错误收集在com.datastax.driver.core.excep tions包中。
RetryPolicy操作返回RetryDecision,它指示是否应该重试查询,如果应该,则指示在什么一致性级别。
如果未重试异常,则可以重新抛出或忽略该异常,在这种情况下,查询操作将返回一个空的ResultSet。
Java驱动程序提供了几个RetryPolicy实现:
•DefaultRetryPolicy是一种保守的实现,仅在有限的几种情况下重试查询。
•FallthroughRetryPolicy从不重试查询,始终重新抛出异常。
•DowngradingConsistencyRetryPolicy是一种更积极的策略,它会降低所需的一致性级别,以尝试使查询成功。

关于DowngradingConsistencyRetryPolicy的一个词
此策略附带警告:
如果您愿意在某些情况下接受降级的一致性级别,那么您是否真的需要更高的一致性级别的一致性级别?
可以在构建时在集群上设置RetryPolicy,如以下语句所示,该语句选择DowngradingConsistencyRetryPolicy并使用LoggingRetryPolicy对其进行包装,以便记录每次重试尝试:

Cluster.builder()。withRetryPolicy(new >LoggingRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE));

除非通过Statement.setRetryPolicy()操作覆盖任何单个查询,否则群集上的RetryPolicy将用于在该群集上执行的所有查询。

推测执行策略
虽然拥有一个自动响应网络超时的重试机制非常棒,但我们通常无法等待超时甚至长时间的垃圾收集暂停。
为了加快速度,驱动程序提供了一种推测执行机制。
如果查询的原始协调器节点未能在预定间隔内响应,则驱动程序抢先地针对不同的协调器节点另外开始执行查询。
当其中一个查询有返回时,驱动程序提供该响应并取消任何其他未完成的查询。
通过指定com.datastax.driver.core.policies.SpeculativeExecutionPolicy的实现,在集群上设置推测执行行为。
默认值为NoSpeculativeExecutionPolicy,它不会安排任何推测性执行。
还有一个ConstantSpeculativeExecutionPolicy,它以最多的固定延迟毫秒数计划最多重试次数。 PercentileSpeculativeExecutionPolicy是一个在3.0驱动程序版本中还被认为是Beta版的更新的策略。
它会根据观察到的原始协调节点的延迟,在延迟时触发推测性执行。
此策略使用Cluster.Builder来设置,例如:

Cluster.builder().withSpeculativeExecutionPolicy(
new ConstantSpeculativeExecutionPolicy (
200, // delay in ms 延迟的毫秒数
3 // max number of speculative executions 最大推测执行数
);

以后不能更改此策略,也不能在单个语句上覆盖此策略。

地址转换器

在我们到目前为止看到的示例中,每个节点都由在其cassandra.yaml文件中为节点配置的rpc_address的IP地址标识。
在某些部署中,客户端可能无法访问该地址。
为了处理这种情况,驱动程序提供了一个可插入的功能,可以通过com.datastax.driver.core.policies.AddressTranslator接口转换地址(在3.0之前的驱动程序版本中,“translator”在API中被拼错为“translater”)。
例如,Java驱动程序附带IdentityTranslator,一个保持IP地址不变的默认转换器,以及EC2MultiRegionAddressTranslator,它对Amazon EC2环境很有用。
在客户端可能需要通过公共IP地址访问另一个数据中心中的节点的情况下,此转换器非常有用。
我们将在第14章中更详细地讨论EC2部署。

元数据

要访问集群元数据,我们调用Cluster.getMetadata()方法。
com.datastax.driver.core.Metadata类提供有关群集的信息,包括群集名称,包含键空间和表的模式以及群集中的已知主机。
我们可以通过以下代码获取集群的名称:

Metadata metadata = cluster.getMetadata();
System.out.printf("Connected to cluster: %s\n",metadata.getClusterName(), cluster.getClusterName());

指定群集名称
有点令人困惑的是,Cluster.Builder类允许我们在构建Cluster实例时为其分配名称。
此名称实际上只是客户端跟踪多个Cluster对象的一种方式,可以与实际Cassandra集群中的节点所知的名称不同。
第二个集群name是我们通过Metadata类获得的名称。
如果我们在构造时没有为Cluster指定名称,则会为其分配一个默认名称,例如“cluster1”,“cluster2”等等(如果创建了多个集群)。
如果修改之前的示例,将metadata.getClusterName()更改为cluster.getClusterName(),则可以看到此值。

发现节点
Cluster对象维护与其中一个联系点的永久连接,它用于维护有关群集状态和拓扑的信息。
使用此连接,驱动程序将发现当前群集中的所有节点。
驱动程序使用com.datastax.driver.core.Host类来表示每个节点。
以下代码显示了迭代主机以打印其信息的示例:

for (Host host : cluster.getMetadata.getAllHosts())
{
System.out.printf("Data Center: %s; Rack: %s; Host: %s\n",
host.getDatacenter(), host.getRack(), host.getAddress());
}

你可以在com.cassandraguide.clients.SimpleConnection示例中找到此代码。
如果我们使用Cassandra Cluster Manager(ccm)运行多节点集群(例如我们在第7章中创建的集群),则此程序的输出将如下所示:

Connected to cluster: my_cluster
Data Center: datacenter1; Rack: rack1; Host: /127.0.0.1
Data Center: datacenter1; Rack: rack1; Host: /127.0.0.2
Data Center: datacenter1; Rack: rack1; Host: /127.0.0.3

使用该连接,驱动程序还可以发现当前群集中的所有节点。
驱动程序还可以检测到新添加到群集的节点。
你可以通过实现Host.StateListener接口来注册侦听器来实现这个功能。
这需要我们实现几个操作,例如onAdd()和onRemove(),它们在从集群添加或删除节点时被调用,以及onUp()和onDown(),它们指示节点何时上线或下线。
让我们看一下在集群中注册监听器的示例类的一部分:

public class ConnectionListenerExample implements Host.StateListener {
    public String getHostString(Host host) {
        return new StringBuilder("Data Center: " + host.getDatacenter() +
        " Rack: " + host.getRack() +
        " Host: " + host.getAddress().toString() +
        " Version: " + host.getCassandraVersion() +
        " State: " + host.getState());
   }
   public void onUp(Host host) {
      System.out.printf("Node is up: %s\n", getHostString(host));
   }
   public void onDown(Host host) {
      System.out.printf("Node is down: %s\n", getHostString(host));
   }
   // other required methods omitted...
   public static void main(String[] args) {
      List list =
      ArrayList();
      list.add(new ConnectionListenerExample());
      Cluster cluster = Cluster.builder().
      addContactPoint("127.0.0.1").
      withInitialListeners(list).
      build();
      cluster.init();
   }
}

此代码只是在节点上线或下线时打印出状态消息。
你可以注意到,我们使用了比前一个示例更多的有关每个节点的信息,包括每个节点使用的Cassandra版本。
你可以在com.cassandraguide.clients.ConnectionListener类示例中找到完整的代码清单。
我们来运行这个示例程序。
因为在调用init()之前添加了我们的监听器,所以我们立即得到以下输出:

Node added: Data Center: datacenter1 Rack: rack1 Host: /127.0.0.1 Version: 3.0.0 State: UP
Node added: Data Center: datacenter1 Rack: rack1 Host: /127.0.0.2 Version: 3.0.0 State: UP
Node added: Data Center: datacenter1 Rack: rack1 Host: /127.0.0.3 Version: 3.0.0 State: UP

现在让我们使用ccm stop命令关闭我们的一个节点,我们将看到如下内容:

Node is down: Data Center: datacenter1 Rack: rack1 Host: /127.0.0.1 Version: 3.0.0 State: DOWN

同样,如果我们重新启动节点,我们会看到节点重新联机的通知:

Node is up: Data Center: datacenter1 Rack: rack1 Host: /127.0.0.1 Version: 3.0.0 State: UP
Schema access 架构访问

Metadata类还允许客户端了解群集中的架构。
exportSchemaAsString() 操作创建一个String,描述集群中定义的所有键空间和表,包括system键空间。
此输出等同于cqlsh命令DESCRIBE FULL SCHEMA。
其他操作支持浏览各个键空间和表的内容。
我们之前已经在第2章讨论了Cassandra对最终一致性的支持。
因为schema信息本身是使用Cassandra存储的,所以它也是最终一致的,因此不同节点可能有不同版本的schema。
从3.0版本开始,Java驱动程序不会直接公开schema版本,但您可以通过运行nodetool describecluster命令来查看,如下示例:

$ ccm node1 nodetool describecluster
Cluster Information:
    Name: test_cluster
    Snitch: org.apache.cassandra.locator.DynamicEndpointSnitch
    Partitioner: org.apache.cassandra.dht.Murmur3Partitioner
    Schema versions:
        ea46580a-4ab4-3e70-b68f-5e57da189ac5:
            [127.0.0.1, 127.0.0.2, 127.0.0.3]

输出向我们展示了一些信息。
首先,我们看到schema版本是一个UUID值。
该值是基于节点知道的所有键空间和表定义进行散列计算得到的。
所有三个节点共享相同的schema版本的事实意味着它们都定义有相同的schema。
当然,在创建,更改和删除键空间和表时,schema的版本会随着时间的推移而发生变化。
驱动程序通过向Cluster 注册一个com.datastax.driver.core.Schema ChangeListener,为客户端提供通知机制以了解这些更改。
你可以通过运行com.cassandra guide.clients.SimpleSchemaExample示例找到这些调用的示例。
除了我们刚刚在Metadata类中检查的schema access模式访问之外,Java驱动程序还提供了一个用于在com.datastax.driver.core.schemabuilder包中管理schema的工具。
SchemaBuilder提供了一种流式的API,用于创建SchemaStatements,表示对键空间,表,索引和用户定义类型(UDT)的CREATE,ALTER和DROP操作等操作。
例如,以下代码可用于创建hotels keyspace:

SchemaStatement hotelSchemaStatement = SchemaBuilder.createTable("hotels").
    addPartitionKey("id", DataType.text()).
   addColumn("name", DataType.text()).
   addColumn("phone", DataType.text()).
   addColumn("address", DataType.text()).
   addColumn("pois", DataType.set(DataType.text()));
   session.execute(hotelSchemaStatement);

我们还导入com.datastax.driver.core.DataType,以便我们可以利用其静态操作来定义每列的数据类型。

【使用编程定义schema时避免冲突】 许多开发人员已经注意到这种编程schema管理功能可以用作简化应用程序部署的“延迟初始化”技术:如果我们的应用程序使用的schema不存在,我们可以简单地以编程方式创建它。 但是,在运行多个客户端时,建议不要使用此技术,即使使用IF NOT EXISTS语义也是如此。 来自多个并发客户端的CREATE TABLE或ALTER TABLE语句可能导致节点之间的状态不一致,需要手动修复。
调试和监控

驱动程序提供了功能来用于监视和调试客户端对Cassandra的使用,包括用于记录和度量的工具。
还有一个查询跟踪功能,我们将在第12章中学习。
记录
正如我们将在第10章中学到的,Cassandra使用了一个名为Simple Logging Facade for Java的日志API(SLF4J)。
Java驱动程序也使用SLF4J API。
为了在Java客户端应用程序上启用日志记录,您需要在类路径上提供兼容的SLF4J实现。
下面是我们可以添加到Maven POM文件中的依赖示例,以选择Logback项目作为实现:


ch.qos.logback
logback-classic
1.1.3

您可以在http://logback.qos.ch/上了解有关Logback的更多信息。
默认情况下,Java驱动程序设置为使用DEBUG日志记录级别,这非常详细。
我们可以通过利用Logback的配置机制来配置日志记录,该机制支持测试和生产环境的分别单独配置。
Logback首先检查表示测试配置的文件logback-test.xml的类路径,然后如果没有找到测试配置,它将搜索文件log-back.xml。
有关Logback配置的更多详细信息,包括测试和生产环境的配置文件的示例,请参阅配置页。

度量
有时,监视客户端应用程序的行为会有所帮助,以便检测异常情况和调试错误。
Java驱动程序收集有关其活动的指标,并使用Dropwizard Metrics库使这些指标可用。
驱动程序报告连接、任务队列、查询和错误(如连接错误、读取和写入超时、重试和推测执行)的度量信息。
您可以通过Cluster.getMetrics() 操作在本地访问Java驱动程序度量指标。
Metrics库还与Java Management Extensions(JMX)集成,以允许远程监控metrics指标。
默认情况下启用JMX报告,但可以在构建Cluster时提供的配置中覆盖。

你可能感兴趣的:(2019-02-21 Cassandra权威指南(2版)8章 【译】)