提升自己
VMware 提供培训和认证,助您加速进步。
了解更多本篇博客文章解释了 Spring Data Lovelace 在 Apache Cassandra 和 Redis 方面的新特性和重要改进。请务必查看关于 Spring Data Lovelace 在 MongoDB 方面有哪些新特性? 的博客文章。
Spring Data Lovelace 的通用版本已于上周发布,现在是时候简要介绍我们新增的功能了。这个版本列车功能丰富。
在本篇博客文章中,我将介绍 Apache Cassandra 和 Redis。
在此版本中,我们使用特定于 Cassandra 的类型改进了数据访问,引入了生命周期事件支持,改善了 Java 和 Kotlin 的编程体验,并包含了各种其他优化。让我们看看此版本如何帮助改进您对 Cassandra 的数据访问。
Map 和 Tuple 数据类型是 Cassandra 中的特定类型,允许在单个列中存储多个值。以前,我们在映射实体中以原始形式支持这两种类型,这意味着您只能使用原始键和值的 Map。对于 Tuple,您只能使用原始的 Cassandra 驱动程序类型 TupleValue
,而没有进一步的映射甚至模式支持。
在此版本中,我们为 Map 和 Tuple 类型的属性添加了映射和转换支持。Map 现在可以包含非原始键和值,并且转换层会应用可能已注册的转换器。
考虑以下类型
@UserDefinedType
class Manufacturer {
String name;
// getters/setters omitted
}
@Table
class Supplier {
Map<Manufacturer, List<String>> acceptedCurrencies;
// getters/setters omitted
}
Manufacturer
是一种映射的用户定义类型,用作 Map 的键。值表示为字符串的 List
。我们现在可以重构代码,在列表中使用合适的 Currency
类型(例如 java.util.Currency
)。为此,我们提供了 String
和 Currency
之间的转换器,并通过 CassandraCustomConversions
进行注册。以下示例展示了如何执行此操作
enum StringToCurrencyConverter implements Converter<String, Currency> {
INSTANCE;
@Override
public Currency convert(String source) {
return Currency.getInstance(source);
}
}
enum CurrencyToStringConverter implements Converter<Currency, String> {
INSTANCE;
@Override
public String convert(Currency source) {
return source.getCurrencyCode();
}
}
@Configuration
class MyCassandraConfiguration {
public CassandraCustomConversions cassandraCustomConversions() {
return new CassandraCustomConversions(
Arrays.asList(StringToCurrencyConverter.INSTANCE, CurrencyToStringConverter.INSTANCE));
}
}
注册转换器后,我们可以继续在 Supplier
类型中使用 Currency
来处理值对象而不是原始类型,如下面的示例所示
@Table
class Supplier {
Map<Manufacturer, List<Currency>> acceptedCurrencies;
// getters/setters omitted
}
在以前版本的 Spring Data for Apache Cassandra 中,Tuple 实际上无法使用。使用 Tuple 需要直接与 Row
交互并检索 TupleType
来创建适当的 Tuple 值。因此,我们决定提供映射的 Tuple 类型,如下面的示例所示
@Table
class Supplier {
List<Dependance> dependances;
// getters/setters omitted
}
@Tuple
class Dependance {
@Element(0) String address;
@Element(1) String city;
@Element(2) Currency currency;
// getters/setters omitted
}
映射的 Tuple 使用 @Tuple
进行注解,Tuple 的各个组件通过使用 @Element(…)
引用它们在 Tuple 中的序数索引。转换器检查加载的 Tuple,并将它们映射到作为您的领域模型一部分的常规 Java 类。您不再需要直接与 TupleType
和 TupleValue
交互(尽管您仍然可以),但您可以用类型安全的方法表示 Tuple 值。映射的 Tuple 受益于转换器的各种映射功能,并且可以引用已注册自定义转换器的类型。
对 Map 和 Tuple 的支持还包括模式生成,可以通过从您的领域模型派生类型来快速设置模式。
有关更多详细信息,请参阅我们的 映射 Tuple 示例。
Cassandra 映射框架现在包含多个 org.springframework.context.ApplicationEvent
事件,您的应用程序可以通过在 ApplicationContext
中注册特殊 bean 来响应这些事件。要在对象通过转换过程(将您的领域对象转换为 Statement
)之前拦截该对象,您可以注册 AbstractCassandraEventListener
的子类,并重写 onBeforeSave
方法。当事件被分发时,您的监听器将被调用,并在对象进入转换器之前传递领域对象。以下示例展示了如何使用 onBeforeSave
public class BeforeConvertListener extends AbstractCassandraEventListener<Person> {
@Override
public void onBeforeSave(BeforeSaveEvent<Person> event) {
// does some auditing manipulation, set timestamps, whatever
}
}
在您的 Spring ApplicationContext 中声明这些 bean 会使得它们在事件分发时被调用。
AbstractCassandraEventListener
中存在以下回调方法
onBeforeSave
: 在 CassandraTemplate
的 save
操作中调用,在将行插入或保存到数据库之前。
onAfterSave
: 在 CassandraTemplate
的 save
操作中调用,在将行插入或保存到数据库之后。
onBeforeDelete
: 在 CassandraTemplate
的 delete
操作中调用,在从数据库删除行之前。
onAfterDelete
: 在 CassandraTemplate
的 delete
操作中调用,在从数据库删除行之后。
onAfterLoad
: 在 CassandraTemplate
的 select
和 selectOne
方法中调用,在从数据库检索到行之后。
onAfterConvert
: 在 CassandraTemplate
的 select
和 selectOne
方法中调用,在从数据库检索到的行被转换为 POJO 之后。
生命周期事件仅针对根级类型发布。作为实体根内部属性使用的复杂类型不受事件发布的约束。
请参阅我们的 生命周期事件示例。
Spring Data 公开了接受目标类型的方法,用于查询或将结果值投影到该类型。Kotlin 使用自己的类型 (KClass
) 表示类,这在尝试获取 Java Class 类型时可能是一个障碍。
Spring Data for Apache Cassandra 附带了扩展,这些扩展为接受类型参数的方法添加了重载,可以通过使用泛型或直接接受 KClass 来实现,如下面的示例所示
operations.getTableName<Person>()
operations.getTableName(Person::class)
operations.find<Person>().as<Contact>
.matching(query(where("firstname").isEqualTo("luke"))).all();
有关更多详细信息,请参阅我们的 Cassandra Kotlin 用法 示例。
CassandraOperations
接口是与 Apache Cassandra 进行更底层交互的核心组件之一。它提供了广泛的方法,涵盖了从批处理、结果流到 CRUD 操作的需求。每个方法都有多个重载。其中大多数涵盖了 API 的可选或备用部分,例如按 CQL、Statement
或 Query
进行查询。
FluentCassandraOperations
为 CassandraOperations
的常用方法提供了一个更精简的接口,并提供了一个更具可读性的流式 API。入口点(insert(…)
、query(…)
、update(…)
等)遵循基于要运行操作的自然命名方案。从入口点开始,API 被设计为仅提供依赖于上下文的方法,这些方法最终会调用实际的 Cassandra
对应方法。
考虑一个查询示例
List<Person> all = operations.query(Person.class)
.inTable("people")
.all();
此查询查询 people
表中的所有行,并将结果映射到 Person
类型。省略 inTable(…)
会从实体类型派生表名。
下一个示例使用投影和查询
List<Contact> all = operations.query(Person.class)
.as(Contact.class)
.matching(query(where("firstname").is("luke")))
.all();
此查询使用 Person
类型映射到的表,并将结果(DTO 或接口投影)投影到 Contact
。查询本身是使用 Person
类型中的字段名映射的。您可以通过终止方法:first()
、one()
、all()
或 stream()
在检索单个实体和将多个对象作为 List
或 Stream
检索之间切换。
流式 API 是类型安全的,并且中间对象是不可变的。您可以准备查询的基础部分,然后继续进行更具体的执行,如下面的示例所示
TerminatingSelect<Contact> select = operations.query(Person.class)
.as(Contact.class)
.matching(query(where("firstname").is("luke")))
Contact contact = select.first();
long count = select.count();
有关更多详细信息,请参阅我们的 Kotlin 示例。
Spring Data for Apache Cassandra 模块中还包含其他几项增强功能,请务必查看参考文档中的 新特性 部分,以了解有关响应式分片查询 (reactive slice queries) 和存在/计数投影 (exists/count projections) 的更多信息。
Spring Data Redis 的此版本包含了 2.0 版本中未能涵盖的各种主题的改进。其中大部分优化了 Redis 集群使用中的一些问题。核心主题包括:
连接改进
Redis 集群使用的优化
框架的各种改进
Redis 支持多种操作模式:Standalone、带复制的 Standalone、带或不带复制的 Redis Sentinel、Redis Cluster。我们已经涵盖了 Standalone、Redis Sentinel 和 Redis Cluster 模式。迄今为止缺失的部分是从副本读取。此版本引入了对各种 Redis 操作模式下副本读取的支持。以下示例展示了如何使用此新功能
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.NEAREST)
.build();
RedisSentinelConfiguration endpoint = new RedisSentinelConfiguration()
.master("my-master")
.sentinel("sentinel-host1", 26379)
.sentinel("sentinel-host2", 26379);
LettuceConnectionFactory factory = new LettuceConnectionFactory(endpoint, clientConfiguration);
指定 ReadFrom
允许您在发出只读命令(例如 GET
或 SMEMBERS
)时选择特定的节点类型。您可以使用 Lettuce 的预定义设置之一,或创建新的 ReadFrom
策略。在所有可用副本的设置中都会考虑 ReadFrom
:Redis Sentinel、Redis Cluster 以及静态主/副本设置(例如 AWS ElastiCache),这引出了下一个改进。
您可以将 AWS ElastiCache 或任何其他静态主/副本设置(即使用带有一个或多个专用副本的 Redis)与 Spring Data Redis 和 Lettuce 一起使用,以从副本节点读取数据。在以前的版本中,您只能使用主节点。请看以下配置代码片段
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder().readFrom(ReadFrom.NEAREST).build();
RedisStaticMasterSlaveConfiguration endpoint = new RedisStaticMasterSlaveConfiguration("my-master-host", 6379)
.node("my-replica-host1", 6379)
.node("my-replica-host2", 6379);
LettuceConnectionFactory factory = new LettuceConnectionFactory(endpoint, clientConfiguration);
在此代码中,我们将 LettuceConnectionFactory
配置为使用多个节点,而无需实际指定其角色。Lettuce 本身会确定各个主机的作用,并根据其角色使用节点。
此类别中的最后一个优化是使用 Unix 域套接字建立本地连接。Unix 域套接字或 IPC(进程间通信)套接字是一种数据通信端点,用于在同一主机操作系统上运行的进程之间交换数据。与命名管道类似,Unix 域套接字支持传输与 TCP 相当的可靠字节流。由于 Unix 域套接字通信仅发生在内核内部,因此通信绕过了网络层,通常具有改进的性能。
要使用 Unix 域套接字,您需要使用 Lettuce 并为 Netty 添加原生扩展(在 Linux 上运行时使用 netty-transport-native-epoll
,在 MacOS 上运行时使用 netty-transport-native-kqueue
)。以下示例配置了通过套接字与 Redis 进行通信
RedisSocketConfiguration endpoint = new RedisSocketConfiguration("/var/run/redis");
LettuceConnectionFactory factory = new LettuceConnectionFactory(endpoint);
此版本对使用 Lettuce 驱动程序的 Redis Cluster 连接的处理进行了优化。以前的版本不共享底层 Lettuce 到 Redis Cluster 的连接,这表现为性能下降,因为新连接总是建立新的集群连接。这种行为在发出多个命令时会产生影响,因为每个命令基本上都使用一个新的 RedisConnection
。
默认情况下,现在为 Redis Cluster 连接启用了原生连接共享。其他使用模式(例如 Redis Standalone)在以前的版本中已经使用了连接共享。以下示例展示了如何使用共享原生连接创建 LettuceConnectionFactory
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(…);
LettuceConnectionFactory factory = new LettuceConnectionFactory(clusterConfiguration);
factory.setShareNativeConnection(true);
某些操作(例如阻塞操作)需要专用的连接,以免影响在同一原生连接上运行的其他进程。如果您的应用程序高度依赖于阻塞的 Redis 命令,您可以为 Redis Cluster 连接启用连接池,以缓冲连接创建。启用连接池是客户端配置的一个方面。启用连接池后,LettuceConnectionFactory
会将连接池应用于配置的 Redis 使用方案。您可以使用 LettucePoolingClientConfiguration
作为入口点来启用连接池,如下面的示例所示
LettucePoolingClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(…).build();
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(…);
LettuceConnectionFactory factory = new LettuceConnectionFactory(clusterConfiguration, clientConfiguration);
随着 ReadFrom
设置的引入和简化的集群连接处理,我们现在可以通过使用 SCAN
命令支持集群范围的键空间扫描 (keyspace scanning)。在幕后,驱动程序维护一个有状态的光标,允许您遍历集群中持有键的所有主/副本节点。使用连接的 scan(…)
方法为您提供了与在 Redis Standalone 设置上使用时相同的体验,如下面的示例所示
Cursor<byte[]> scan = clusterConnection.keyCommands()
.scan(ScanOptions.scanOptions().match("foo*").build());
scan.forEachRemaining(key -> …);
键空间扫描还为所有 Redis 操作模式提供了响应式变体。在响应式 Redis Template API 上调用 scan(…)
会返回一个键的 Flux
。结果 Flux
具备背压感知能力,并在有足够的请求来扫描整个键空间时将需求转换为 SCAN
调用。如果需求得到满足,它将停止扫描。以下示例构造了这样的 Flux
Flux<String> scan = redisTemplate.scan(ScanOptions.scanOptions().match("something*").build());
此版本附带了 Redis repository 的 Query by Example 支持。Query by Example 是一种用户友好的查询技术,接口简单。它允许动态创建查询,并且不需要您编写包含字段名的查询。Query by Example 的性质不需要查询语言,因为实际查询是从 Example
对象派生的。您现在可以定义一个 Example
来查询存储在 Redis hash 中的索引值。Redis repository 可以实现 QueryByExampleExecutor
片段来继承 Query by Example 方法。请看以下代码片段
interface PersonRepository extends CrudRepository<Person, String>, QueryByExampleExecutor<Person> {
}
PersonRepository repository = …;
Person eddard = new Person("eddard", "stark");
Person tyrion = new Person("tyrion", "lannister");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
Person arya = new Person("arya", "stark");
repository.saveAll(Arrays.asList(eddard, tyrion, robb, jon, arya));
List<Person> result = repository.findAll(Example.of(new Person(null, "stark")));
此代码插入了一堆 Person
对象。Example
对象定义了一个探测器 (probe),只设置了姓氏。查询引擎会创建一个查询,默认只包含非空字段,查询 lastname
为 stark
的对象。
有关更多详细信息,请参阅 Query-by-Example 示例。
Redis repository 现在支持类型别名,您可以通过使用 @TypeAlias
注解您的领域类来使用。默认情况下,Redis 中的类型提示使用完全限定类名。您可以应用别名来定制类型名称并减少 Redis 内存使用。
以下示例持久化了一个 Person
实例
package com.acme;
@TypeAlias("person")
class Person {
// …
}
此代码使得类型提示 (person
) 被使用,而不是 com.acme.Person
。用于在 Redis 中存储此实体的相应命令如下
HMSET "person:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9"
Redis 模块中还包含其他几项增强功能,请务必查看参考文档中的 新特性 部分,以了解有关键空间扫描 (keyspace scanning)、响应式发布/订阅 (reactive Pub/Sub) 和新命令的更多信息。