Spring Data(数据)Redis

时间:2022-11-22 12:01:11

版本 3.0.0

Spring Data Redis 项目通过使用键值样式数据存储将核心 Spring 概念应用于解决方案的开发。 我们提供了一个“模板”作为发送和接收消息的高级抽象。 您可能会注意到与 Spring 框架中的 JDBC 支持有相似之处。

本节提供了一个易于遵循的指南,用于开始使用 Spring Data Redis 模块。

Spring Data(数据)Redis

1. 学习之春

Spring Data 使用 Spring 框架的核心功能,包括:

  • 国际奥委会容器
  • 型式转换系统
  • 表达式语言
  • JMX 集成
  • DAO 异常层次结构。

虽然你不需要知道Spring API,但理解它们背后的概念是很重要的。 至少,控制反转 (IoC) 背后的想法应该很熟悉,并且您应该熟悉您选择使用的任何 IoC 容器。

Redis 支持的核心功能可以直接使用,无需调用 Spring 容器的 IoC 服务。 这很像,它可以“独立”使用,而无需 Spring 容器的任何其他服务。 要利用 Spring Data Redis 的所有功能,例如存储库支持,您需要配置库的某些部分以使用 Spring。​​JdbcTemplate​

要了解有关 Spring 的更多信息,您可以参考详细解释 Spring 框架的综合文档。 有很多关于这个主题的文章、博客条目和书籍。 有关更多信息,请参阅 Spring 框架主页。

一般来说,这应该是想要尝试Spring Data Redis的开发人员的起点。

2. 学习 NoSQL 和键值存储

NoSQL存储已经席卷了存储世界。 这是一个广阔的领域,拥有大量的解决方案、术语和模式(更糟糕的是,甚至术语本身也有多重含义)。 虽然有些原则很常见,但您必须在一定程度上熟悉 SDR 支持的商店。熟悉这些解决方案的最佳方法是阅读其文档并遵循其示例。 通常不需要超过五到十分钟来完成它们,如果您来自仅 RDMBS 的背景,很多时候这些练习可能会让人大开眼界。

2.1. 试用示例

人们可以在专用的 Spring 数据示例存储库中找到键值存储的各种示例,https://github.com/spring-projects/spring-data-keyvalue-examples。 对于Spring Data Redis,您应该特别注意样本,这是一个建立在Redis之上的Twitter克隆,可以在本地运行或部署到云中。 有关详细信息,请参阅其文档,以下博客条目。retwisj

3. 要求

Spring Data Redis 二进制文件需要 JDK 级别 17 及更高版本和Spring Framework6.0.0 及更高版本。

在键值存储方面,需要 Redis2.6.x 或更高版本。 Spring Data Redis 目前正在针对最新的 6.0 版本进行测试。

4. 其他帮助资源

学习一个新框架并不总是那么简单。 在本节中,我们尝试提供我们认为易于遵循的指南,以便从 Spring Data Redis 模块开始。 但是,如果您遇到问题或需要建议,请随时使用以下链接之一:

社区论坛

Stack Overflow上的Spring Data是所有Spring Data(不仅仅是文档)用户共享信息和互相帮助的标签。 请注意,只有发布时才需要注册。

专业支持

专业的,从源头支持,保证响应时间,可从Spring Data和Spring背后的公司Pivotal Sofware,Inc.获得。

5. 后续开发

有关 Spring 数据源存储库、夜间构建和快照工件的信息,请参阅 Spring数据主页。

您可以通过在spring-data或spring-data-redis 上与 Stack Overflow 上的开发人员进行交互,帮助 Spring Data 最好地满足 Spring 社区的需求。

如果您遇到错误或想要提出改进建议(包括本文档),请在Github 上创建工单。

要了解Spring生态系统中的最新消息和公告,请订阅Spring社区门户。

最后,您可以在Twitter上关注Spring博客或项目团队(@SpringData)。

6. 依赖关系

由于各个 Spring 数据模块的开始日期不同,因此它们中的大多数都带有不同的主要和次要版本号。找到兼容版本的最简单方法是依靠我们随附的与定义的兼容版本一起提供的春季数据发布列车 BOM。在 Maven 项目中,您将在 POM 的部分中声明此依赖项,如下所示:​​<dependencyManagement />​

例 1.使用弹簧数据发布列车物料清单

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2022.0.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

当前发布训练版本是。火车版本使用带有图案的犊牛。 对于 GA 版本和服务版本,版本名称如下,对于所有其他版本,版本名称如下:,其中可以是以下之一:​​2022.0.0​​​​YYYY.MINOR.MICRO​​​​${calver}​​​​${calver}-${modifier}​​​​modifier​

  • ​SNAPSHOT​​:当前快照
  • ​M1​​,,等等:里程碑M2
  • ​RC1​​,,等等:发布候选版本RC2

您可以在我们的Spring 数据示例存储库中找到使用 BOM 的工作示例。有了这个,你可以声明你想要使用的 Spring 数据模块,而无需在块中有一个版本,如下所示:​​<dependencies />​

例 2.声明对 Spring 数据模块的依赖关系

<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>

6.1. 使用 Spring 引导进行依赖管理

Spring Boot 会为你选择最新版本的 Spring 数据模块。如果仍要升级到较新版本,请将 要使用的训练版本和迭代的属性。​​spring-data-releasetrain.version​

6.2. 弹簧框架

Spring 数据模块的当前版本需要 Spring Framework 6.0.0 或更高版本。这些模块还可以使用该次要版本的较旧错误修复版本。但是,强烈建议使用该代中的最新版本。

Spring Data(数据)Redis

7. 简介

7.1. 文档结构

参考文档的这一部分解释了Spring Data Redis提供的核心功能。 它解释了键值模块的概念和语义以及各种存储命名空间的语法。 有关键值存储、Spring 或 Spring 数据示例的介绍,请参阅学习 NoSQL 和键值存储。 本文档仅涉及 Spring Data Redis 支持,并假设用户熟悉键值存储和 Spring 概念。

“Redis 支持”引入了 Redis 模块功能集。

“Redis 存储库”介绍了对 Redis 的存储库支持。

本文档是 Spring 数据 Redis (SDR) 支持的参考指南。

8. 升级弹簧数据

有关如何从早期版本的 Spring 数据升级的说明在项目wiki 上提供。 按照发行说明部分中的链接查找要升级到的版本。

升级说明始终是发行说明中的第一项。如果您落后多个版本,请确保您还查看了您跳转的版本的发行说明。

8.1. 接下来要读什么

决定升级应用程序后,可以在文档的其余部分找到有关特定功能的详细信息。 您可以在本文档末尾找到特定于主要版本迁移的迁移指南。

Spring Data 的文档特定于该版本,因此您在此处找到的任何信息都将包含该版本中的最新更改。

9. 为什么选择弹簧数据 Redis?

Spring 框架是领先的全栈 Java/JEE 应用程序框架。它提供了一个轻量级容器和一个通过使用依赖注入、AOP 和可移植服务抽象实现的非侵入式编程模型。

NoSQL存储系统为传统RDBMS提供了水平可扩展性和速度的替代方案。在实现方面,键值存储代表了NoSQL空间中最大(也是最古老的)成员之一。

Spring Data Redis(SDR)框架通过Spring出色的基础架构支持消除了与存储交互所需的冗余任务和样板代码,从而可以轻松编写使用Redis键值存储的Spring应用程序。

10. 红地思支持

Spring Data 支持的键值存储之一是Redis。引用 Redis 项目主页:

Redis 是一个高级键值存储。它类似于 memcached 但数据集不是易失性的,值可以是字符串,就像在 memcached 中一样,也可以是列表、集合和有序集合。所有这些数据类型都可以通过原子操作来推送/弹出元素、添加/删除元素、执行服务器端并集、交集、集合之间的差异等。Redis 支持不同类型的排序功能。

Spring Data Redis 提供了从 Spring 应用程序对 Redis 的简单配置和访问。它提供了用于与商店交互的低级和高级抽象,使用户摆脱了基础设施问题。

10.1. 入门

设置工作环境的一种简单方法是在STS 中创建基于 Spring 的项目。

首先,您需要设置一个正在运行的 Redis 服务器。

要在 STS 中创建 Spring 项目,请执行以下操作:

  1. 转到→“新建→ Spring 模板项目”→“简单 Spring 实用程序项目”,并在出现提示时按“是”。然后输入项目和包名称,例如。 .将以下内容添加到 pom.xml 文件元素:org.spring.redis.exampledependencies
<dependencies>

<!-- other dependency elements omitted -->

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>3.0.0</version>
</dependency>

</dependencies>
  1. 将 pom 中的 Spring 版本.xml更改为
<spring.framework.version>6.0.0</spring.framework.version>
  1. 将 Maven 的 Spring 里程碑存储库的以下位置添加到您的位置,使其与 yourelement 处于同一级别:pom.xml<dependencies/>
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Maven MILESTONE Repository</name>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>

存储库也可以在此处浏览。

10.2. 红地符号要求

Spring Redis 需要 Redis 2.6 或更高版本,Spring Data Redis 集成了Lettuce和Jedis,这是两个流行的 Redis 开源 Java 库。

10.3. Redis 支持高级视图

Redis 支持提供了多个组件。对于大多数任务,高级抽象和支持服务是最佳选择。请注意,您可以随时在图层之间移动。例如,您可以获得低级连接(甚至本机库)以直接与 Redis 通信。

10.4. 连接到 Redis

使用 Redis 和 Spring 时的首要任务之一是通过 IoC 容器连接到存储。为此,需要 Java 连接器(或绑定)。无论您选择哪个库,您只需要使用一组 Spring Data Redis API(在所有连接器上行为一致):用于处理和检索与 Redis 的活动连接的 package 和 itsand接口。​​org.springframework.data.redis.connection​​​​RedisConnection​​​​RedisConnectionFactory​

10.4.1. 重新连接和重新连接工厂

​RedisConnection​​为 Redis 通信提供核心构建块,因为它处理与 Redis 后端的通信。它还会自动将底层连接库异常转换为 Spring 一致的 DAO 异常层次结构,以便您可以在不更改任何代码的情况下切换连接器,因为操作语义保持不变。

对于需要本机库 API 的极端情况,提供专用方法 (),该方法返回用于通信的原始基础对象。​​RedisConnection​​​​getNativeConnection​

活动对象是通过创建的。此外,工厂充当对象,这意味着一旦声明,它们就允许您执行透明的异常转换。例如,您可以通过使用注释和 AOP 进行异常翻译。有关更多信息,请参阅 Spring 框架文档中的专用部分。​​RedisConnection​​​​RedisConnectionFactory​​​​PersistenceExceptionTranslator​​​​@Repository​


根据基础配置,工厂可以返回新连接或现有连接(使用池或共享本机连接时)。

使用 ais 通过 IoC 容器配置适当的连接器并将其注入 using 类的最简单方法。​​RedisConnectionFactory​

遗憾的是,目前并非所有连接器都支持所有 Redis 功能。在连接 API 上调用基础库不支持的方法时,会引发 anis。以下概述介绍了各个 Redis 连接器支持的功能:​​UnsupportedOperationException​

表 1.跨 Redis 连接器的功能可用性

支持的功能

生菜

杰迪斯

独立连接

X

X

主/副本连接

X

雷迪斯哨兵

主查找、哨兵身份验证、副本读取

主查找

红地斯集群

群集连接、群集节点连接、副本读取

群集连接, 群集节点连接

运输渠道

TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets

技术合作计划(TCP

连接池

X(使用​​commons-pool2​​)

X(使用​​commons-pool2​​)

其他连接功能

非阻塞命令的单一实例连接共享

流水线和事务互斥。无法在管道/事务中使用服务器/连接命令。

静态服务器支持

X

X

发布/订阅

X

X

流水线

X

X(流水线和事务互斥)

交易

X

X(流水线和事务互斥)

数据类型支持

键, 字符串, 列表, 设置, 排序集, 哈希, 服务器, 流, 脚本, 地理, 超日志日志

键, 字符串, 列表, 设置, 排序集, 哈希, 服务器, 脚本, 地理, 超级日志日志

反应式(非阻塞)API

X

10.4.2. 配置生菜连接器

生菜是一个基于Netty 的开源连接器,由 Spring Data Redis 通过软件包支持。​​org.springframework.data.redis.connection.lettuce​

将以下内容添加到 pom.xml 文件元素:​​dependencies​

<dependencies>

<!-- other dependency elements omitted -->

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>

</dependencies>

以下示例演示如何创建新的生菜连接工厂:

@Configuration
class AppConfig {

@Bean
public LettuceConnectionFactory redisConnectionFactory() {

return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379));
}
}

还有一些特定于生菜的连接参数可以调整。默认情况下,the创建的所有实例为所有非阻塞和非事务性操作共享相同的线程安全本机连接。要每次都使用专用连接,setto.还可以配置为使用 afor 池阻塞和事务连接或 ifis 设置为的所有连接。​​LettuceConnection​​​​LettuceConnectionFactory​​​​shareNativeConnection​​​​false​​​​LettuceConnectionFactory​​​​LettucePool​​​​shareNativeConnection​​​​false​

Lettuce与Netty的本地传输集成,允许您使用Unix域套接字与Redis进行通信。请确保包含与运行时环境匹配的适当本机传输依赖项。以下示例演示如何为 Unix 域套接字创建生菜连接工厂:​​/var/run/redis.sock​

@Configuration
class AppConfig {

@Bean
public LettuceConnectionFactory redisConnectionFactory() {

return new LettuceConnectionFactory(new RedisSocketConfiguration("/var/run/redis.sock"));
}
}

Netty目前支持用于操作系统原生传输的epoll(Linux)和kqueue(BSD/macOS)接口。

10.4.3. 配置 Jedis 连接器

Jedis是一个社区驱动的连接器,由Spring Data Redis模块通过软件包支持。​​org.springframework.data.redis.connection.jedis​

将以下内容添加到 pom.xml 文件元素:​​dependencies​

<dependencies>

<!-- other dependency elements omitted -->

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>

</dependencies>

在最简单的形式中,Jedis 配置如下所示:

@Configuration
class AppConfig {

@Bean
public JedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}
}

但是,对于生产用途,您可能需要调整主机或密码等设置,如以下示例所示:

@Configuration
class RedisConfiguration {

@Bean
public JedisConnectionFactory redisConnectionFactory() {

RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("server", 6379);
return new JedisConnectionFactory(config);
}
}

10.4.4. 写入主节点,从副本读取

Redis 主/副本设置 — 没有自动故障转移(有关自动故障转移,请参阅:Sentinel) — 不仅允许将数据安全地存储在更多节点上。它还允许通过使用Lettuce,从副本读取数据,同时将写入推送到主服务器。您可以设置要使用的读/写策略,如以下示例所示:​​LettuceClientConfiguration​

@Configuration
class WriteToMasterReadFromReplicaConfiguration {

@Bean
public LettuceConnectionFactory redisConnectionFactory() {

LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(REPLICA_PREFERRED)
.build();

RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("server", 6379);

return new LettuceConnectionFactory(serverConfig, clientConfig);
}
}

对于通过命令报告非公共地址的环境(例如,使用 AWS 时),请使用代替。请注意,不支持发布/订阅,因为缺少跨单个服务器的发布/订阅消息传播。​​INFO​​​​RedisStaticMasterReplicaConfiguration​​​​RedisStandaloneConfiguration​​​​RedisStaticMasterReplicaConfiguration​

10.5. 雷迪斯哨兵支持

为了处理高可用性 Redis,Spring Data Redis 支持Redis Sentinel,如以下示例所示:​​RedisSentinelConfiguration​

/**
* Lettuce
*/
@Bean
public RedisConnectionFactory lettuceConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("127.0.0.1", 26379)
.sentinel("127.0.0.1", 26380);
return new LettuceConnectionFactory(sentinelConfig);
}

/**
* Jedis
*/
@Bean
public RedisConnectionFactory jedisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("127.0.0.1", 26379)
.sentinel("127.0.0.1", 26380);
return new JedisConnectionFactory(sentinelConfig);
}


​RedisSentinelConfiguration​​​也可以使用 A 定义,这允许您设置以下属性:​​PropertySource​



配置属性

  • ​spring.redis.sentinel.master​​:主节点的名称。
  • ​spring.redis.sentinel.nodes​​:以逗号分隔的主机:端口对列表。
  • ​spring.redis.sentinel.username​​:使用 Redis Sentinel 进行身份验证时要应用的用户名(需要 Redis 6)
  • ​spring.redis.sentinel.password​​:使用 Redis 哨兵进行身份验证时要应用的密码


有时,需要与其中一个哨兵直接互动。使用或允许您访问配置的第一个活动哨兵。​​RedisConnectionFactory.getSentinelConnection()​​​​RedisConnection.getSentinelCommands()​

10.6. 通过 RedisTemplate 处理对象

大多数用户可能会使用及其相应的包,。事实上,该模板是 Redis 模块的核心类,因为它具有丰富的功能集。该模板为 Redis 交互提供了高级抽象。虽然提供接受和返回二进制值(数组)的低级方法,但模板负责序列化和连接管理,使用户免于处理此类细节。​​RedisTemplate​​​​org.springframework.data.redis.core​​​​RedisConnection​​​​byte​

此外,该模板还提供操作视图(遵循 Redis 命令参考中的分组),这些视图提供了丰富的通用接口,用于处理特定类型或特定键(通过接口),如下表所述:​​KeyBound​

表 2.操作视图

接口

描述

键类型操作

​GeoOperations​

Redis 地理空间操作,例如,,...​​GEOADD​​​​GEORADIUS​

​HashOperations​

红与人哈希操作

​HyperLogLogOperations​

Redis HyperLogLog 操作,例如,,...​​PFADD​​​​PFCOUNT​

​ListOperations​

红地符号列表操作

​SetOperations​

红地集操作

​ValueOperations​

Redis 字符串(或值)操作

​ZSetOperations​

Redis zset(或排序集)操作

键绑定操作

​BoundGeoOperations​

Redis 键绑定地理空间操作

​BoundHashOperations​

Redis 哈希键绑定操作

​BoundKeyOperations​

Redis 键绑定操作

​BoundListOperations​

Redis 列表键绑定操作

​BoundSetOperations​

Redis 设置键绑定操作

​BoundValueOperations​

Redis 字符串(或值)键绑定操作

​BoundZSetOperations​

Redis zset(或排序集)键绑定操作

配置后,模板是线程安全的,可以在多个实例中重复使用。

​RedisTemplate​​使用基于 Java 的序列化程序进行大部分操作。这意味着模板写入或读取的任何对象都通过 Java 进行序列化和反序列化。您可以更改模板上的序列化机制,Redis 模块提供了多种实现,这些实现在包中可用。有关详细信息,请参阅序列化程序。您还可以将任何序列化程序设置为 null,并通过将属性设置为 来将 RedisTemplate 与原始字节数组一起使用。请注意,模板要求所有键均为非 null。但是,只要基础序列化程序接受值,它们就可以为 null。有关详细信息,请阅读每个序列化程序的 Javadoc。​​org.springframework.data.redis.serializer​​​​enableDefaultSerializer​​​​false​

对于需要某个模板视图的情况,请将该视图声明为依赖项并注入模板。容器会自动执行转换,消除调用,如以下示例所示:​​opsFor[X]​

爪哇岛

.XML

@Configuration
class MyConfig {

@Bean
LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}

@Bean
RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
public class Example {

// inject the actual template
@Autowired
private RedisTemplate<String, String> template;

// inject the template as ListOperations
@Resource(name="redisTemplate")
private ListOperations<String, String> listOps;

public void addLink(String userId, URL url) {
listOps.leftPush(userId, url.toExternalForm());
}
}

10.7. 以字符串为中心的便利类

由于存储在 Redis 中的键和值很常见,因此 Redis 模块提供了两个扩展和(及其实现),分别是 and,作为密集字符串操作的便捷一站式解决方案。除了绑定到密钥之外,模板和连接还使用 underneath,这意味着存储的密钥和值是人类可读的(假设在 Redis 和您的代码中使用相同的编码)。以下清单显示了一个示例:​​java.lang.String​​​​RedisConnection​​​​RedisTemplate​​​​StringRedisConnection​​​​DefaultStringRedisConnection​​​​StringRedisTemplate​​​​String​​​​StringRedisSerializer​

爪哇岛

.XML

@Configuration
class MyConfig {

@Bean
LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}

@Bean
StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {

StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
public class Example {

@Autowired
private StringRedisTemplate redisTemplate;

public void addLink(String userId, URL url) {
redisTemplate.opsForList().leftPush(userId, url.toExternalForm());
}
}

与其他 Spring 模板一样,并允许您通过界面直接与 Redis 对话。此功能为您提供了完全控制权,因为它直接与 .请注意,回调接收何时使用 ais 的实例。以下示例演示如何使用接口:​​RedisTemplate​​​​StringRedisTemplate​​​​RedisCallback​​​​RedisConnection​​​​StringRedisConnection​​​​StringRedisTemplate​​​​RedisCallback​

public void useCallback() {

redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
Long size = connection.dbSize();
// Can cast to StringRedisConnection if using a StringRedisTemplate
((StringRedisConnection)connection).set("key", "value");
}
});
}

10.8. 序列化程序

从框架的角度来看,存储在 Redis 中的数据只是字节。虽然 Redis 本身支持各种类型,但在大多数情况下,这些类型是指数据的存储方式,而不是它所代表的内容。由用户决定是将信息转换为字符串还是任何其他对象。

在 Spring Data 中,用户(自定义)类型和原始数据(反之亦然)之间的转换在包中处理 Redis。​​org.springframework.data.redis.serializer​

此包包含两种类型的序列化程序,顾名思义,它们负责序列化过程:

  • 基于双向序列化程序。RedisSerializer
  • 元素读取器和使用和的编写器。RedisElementReaderRedisElementWriter

这些变体之间的主要区别在于,它主要序列化给读者和作家使用。​​RedisSerializer​​​​byte[]​​​​ByteBuffer​

有多种实现可用(包括本文档中已经提到的两个):

  • ​JdkSerializationRedisSerializer​​,默认用于。RedisCacheRedisTemplate
  • 这。StringRedisSerializer

但是,可以通过SpringOXM支持对象/ XML映射或以JSON格式存储数据。​​OxmSerializer​​​​Jackson2JsonRedisSerializer​​​​GenericJackson2JsonRedisSerializer​

请注意,存储格式不仅限于值。它可以用于键、值或哈希,没有任何限制。


默认情况下,并且配置为使用 Java 本机序列化。Java 本机序列化以允许运行由有效负载引起的远程代码而闻名,这些有效负载利用易受的库和类注入未经验证的字节码。操作的输入可能会导致在反序列化步骤期间在应用程序中运行不需要的代码。因此,不要在不受信任的环境中使用序列化。通常,我们强烈建议改用任何其他消息格式(例如 JSON)。​​RedisCache​​​​RedisTemplate​



如果您担心 Java 序列化带来的安全漏洞,请考虑核心 JVM 级别的通用序列化过滤器机制,该机制最初是为 JDK 9 开发的,但向后移植到 JDK 8、7 和 6:

  • 筛选传入的序列化数据。
  • 吉普车 290.
  • OWASP:不受信任数据的反序列化。


10.9. 哈希映射

可以使用 Redis 中的各种数据结构存储数据,可以转换JSON格式的对象。理想情况下,JSON 可以使用纯键存储为值。您可以使用 Redis 哈希实现更复杂的结构化对象映射。Spring Data Redis 提供了各种将数据映射到哈希的策略(取决于用例):​​Jackson2JsonRedisSerializer​

  • 直接映射,通过使用和序列化程序HashOperations
  • 使用Redis 存储库
  • 使用和HashMapperHashOperations

10.9.1. 哈希映射器

哈希映射器是将映射对象转换为 aand back.is,用于与 Redis 哈希一起使用。​​Map<K, V>​​​​HashMapper​

有多种实现方式可用:

  • ​BeanUtilsHashMapper​​使用Spring的BeanUtils。
  • ObjectHashMapper使用对象到哈希映射。
  • Jackson2HashMapper using FasterXML Jackson.

以下示例显示了实现哈希映射的一种方法:

public class Person {
String firstname;
String lastname;

// …
}

public class HashMapping {

@Autowired
HashOperations<String, byte[], byte[]> hashOperations;

HashMapper<Object, byte[], byte[]> mapper = new ObjectHashMapper();

public void writeHash(String key, Person person) {

Map<byte[], byte[]> mappedHash = mapper.toHash(person);
hashOperations.putAll(key, mappedHash);
}

public Person loadHash(String key) {

Map<byte[], byte[]> loadedHash = hashOperations.entries("key");
return (Person) mapper.fromHash(loadedHash);
}
}

10.9.2. Jackson2HashMapper

​Jackson2HashMapper​​通过使用FasterXML Jackson 为域对象提供 Redis 哈希映射。可以将*属性映射为哈希字段名称,并选择性地平展结构。 简单类型映射到简单值。复杂类型(嵌套对象、集合、映射等)表示为嵌套 JSON。​​Jackson2HashMapper​

平展为所有嵌套属性创建单独的哈希条目,并尽可能将复杂类型解析为简单类型。

请考虑以下类及其包含的数据结构:

public class Person {
String firstname;
String lastname;
Address address;
Date date;
LocalDateTime localDateTime;
}

public class Address {
String city;
String country;
}

下表显示了上述类中的数据在法线映射中的显示方式:

表 3.法线贴图

哈希字段

价值

​Jon​

姓氏

​Snow​

地址

​{ "city" : "Castle Black", "country" : "The North" }​

日期

​1561543964015​

本地日期时间

​2018-01-02T12:13:14​

下表显示了上述类中的数据在平面映射中的显示方式:

表 4.平面映射

哈希字段

价值

​Jon​

姓氏

​Snow​

地址.城市

​Castle Black​

地址.国家

​The North​

日期

​1561543964015​

本地日期时间

​2018-01-02T12:13:14​

平展要求所有属性名称不会干扰 JSON 路径。使用平展时,不支持在映射键中使用点或括号或作为属性名称。生成的哈希无法映射回对象。

​java.util.Date​​​并且用毫秒表示。JSR-310 日期/时间类型在类路径上序列化为它们的形式 ifi。​​java.util.Calendar​​​​toString​​​​jackson-datatype-jsr310​

10.10. Redis 消息传递(发布/订阅)

Spring Data 为 Redis 提供了专用的消息传递集成,在功能和命名上类似于 Spring Framework 中的 JMS 集成。

Redis 消息传递大致可以分为两个功能区域:

  • 信息的发布或制作
  • 订阅或使用消息

这是通常称为发布/订阅(简称 Pub/Sub)的模式的示例。该类用于消息生成。对于类似于Java EE的消息驱动Bean风格的异步接收,Spring Data提供了一个专用的消息侦听器容器,用于创建消息驱动的POJO(MDP),对于同步接收,合约。​​RedisTemplate​​​​RedisConnection​

Theandpackages 为 Redis 消息传递提供了核心功能。​​org.springframework.data.redis.connection​​​​org.springframework.data.redis.listener​

10.10.1. 发布(发送消息)

要发布消息,您可以像其他操作一样使用低级别或高级。这两个实体都提供方法,该方法接受消息和目标通道作为参数。虽然需要原始数据(字节数组),但允许任意对象作为消息传入,如以下示例所示:​​RedisConnection​​​​RedisTemplate​​​​publish​​​​RedisConnection​​​​RedisTemplate​

// send message through connection RedisConnection con = ...
byte[] msg = ...
byte[] channel = ...
con.publish(msg, channel); // send message through RedisTemplate
RedisTemplate template = ...
Long numberOfClients = template.convertAndSend("hello!", "world");

10.10.2. 订阅(接收消息)

在接收端,可以通过直接命名或使用模式匹配来订阅一个或多个通道。后一种方法非常有用,因为它不仅允许使用一个命令创建多个订阅,还可以侦听订阅时尚未创建的通道(只要它们与模式匹配)。

在低级别,提供了映射 Redis 命令以分别按通道或模式订阅的 theand方法。请注意,多个通道或模式可以用作参数。要更改连接的订阅或查询是否正在侦听,请提供 和 方法。​​RedisConnection​​​​subscribe​​​​pSubscribe​​​​RedisConnection​​​​getSubscription​​​​isSubscribed​

Spring Data Redis 中的订阅命令正在阻塞。也就是说,在连接上调用订阅会导致当前线程在开始等待消息时阻塞。仅当订阅被取消时,才会释放线程,当另一个线程调用同一连接时,就会发生这种情况。有关此问题的解决方案,请参阅“消息侦听器容器​”(本文档后面)。​​unsubscribe​​​​pUnsubscribe​

如前所述,订阅后,连接开始等待消息。仅允许添加新订阅、修改现有订阅和取消现有订阅的命令。调用除,,,或引发异常以外的任何内容。​​subscribe​​​​pSubscribe​​​​unsubscribe​​​​pUnsubscribe​

为了订阅消息,需要实现回调。每次有新消息到达时,都会调用回调,用户代码由方法运行。该接口不仅可以访问实际消息,还可以访问接收消息的通道以及订阅用于匹配通道的模式(如果有)。此信息使被叫方不仅可以通过内容来区分各种消息,还可以检查其他详细信息。​​MessageListener​​​​onMessage​

消息侦听器容器

由于其阻塞性质,低级订阅没有吸引力,因为它需要为每个侦听器进行连接和线程管理。为了缓解这个问题,Spring Data提供了所有繁重的工作。如果你熟悉 EJB 和 JMS,你应该会发现这些概念很熟悉,因为它被设计为尽可能接近 Spring 框架及其消息驱动的 POJO (MDP) 中的支持。​​RedisMessageListenerContainer​

​RedisMessageListenerContainer​​充当消息侦听器容器。它用于从 Redis 通道接收消息并驱动注入其中的实例。侦听器容器负责消息接收的所有线程处理,并调度到侦听器进行处理。消息侦听器容器是 MDP 和消息传递提供程序之间的中介,负责注册以接收消息、资源获取和释放、异常转换等。这使您作为应用程序开发人员可以编写与接收消息(并对其做出反应)相关的(可能很复杂的)业务逻辑,并将样板 Redis 基础架构问题委托给框架。​​MessageListener​

Acan 还实现了在订阅/取消订阅确认时接收通知。同步调用时,侦听订阅通知可能很有用。​​MessageListener​​​​SubscriptionListener​

此外,为了最大程度地减少应用程序占用空间,允许多个侦听器共享一个连接和一个线程,即使它们不共享订阅。因此,无论应用程序跟踪多少个侦听器或通道,运行时成本在其整个生命周期中都保持不变。此外,容器允许更改运行时配置,以便您可以在应用程序运行时添加或删除侦听器,而无需重新启动。此外,容器使用延迟订阅方法,仅在需要时使用 a。如果取消订阅了所有侦听器,则会自动执行清理,并释放线程。​​RedisMessageListenerContainer​​​​RedisConnection​

为了帮助解决消息的异步性质,容器需要一个(或 Spring 的)来调度消息。根据负载、侦听器数量或运行时环境,应更改或调整执行程序以更好地满足您的需求。特别是,在托管环境(如应用服务器)中,强烈建议选择适当的环境以利用其运行时。​​java.util.concurrent.Executor​​​​TaskExecutor​​​​TaskExecutor​

消息侦听器适配器

Theclass 是 Spring 异步消息传递支持的最后一个组件。简而言之,它允许您将几乎任何类公开为 MDP(尽管存在一些约束)。​​MessageListenerAdapter​

请考虑以下接口定义:

public interface MessageDelegate {
void handleMessage(String message);
void handleMessage(Map message);
void handleMessage(byte[] message);
void handleMessage(Serializable message);
// pass the channel/pattern as well
void handleMessage(Serializable message, String channel);
}

请注意,尽管接口不扩展接口,但它仍可用作 MDP。另请注意,各种消息处理方法是如何根据它们可以接收和处理的各种类型的内容进行强类型的。此外,消息发送到的通道或模式可以作为类型的第二个参数传递给该方法:​​MessageListener​​​​MessageListenerAdapter​​​​Message​​​​String​

public class DefaultMessageDelegate implements MessageDelegate {
// implementation elided for clarity...
}

请注意,上述接口实现(上面的类)根本没有 Redis依赖项。它确实是一个POJO,我们将其制作成具有以下配置的MDP:​​MessageDelegate​​​​DefaultMessageDelegate​

爪哇岛

.XML

@Configuration
class MyConfig {

// …

@Bean
DefaultMessageDelegate listener() {
return new DefaultMessageDelegate();
}

@Bean
RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, DefaultMessageDelegate listener) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(new MessageListenerAdapter(listener, "handleMessage"), ChannelTopic.of("chatroom"));
return container;
}
}

侦听器主题可以是通道(例如)或模式(例如,​​topic="chatroom"​​​​topic="*room"​​)

前面的示例使用 Redis 命名空间声明消息侦听器容器,并自动将 POJO 注册为侦听器。成熟的豆子定义如下:

<bean  class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="redisexample.DefaultMessageDelegate"/>
</constructor-arg>
</bean>

<bean class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="messageListeners">
<map>
<entry key-ref="messageListener">
<bean class="org.springframework.data.redis.listener.ChannelTopic">
<constructor-arg value="chatroom"/>
</bean>
</entry>
</map>
</property>
</bean>

每次收到消息时,适配器都会自动且透明地在低级格式和所需对象类型之间执行转换(使用配置的)。由方法调用导致的任何异常都由容器捕获和处理(默认情况下,将记录异常)。​​RedisSerializer​

10.11. 红地斯流

Redis 流以抽象方法对日志数据结构进行建模。通常,日志是仅追加的数据结构,从一开始就在随机位置或通过流式传输新消息来使用。

在Redis 参考文档中了解有关 Redis 流的更多信息。

Redis 流大致可以分为两个功能区域:

  • 附加记录
  • 使用记录

尽管此模式与Pub/Sub 有相似之处,但主要区别在于消息的持久性以及它们的使用方式。

虽然 Pub/Sub 依赖于瞬态消息的广播(即,如果您不侦听,则会错过一条消息),而 Redis 流使用持久、仅追加的数据类型,该数据类型保留消息直到流被修剪。使用的另一个区别是 Pub/Sub 注册服务器端订阅。Redis 将到达的消息推送到客户端,而 Redis 流需要主动轮询。

Theandpackages 为 Redis Streams 提供了核心功能。​​org.springframework.data.redis.connection​​​​org.springframework.data.redis.stream​

10.11.1. 追加

要发送记录,您可以像其他操作一样使用低级别或高级。这两个实体都提供 () 方法,该方法接受记录和目标流作为参数。虽然需要原始数据(字节数组),但允许将任意对象作为记录传入,如以下示例所示:​​RedisConnection​​​​StreamOperations​​​​add​​​​xAdd​​​​RedisConnection​​​​StreamOperations​

// append message through connection
RedisConnection con = …
byte[] stream = …
ByteRecord record = StreamRecords.rawBytes(…).withStreamKey(stream);
con.xAdd(record);

// append message through RedisTemplate
RedisTemplate template = …
StringRecord record = StreamRecords.string(…).withStreamKey("my-stream");
template.streamOps().add(record);

流记录携带键值元组作为其有效负载。将记录追加到流返回可用作进一步参考的内容。​​Map​​​​RecordId​

10.11.2. 消费

在消费端,可以消费一个或多个流。Redis 流提供读取命令,允许从已知流内容内的任意位置(随机访问)和流端之外使用流,以使用新的流记录。

在低级别,提供映射 Redis 命令以分别在使用者组中读取和读取的 theand方法。请注意,多个流可以用作参数。​​RedisConnection​​​​xRead​​​​xReadGroup​


Redis 中的订阅命令可能会阻塞。也就是说,调用连接会导致当前线程在开始等待消息时阻塞。仅当读取命令超时或收到消息时,才会释放线程。​​xRead​

若要使用流消息,可以在应用程序代码中轮询消息,也可以通过消息侦听器容器使用两个异步接收之一,即命令式或反应式接收。每次有新记录到达时,容器都会通知应用程序代码。

同步接收

虽然流消耗通常与异步处理相关联,但可以同步使用消息。重载方法提供此功能。在同步接收期间,调用线程可能会阻塞,直到消息可用。该属性指定接收方在放弃等待消息之前应等待的时间。​​StreamOperations.read(…)​​​​StreamReadOptions.block​

// Read message through RedisTemplate
RedisTemplate template = …

List<MapRecord<K, HK, HV>> messages = template.streamOps().read(StreamReadOptions.empty().count(2),
StreamOffset.latest("my-stream"));

List<MapRecord<K, HK, HV>> messages = template.streamOps().read(Consumer.from("my-group", "my-consumer"),
StreamReadOptions.empty().count(2),
StreamOffset.create("my-stream", ReadOffset.lastConsumed()))
通过消息侦听器容器进行异步接收

由于其阻塞性质,低级轮询没有吸引力,因为它需要为每个消费者进行连接和线程管理。为了缓解这个问题,Spring Data提供了消息侦听器,可以完成所有繁重的工作。如果你熟悉 EJB 和 JMS,你应该会发现这些概念很熟悉,因为它被设计为尽可能接近 Spring 框架及其消息驱动的 POJO (MDP) 中的支持。

Spring Data 附带了两个针对所用编程模型量身定制的实现:

  • ​StreamMessageListenerContainer​​充当命令式编程模型的消息侦听器容器。它用于使用来自 Redis 流的记录并驱动注入其中的实例。StreamListener
  • ​StreamReceiver​​提供消息侦听器的反应变体。它用于将来自 Redis 流的消息用作潜在的无限流,并通过 发出流消息。Flux

​StreamMessageListenerContainer​​并且负责消息接收的所有线程,并调度到侦听器进行处理。消息侦听器容器/接收方是 MDP 和消息传递提供者之间的中介,负责注册以接收消息、资源获取和释放、异常转换等。这使您作为应用程序开发人员可以编写与接收消息(并对其做出反应)相关的(可能很复杂的)业务逻辑,并将样板 Redis 基础架构问题委托给框架。​​StreamReceiver​

这两个容器都允许更改运行时配置,以便你可以在应用程序运行时添加或删除订阅,而无需重新启动。此外,容器使用延迟订阅方法,仅在需要时使用 a。如果取消订阅所有侦听器,它会自动执行清理,并释放线程。​​RedisConnection​

祈使的​​StreamMessageListenerContainer​

与 EJB 世界中的消息驱动 Bean (MDB) 类似,流驱动 POJO (SDP) 充当流消息的接收器。SDP 的一个限制是它必须实现接口。另请注意,如果您的 POJO 在多个线程上接收消息,请务必确保您的实现是线程安全的。​​org.springframework.data.redis.stream.StreamListener​

class ExampleStreamListener implements StreamListener<String, MapRecord<String, String, String>> {

@Override
public void onMessage(MapRecord<String, String, String> message) {

System.out.println("MessageId: " + message.getId());
System.out.println("Stream: " + message.getStream());
System.out.println("Body: " + message.getValue());
}
}

​StreamListener​​表示一个功能接口,因此可以使用其 Lambda 形式重写实现:

message -> {

System.out.println("MessageId: " + message.getId());
System.out.println("Stream: " + message.getStream());
System.out.println("Body: " + message.getValue());
};

实现 后,就可以创建消息侦听器容器并注册订阅了:​​StreamListener​

RedisConnectionFactory connectionFactory = …
StreamListener<String, MapRecord<String, String, String>> streamListener = …

StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainerOptions
.builder().pollTimeout(Duration.ofMillis(100)).build();

StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(connectionFactory,
containerOptions);

Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), streamListener);

请参考各种消息侦听器容器的 Javadoc,了解每个实现所支持的功能的完整描述。

反应性的​​StreamReceiver​

流数据源的反应性使用通常通过 aof 事件或消息发生。提供了反应式接收器实现及其重载消息。与利用驱动程序提供的线程资源相比,反应式方法需要较少的基础结构资源(如线程)。接收流是以下各项的需求驱动发布者:​​Flux​​​​StreamReceiver​​​​receive(…)​​​​StreamMessageListenerContainer​​​​StreamMessage​

Flux<MapRecord<String, String, String>> messages = …

return messages.doOnNext(it -> {
System.out.println("MessageId: " + message.getId());
System.out.println("Stream: " + message.getStream());
System.out.println("Body: " + message.getValue());
});

现在我们需要创建并注册一个订阅来使用流消息:​​StreamReceiver​

ReactiveRedisConnectionFactory connectionFactory = …

StreamReceiverOptions<String, MapRecord<String, String, String>> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100))
.build();
StreamReceiver<String, MapRecord<String, String, String>> receiver = StreamReceiver.create(connectionFactory, options);

Flux<MapRecord<String, String, String>> messages = receiver.receive(StreamOffset.fromStart("my-stream"));

请参考各种消息侦听器容器的 Javadoc,了解每个实现所支持的功能的完整描述。

需求驱动的消费使用背压信号来激活和停用轮询。如果需求得到满足,订阅将暂停轮询,直到订阅者发出进一步需求的信号。根据策略的不同,这可能会导致跳过消息。​​StreamReceiver​​​​ReadOffset​

​Acknowledge​​策略

当您通过 读取消息时,服务器将记住给定的消息已传递,并将其添加到待处理条目列表 (PEL) 中。已送达但尚未确认的消息列表。
必须通过确认消息才能从待处理条目列表中删除,如下面的代码片段所示。​​Consumer Group​​​​StreamOperations.acknowledge​

StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = ...

container.receive(Consumer.from("my-group", "my-consumer"),
StreamOffset.create("my-stream", ReadOffset.lastConsumed()),
msg -> {

// ...
redisTemplate.opsForStream().acknowledge("my-group", msg);
});

从组my-group 中读取为我的消费者。收到的消息不会被确认。

处理后确认消息。

在接收时使用自动确认消息,而不是。​​receiveAutoAck​​​​receive​

​ReadOffset​​策略

流读取操作接受读取偏移量规范以使用来自给定偏移量的消息。表示读取偏移量规范。Redis 支持 3 种偏移变体,具体取决于您是独立使用流还是在使用者组中使用流:​​ReadOffset​

  • ​ReadOffset.latest()​​– 阅读最新消息。
  • ​ReadOffset.from(…)​​– 在特定消息 ID 后阅读。
  • ​ReadOffset.lastConsumed()​​– 在上次使用的消息 ID 之后读取(仅限使用者组)。

在基于消息容器的使用上下文中,我们需要在使用消息时提前(或增加)读取偏移量。推进取决于请求和消费模式(有/没有消费组)。以下矩阵说明了容器如何推进:​​ReadOffset​​​​ReadOffset​

表 5.读取偏移前进

最近的

阅读最新消息

阅读最新消息

特定消息 ID

使用上次查看的消息作为下一个消息 ID

使用上次查看的消息作为下一个消息 ID

读取偏移量

独立

消费群体

上次消耗

使用上次查看的消息作为下一个消息 ID

每个使用者组的上次使用的消息

从特定消息 ID 和上次使用的消息读取可被视为安全操作,可确保使用附加到流的所有消息。 使用最新消息进行读取可以跳过在轮询操作处于死区时间状态时添加到流中的消息。轮询引入了一个死区时间,在区时间中,消息可以在各个轮询命令之间到达。流消耗不是线性连续读取,而是拆分为重复调用。​​XREAD​

序列化

发送到流的任何记录都需要序列化为其二进制格式。由于流接近于流键的哈希数据结构,字段名称和值使用在上面配置的相应序列化程序。​​RedisTemplate​

表 6.流序列化

.key

键序列化程序

用于​​Record#getStream()​

哈希键序列化程序

用于有效负载中的每个映射键

流属性

序列化程序

描述

价值

哈希值序列化程序

用于有效负载中的每个映射值

请确保查看正在使用的内容,并注意,如果您决定不使用任何序列化程序,则需要确保这些值已经是二进制的。​​RedisSerializer​

对象映射
简单值

​StreamOperations​​允许通过 via 将简单值直接附加到流中,而无需将这些值放入结构中。 然后,该值将被分配给有效负载字段,并且可以在读回该值时提取。​​ObjectRecord​​​​Map​

ObjectRecord<String, String> record = StreamRecords.newRecord()
.in("my-stream")
.ofObject("my-value");

redisTemplate()
.opsForStream()
.add(record);

List<ObjectRecord<String, String>> records = redisTemplate()
.opsForStream()
.read(String.class, StreamOffset.fromStart("my-stream"));

XADD my-stream * “_class” “java.lang.String” “_raw” “my-value”

​ObjectRecord​​s 通过与所有其他记录完全相同的序列化过程,因此也可以使用返回 a 的非类型化读取操作获取记录。​​MapRecord​

复杂值

可以通过 3 种方式向流添加复杂值:

  • 使用 eg 转换为简单值。字符串 JSON 表示形式。
  • 使用合适的值序列化值。RedisSerializer
  • 使用 a 将值转换为适合序列化的值。MapHashMapper

第一个变体是最直接的变体,但忽略了流结构提供的字段值功能,流中的值仍然可供其他使用者读取。 第二个选项具有与第一个选项相同的优势,但可能会导致非常具体的使用者限制,因为所有使用者都必须实现完全相同的序列化机制。 该方法是使用蒸汽哈希结构的方法稍微复杂一些,但使源代码扁平化。其他使用者只要选择了合适的序列化程序组合,就能够读取记录。​​HashMapper​


HashMappers将有效负载转换为具有特定类型的a。确保使用能够(反)序列化哈希的哈希键和哈希值序列化程序。​​Map​

ObjectRecord<String, User> record = StreamRecords.newRecord()
.in("user-logon")
.ofObject(new User("night", "angel"));

redisTemplate()
.opsForStream()
.add(record);

List<ObjectRecord<String, User>> records = redisTemplate()
.opsForStream()
.read(User.class, StreamOffset.fromStart("user-logon"));

XADD 用户登录 * “_class” “com.example.User” “名字” “夜” “姓氏” “天使”

​StreamOperations​​默认使用ObjectHashMapper。 您可以在获取时提供适合您要求的。​​HashMapper​​​​StreamOperations​

redisTemplate()
.opsForStream(new Jackson2HashMapper(true))
.add(record);

XADD 用户登录 * “名字” “夜” “@class” “com.example.User” “姓氏” “天使”


A可能不知道域类型的任何用法,因为这些需要通过a来解决。 确保使用 a 初始化。​​StreamMessageListenerContainer​​​​@TypeAlias​​​​MappingContext​​​​RedisMappingContext​​​​initialEntitySet​




@Bean
RedisMappingContext redisMappingContext() {
RedisMappingContext ctx = new RedisMappingContext();
ctx.setInitialEntitySet(Collections.singleton(Person.class));
return ctx;
}

@Bean
RedisConverter redisConverter(RedisMappingContext mappingContext) {
return new MappingRedisConverter(mappingContext);
}

@Bean
ObjectHashMapper hashMapper(RedisConverter converter) {
return new ObjectHashMapper(converter);
}

@Bean
StreamMessageListenerContainer streamMessageListenerContainer(RedisConnectionFactory connectionFactory, ObjectHashMapper hashMapper) {
StreamMessageListenerContainerOptions<String, ObjectRecord<String, Object>> options = StreamMessageListenerContainerOptions.builder()
.objectMapper(hashMapper)
.build();

return StreamMessageListenerContainer.create(connectionFactory, options);
}



10.12. 瑞迪斯交易

Redis 通过 、 和 命令为事务提供支持。 这些操作可用。 但是,不能保证以相同的连接运行事务中的所有操作。​​multi​​​​exec​​​​discard​​​​RedisTemplate​​​​RedisTemplate​

Spring Data Redis 提供了接口,用于需要使用相同的多个操作时,例如使用 Redis 事务时。下面的示例使用该方法:​​SessionCallback​​​​connection​​​​multi​

//execute a transaction
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add("key", "value1");

// This will contain the results of all operations in the transaction
return operations.exec();
}
});
System.out.println("Number of items added to set: " + txResults.get(0));

​RedisTemplate​​使用其值、哈希键和哈希值序列化程序在返回之前反序列化所有结果。 还有一个附加方法,可让您为事务结果传递自定义序列化程序。​​exec​​​​exec​

10.12.1. @Transactional支持

默认情况下,不参与托管的 Spring 事务。 如果要在使用 or 时使用 Redis 事务,则需要显式启用 each by 设置的事务支持。 启用事务支持将绑定到 a 支持的当前事务。 如果事务完成且没有错误,则提交 Redis 事务,否则回滚。 Redis 事务是面向批处理的。 在正在进行的事务期间发出的命令将排队,并且仅在提交事务时应用。​​RedisTemplate​​​​RedisTemplate​​​​@Transactional​​​​TransactionTemplate​​​​RedisTemplate​​​​setEnableTransactionSupport(true)​​​​RedisConnection​​​​ThreadLocal​​​​EXEC​​​​DISCARD​

Spring Data Redis 区分正在进行的事务中的只读命令和写入命令。 只读命令(例如)通过管道传输到新的(非线程绑定)以允许读取。 写入命令由提交排队并在提交时应用。​​KEYS​​​​RedisConnection​​​​RedisTemplate​

以下示例演示如何配置事务管理:

例 3.启用事务管理的配置

@Configuration
@EnableTransactionManagement
public class RedisTxContextConfiguration {

@Bean
public StringRedisTemplate redisTemplate() {
StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
// explicitly enable transaction support
template.setEnableTransactionSupport(true);
return template;
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
// jedis || Lettuce
}

@Bean
public PlatformTransactionManager transactionManager() throws SQLException {
return new DataSourceTransactionManager(dataSource());
}

@Bean
public DataSource dataSource() throws SQLException {
// ...
}
}

配置 Spring 上下文以启用声明式事务管理。

配置为通过将连接绑定到当前线程来参与事务。​​RedisTemplate​

事务管理需要 a。 Spring Data Redis 不附带实现。 假设您的应用程序使用 JDBC,Spring Data Redis 可以使用现有的事务管理器参与事务。​​PlatformTransactionManager​​​​PlatformTransactionManager​

以下示例分别演示了一个使用约束:

例 4.使用限制

// must be performed on thread-bound connection
template.opsForValue().set("thing1", "thing2");

// read operation must be run on a free (not transaction-aware) connection
template.keys("*");

// returns null as values set within a transaction are not visible
template.opsForValue().get("thing1");

10.13. 流水线

Redis 提供对流水线的支持,流水线涉及向服务器发送多个命令而无需等待回复,然后一步读取回复。当您需要连续发送多个命令(例如将许多元素添加到同一列表中)时,流水线可以提高性能。

Spring Data Redis 提供了几种在管道中运行命令的方法。如果你不关心流水线操作的结果,你可以使用标准方法,传递 for theargument。这些方法运行提供的管道并返回结果,如以下示例所示:​​RedisTemplate​​​​execute​​​​true​​​​pipeline​​​​executePipelined​​​​RedisCallback​​​​SessionCallback​

//pop a specified number of items from a queue
List<Object> results = stringRedisTemplate.executePipelined(
new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnection stringRedisConn = (StringRedisConnection)connection;
for(int i=0; i< batchSize; i++) {
stringRedisConn.rPop("myqueue");
}
return null;
}
});

前面的示例从管道中的队列中运行批量右弹出项。包含所有弹出的项目。使用其值、哈希键和哈希值序列化程序在返回之前反序列化所有结果,因此前面示例中返回的项是字符串。还有其他方法可用于传递管道结果的自定义序列化程序。​​results​​​​List​​​​RedisTemplate​​​​executePipelined​

请注意,从 theis 返回的值是必需的,因为此值被丢弃,以便返回流水线命令的结果。​​RedisCallback​​​​null​


生菜驱动程序支持细粒度刷新控制,允许在命令出现时刷新命令、缓冲或在连接关闭时发送命令。




LettuceConnectionFactory factory = // ...
factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3));



在本地缓冲,并在每个第 3 个命令后刷新。

10.14. 红地脚本

Redis 版本 2.6 及更高版本支持通过eval和evalsha命令运行 Lua 脚本。Spring Data Redis 为运行脚本提供了一个高级抽象,该脚本处理序列化并自动使用 Redis 脚本缓存。

脚本可以通过调用 of and 的方法运行。两者都使用可配置(或)来运行提供的脚本。默认情况下,the(or) 负责序列化提供的键和参数以及反序列化脚本结果。这是通过模板的键和值序列化程序完成的。还有一个额外的重载,允许您传递脚本参数和结果的自定义序列化程序。​​execute​​​​RedisTemplate​​​​ReactiveRedisTemplate​​​​ScriptExecutor​​​​ReactiveScriptExecutor​​​​ScriptExecutor​​​​ReactiveScriptExecutor​

默认值通过检索脚本的 SHA1 并首先尝试运行来优化性能,如果脚本尚未存在于 Redis 脚本缓存中,则回退到该脚本。​​ScriptExecutor​​​​evalsha​​​​eval​

下面的示例使用 Lua 脚本运行常见的“检查和设置”方案。这是 Redis 脚本的理想用例,因为它要求以原子方式运行一组命令,并且一个命令的行为受另一个命令的结果的影响。

@Bean
public RedisScript<Boolean> script() {

ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua"));
return RedisScript.of(scriptSource, Boolean.class);
}
public class Example {

@Autowired
RedisScript<Boolean> script;

public boolean checkAndSet(String expectedValue, String newValue) {
return redisTemplate.execute(script, singletonList("key"), asList(expectedValue, newValue));
}
}
-- checkandset.lua
local current = redis.call('GET', KEYS[1])
if current == ARGV[1]
then redis.call('SET', KEYS[1], ARGV[2])
return true
end
return false

前面的代码配置指向调用的文件,该文件应返回布尔值。脚本应为,,, 或反序列化值类型之一。如果脚本返回丢弃状态(特别是,),也可以是。​​RedisScript​​​​checkandset.lua​​​​resultType​​​​Long​​​​Boolean​​​​List​​​​null​​​​OK​

理想的做法是在应用程序上下文中配置 的单个实例,以避免在每次脚本运行时重新计算脚本的 SHA1。​​DefaultRedisScript​

然后,上面的方法运行脚本。脚本可以在事务或管道的 aas 部分中运行。有关详细信息,请参阅“Redis 事务”和“流水线”。​​checkAndSet​​​​SessionCallback​

Spring Data Redis 提供的脚本支持还允许您使用 Spring 任务和调度程序抽象来调度 Redis 脚本定期运行。有关更多详细信息,请参阅Spring 框架文档。

10.15. 红地斯缓存

在 2.0 中更改

Spring Redis 通过包为 Spring缓存抽象提供了一个实现。要将 Redis 用作后备实现,请添加到您的配置中,如下所示:​​org.springframework.data.redis.cache​​​​RedisCacheManager​

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);
}

​RedisCacheManager​​可以配置行为,允许您设置默认、事务行为和预定义缓存。​​RedisCacheManagerBuilder​​​​RedisCacheConfiguration​

RedisCacheManager cm = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultCacheConfig())
.withInitialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
.transactionAware()
.build();

如前面的示例所示,允许基于每个缓存定义配置。​​RedisCacheManager​

的行为创建与定义。该配置允许您设置密钥过期时间、前缀和实现,以便与二进制存储格式进行转换,如以下示例所示:​​RedisCache​​​​RedisCacheManager​​​​RedisCacheConfiguration​​​​RedisSerializer​

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1))
.disableCachingNullValues();

​RedisCacheManager​​默认为无锁用于读取和写入二进制值。 无锁缓存可提高吞吐量。 缺少条目锁定可能会导致 the and 方法的重叠、非原子命令,因为这些命令需要将多个命令发送到 Redis。锁定对应项通过设置显式锁定键并检查此键是否存在来防止命令重叠,这会导致额外的请求和潜在的命令等待时间。​​RedisCacheWriter​​​​putIfAbsent​​​​clean​

锁定适用于缓存级别,而不是每个缓存条目

可以选择加入锁定行为,如下所示:

RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory))
.cacheDefaults(defaultCacheConfig())
...

默认情况下,anyfor 缓存条目以实际缓存名称为前缀,后跟两个冒号。 此行为可以更改为静态前缀和计算前缀。​​key​

以下示例演示如何设置静态前缀:

// static key prefix
RedisCacheConfiguration.defaultCacheConfig().prefixKeysWith("( ͡° ᴥ ͡°)");

The following example shows how to set a computed prefix:

// computed key prefix
RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName);

缓存实现默认为 useand 清除缓存。可能会导致大型键空间的性能问题。因此,可以使用 ato 切换到基于 a 的批处理策略来创建默认值。该策略需要批大小以避免过多的 Redis 命令往返:​​KEYS​​​​DEL​​​​KEYS​​​​RedisCacheWriter​​​​BatchStrategy​​​​SCAN​​​​SCAN​

RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
.cacheDefaults(defaultCacheConfig())
...

使用任何驱动程序都完全支持批处理策略,并且使用生菜驱动程序时完全支持Redis操作模式(独立,集群)。Jedis 仅在非群集模式下支持。​​KEYS​​​​SCAN​​​​SCAN​

下表列出了以下各项的默认设置:​​RedisCacheManager​

表 7.默认值​​RedisCacheManager​

设置

价值

缓存编写器

非锁定,批量策略​​KEYS​

缓存配置

​RedisCacheConfiguration#defaultConfiguration​

初始缓存

没有

交易感知

下表列出了以下各项的默认设置:​​RedisCacheConfiguration​

表 8.RedisCacheConfiguration 默认值

密钥过期

没有

缓存​​null​

是的

前缀键

是的

默认前缀

实际缓存名称

密钥序列化程序

​StringRedisSerializer​

值序列化程序

​JdkSerializationRedisSerializer​

转换服务

​DefaultFormattingConversionService​​使用默认缓存键转换器


默认情况下,统计信息处于禁用状态。 用于收集本地命未命中,返回所收集数据的快照。​​RedisCache​​​​RedisCacheManagerBuilder.enableStatistics()​​​​RedisCache#getStatistics()​


10.16. 支持类

包提供了各种依赖 Redis 作为后备存储的可重用组件。目前,该软件包在 Redis 之上包含各种基于 JDK 的接口实现,例如原子计数器和 JDK集合。​​org.springframework.data.redis.support​

原子计数器可以轻松包装 Redis 密钥增量,而集合允许轻松管理 Redis 密钥,同时将存储暴露或 API 泄漏降至最低。特别是,与接口提供了对 Redis 支持的集合操作的轻松访问,例如 and.implements 在 Redis 之上的 、 和 合约(及其等效的阻塞兄弟姐妹),将存储公开为 FIFO(先进先出)、LIFO(后进先出)或具有最小配置的上限集合。以下示例显示了使用 的 Bean 的配置:​​RedisSet​​​​RedisZSet​​​​intersection​​​​union​​​​RedisList​​​​List​​​​Queue​​​​Deque​​​​RedisList​

爪哇岛

.XML

@Configuration
class MyConfig {

// …

@Bean
RedisList<String> stringRedisTemplate(RedisTemplate<String, String> redisTemplate) {
return new DefaultRedisList<>(template, "queue-key");
}
}

以下示例显示了 的 Java 配置示例:​​Deque​

public class AnotherExample {

// injected
private Deque<String> queue;

public void addTag(String tag) {
queue.push(tag);
}
}

如前面的示例所示,使用代码与实际存储实现分离。事实上,没有迹象表明下面使用了 Redis。这使得从开发环境到生产环境的迁移变得透明,并大大提高了可测试性(Redis 实现可以替换为内存中实现)。

10.17. 可观测性

从应用程序组件获取有关其操作、计时以及与应用程序代码的关系的见解对于了解延迟至关重要。 Spring Data Redis 通过 Lettuce 驱动程序附带了千分尺集成,以收集 Redis 交互期间的观测结果。 设置集成后,千分尺将为每个 Redis 命令创建米和跨度(用于分布式跟踪)。

要启用集成,请将以下配置应用于:​​LettuceClientConfiguration​

@Configuration
class ObservabilityConfiguration {

@Bean
public ClientResources clientResources(ObservationRegistry observationRegistry) {

return ClientResources.builder()
.tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache"))
.build();
}

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(ClientResources clientResources) {

LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.clientResources(clientResources).build();
RedisConfiguration redisConfiguration = …;
return new LettuceConnectionFactory(redisConfiguration, clientConfig);
}
}

10.17.1. 可观测性 - 约定

您可以在下面找到该项目声明的alland列表。​​GlobalObservabilityConventions​​​​ObservabilityConventions​

10.17.2. 可观测性 - 指标

您可以在下面找到此项目声明的所有指标的列表。

雷迪斯命令观察


围绕 Redis 命令执行创建的计时器。


指标名称类型和基本单位。​​spring.data.redis​​​​timer​​​​seconds​

封闭类的完全限定名称。​​org.springframework.data.redis.connection.lettuce.observability.RedisObservation​

表 9.低基数键



名字





描述





​db.operation​





Redis 命令值。





​db.redis.database_index​





Redis 数据库索引。





​db.system​





数据库系统。





​db.user​





红人用户。





​net.peer.name​





数据库主机的名称。





​net.peer.port​





逻辑远程端口号。





​net.sock.peer.addr​





蒙戈对等地址。





​net.sock.peer.port​





蒙戈对等端口。





​net.transport​





网络传输。



表 10.高基数键



名字





描述





​db.statement​





雷迪斯声明。





​spring.data.redis.command.error​





Redis 错误响应。



10.17.3. 可观测性 - 跨度

您可以在下面找到该项目声明的所有跨度的列表。

雷迪斯命令观察范围


围绕 Redis 命令执行创建的计时器。


跨度名称。​​spring.data.redis​

封闭类的完全限定名称。​​org.springframework.data.redis.connection.lettuce.observability.RedisObservation​

表 11.标签键

名字

描述

​db.operation​

Redis 命令值。

​db.redis.database_index​

Redis 数据库索引。

​db.statement​

雷迪斯声明。

​db.system​

数据库系统。

​db.user​

红人用户。

​net.peer.name​

数据库主机的名称。

​net.peer.port​

逻辑远程端口号。

​net.sock.peer.addr​

蒙戈对等地址。

​net.sock.peer.port​

蒙戈对等端口。

​net.transport​

网络传输。

​spring.data.redis.command.error​

Redis 错误响应。

另请参阅开放遥测语义约定以获取进一步参考。

11. 反应式 Redis 支持

本节介绍反应式 Redis 支持以及如何入门。响应式 Redis 支持自然与命令式 Redis 支持有一定的重叠。

11.1. 红地符号要求

Spring Data Redis目前与Lettuce集成,作为唯一的反应式Java连接器。项目反应器用作反应性组合物库。

11.2. 使用反应式驱动程序连接到 Redis

使用 Redis 和 Spring 时的首要任务之一是通过 IoC 容器连接到存储。为此,需要 Java 连接器(或绑定)。无论您选择哪种库,都必须使用package和itsandinterfaces来处理和检索activeto Redis。​​org.springframework.data.redis.connection​​​​ReactiveRedisConnection​​​​ReactiveRedisConnectionFactory​​​​connections​

11.2.1. Redis 操作模式

Redis 可以作为独立服务器运行,使用 RedisSentinel 或在Redis 集群模式下运行。生菜支持前面提到的所有连接类型。

11.2.2.和​​ReactiveRedisConnection​​​​ReactiveRedisConnectionFactory​

​ReactiveRedisConnection​​是 Redis 通信的核心,因为它处理与 Redis 后端的通信。它还会自动将底层驱动程序异常转换为 Spring 一致的 DAO 异常层次结构,因此您无需更改任何代码即可切换连接器,因为操作语义保持不变。

​ReactiveRedisConnectionFactory​​创建活动实例。此外,工厂充当实例,这意味着,一旦声明,它们就允许您进行透明的异常翻译 - 例如,通过使用注释和AOP进行异常翻译。有关更多信息,请参阅 Spring 框架文档中的专用部分。​​ReactiveRedisConnection​​​​PersistenceExceptionTranslator​​​​@Repository​

根据基础配置,工厂可以返回新连接或现有连接(如果使用池或共享本机连接)。

使用 ais 通过 IoC 容器配置适当的连接器并将其注入 using 类的最简单方法。​​ReactiveRedisConnectionFactory​

11.2.3. 配置生菜连接器

生菜由Spring Data Redis通过软件包支持。​​org.springframework.data.redis.connection.lettuce​

您可以按如下方式设置生菜:​​ReactiveRedisConnectionFactory​

@Bean
public ReactiveRedisConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}

以下示例显示了更复杂的配置,包括 SSL 和超时,它使用:​​LettuceClientConfigurationBuilder​

@Bean
public ReactiveRedisConnectionFactory lettuceConnectionFactory() {

LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.useSsl().and()
.commandTimeout(Duration.ofSeconds(2))
.shutdownTimeout(Duration.ZERO)
.build();

return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379), clientConfig);
}

有关更详细的客户端配置调整,请参阅生菜客户端配置。

11.3. 通过 ReactiveRedisTemplate 处理对象

大多数用户可能会使用及其相应的包,。由于其丰富的功能集,该模板实际上是 Redis 模块的中心类。该模板为 Redis 交互提供了高级抽象。虽然提供了接受和返回二进制值 () 的低级方法,但模板负责序列化和连接管理,使您免于处理此类细节。​​ReactiveRedisTemplate​​​​org.springframework.data.redis.core​​​​ReactiveRedisConnection​​​​ByteBuffer​

此外,该模板还提供了操作视图(遵循 Redis 命令参考中的分组),这些视图提供了丰富的通用接口,用于处理特定类型,如下表所述:

表 12.操作视图

接口

描述

键类型操作

反应性地理操作

Redis 地理空间操作,例如 等)​​GEOADD​​​​GEORADIUS​

ReactiveHashOperations

红与人哈希操作

ReactiveHyperLogLogOperations

Redis HyperLogLog 操作,例如 (、 和其他)​​PFADD​​​​PFCOUNT​

反应式列表操作

红地符号列表操作

反应式集操作

红地集操作

反应式值操作

Redis 字符串(或值)操作

ReactiveZSetOperations

Redis zset(或排序集)操作

配置后,模板是线程安全的,可以在多个实例中重复使用。

​ReactiveRedisTemplate​​使用基于 Java 的序列化程序进行大部分操作。这意味着模板写入或读取的任何对象都将通过 or 进行序列化或反序列化。序列化上下文在构造时传递到模板,Redis 模块提供了包中可用的多个实现。有关详细信息,请参阅序列化程序。​​RedisElementWriter​​​​RedisElementReader​​​​org.springframework.data.redis.serializer​

以下示例显示了用于返回 a:​​ReactiveRedisTemplate​​​​Mono​

@Configuration
class RedisConfiguration {

@Bean
ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
return new ReactiveRedisTemplate<>(factory, RedisSerializationContext.string());
}
}
public class Example {

@Autowired
private ReactiveRedisTemplate<String, String> template;

public Mono<Long> addLink(String userId, URL url) {
return template.opsForList().leftPush(userId, url.toExternalForm());
}
}

11.4. 以字符串为中心的便利类

由于存储在 Redis 中的键和值通常为 a,因此 Redis 模块提供了一个基于字符串的扩展: 。它是集约化操作的便捷一站式解决方案。除了绑定到键之外,模板还使用基于字符串,这意味着存储的键和值是人类可读的(假设 Redis 和代码中使用相同的编码)。以下示例显示了使用:​​java.lang.String​​​​ReactiveRedisTemplate​​​​ReactiveStringRedisTemplate​​​​String​​​​String​​​​RedisSerializationContext​​​​ReactiveStringRedisTemplate​

@Configuration
class RedisConfiguration {

@Bean
ReactiveStringRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
return new ReactiveStringRedisTemplate<>(factory);
}
}
public class Example {

@Autowired
private ReactiveStringRedisTemplate redisTemplate;

public Mono<Long> addLink(String userId, URL url) {
return redisTemplate.opsForList().leftPush(userId, url.toExternalForm());
}
}

11.5. Redis 消息传递/PubSub

Spring Data 为 Redis 提供了专用的消息传递集成,在功能和命名上与 Spring Framework 中的 JMS 集成非常相似;事实上,熟悉 Spring 中 JMS 支持的用户应该感到宾至如归。

Redis 消息传递可以大致分为两个功能区域,即生产或发布以及消息的消费或订阅,因此快捷方式 pubsub(发布/订阅)。该类用于消息生成。对于异步接收,Spring Data 提供了一个专用的消息侦听器容器,用于使用消息流。 为了仅订阅优惠,剥离了使用侦听器容器的替代方案。​​ReactiveRedisTemplate​​​​ReactiveRedisTemplate​

该软件包提供了使用 Redis 消息传递的核心功能。​​org.springframework.data.redis.connection​​​​org.springframework.data.redis.listener​

11.5.1. 发送/发布消息

要发布消息,可以像其他操作一样使用低级别或高级。这两个实体都提供了一个发布方法,该方法接受需要发送的消息以及目标通道作为参数。虽然需要原始数据,允许任意对象作为消息传入:​​ReactiveRedisConnection​​​​ReactiveRedisTemplate​​​​ReactiveRedisConnection​​​​ReactiveRedisTemplate​

// send message through ReactiveRedisConnection
ByteBuffer msg = …
ByteBuffer channel = …
Mono<Long> publish = con.publish(msg, channel);

// send message through ReactiveRedisTemplate
ReactiveRedisTemplate template = …
Mono<Long> publish = template.convertAndSend("channel", "message");

11.5.2. 接收/订阅消息

在接收端,可以通过直接命名或使用模式匹配来订阅一个或多个通道。后一种方法非常有用,因为它不仅允许使用一个命令创建多个订阅,而且还允许侦听订阅时尚未创建的通道(只要它们与模式匹配)。

在低级别,将 Redis 命令映射为按模式分别按通道订阅的 offerand 方法。请注意,多个通道或模式可以用作参数。要更改订阅,只需查询频道和模式。​​ReactiveRedisConnection​​​​subscribe​​​​pSubscribe​​​​ReactiveSubscription​

Spring Data Redis 中的反应式订阅命令是非阻塞的,可以在不发出元素的情况下结束。

如上所述,一旦订阅,连接就会开始等待消息。除了添加新订阅或修改/取消现有订阅外,不能对其调用其他命令。,,,以外的命令是非法的,将导致异常。​​subscribe​​​​pSubscribe​​​​unsubscribe​​​​pUnsubscribe​

为了接收消息,需要获取消息流。请注意,订阅仅发布在该特定订阅中注册的通道和模式的消息。消息流本身是一个热序列,它产生元素而不考虑需求。确保注册足够的需求,以免耗尽消息缓冲区。

消息侦听器容器

Spring 数据提供代表用户完成转换和订阅状态管理的所有繁重工作。​​ReactiveRedisMessageListenerContainer​

​ReactiveRedisMessageListenerContainer​​充当消息侦听器容器。它用于从 Redis 通道接收消息,并公开发出应用了反序列化的通道消息的消息流。它负责注册接收消息、资源获取和释放、异常转换等。这允许您作为应用程序开发人员编写与接收消息(并对其做出反应)相关的(可能很复杂的)业务逻辑,并将样板 Redis 基础架构问题委托给框架。消息流在发布者订阅时在 Redis 中注册订阅,如果订阅被取消,则取消注册。

此外,为了最大限度地减少应用程序占用空间,允许多个侦听器共享一个连接和一个线程,即使它们不共享订阅。因此,无论应用程序跟踪多少个侦听器或通道,运行时成本在其整个生命周期中都将保持不变。此外,容器允许更改运行时配置,因此可以在应用程序运行时添加或删除侦听器,而无需重新启动。此外,容器使用延迟订阅方法,仅在需要时使用 aonly - 如果取消订阅所有侦听器,则会自动执行清理。​​ReactiveRedisMessageListenerContainer​​​​ReactiveRedisConnection​

消息侦听器容器本身不需要外部线程资源。它使用驱动程序线程来发布消息。

ReactiveRedisConnectionFactory factory = …
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory);

Flux<ChannelMessage<String, String>> stream = container.receive(ChannelTopic.of("my-channel"));

若要等待并确保正确订阅,可以使用返回 a 的方法。 由于完成了对给定主题的订阅,结果由内部发布者完成。通过拦截信号,可以同步服务器端订阅。​​receiveLater​​​​Mono<Flux<ChannelMessage>>​​​​Mono​​​​onNext​

ReactiveRedisConnectionFactory factory = …
ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory);

Mono<Flux<ChannelMessage<String, String>>> stream = container.receiveLater(ChannelTopic.of("my-channel"));

stream.doOnNext(inner -> // notification hook when Redis subscriptions are synchronized with the server)
.flatMapMany(Function.identity())
.…;
通过模板 API 订阅

如上所述,您可以直接用于订阅频道/模式。这种方法 提供简单明了但有限的解决方案,因为您在初始订阅后失去了添加订阅的选项 的。尽管如此,您仍然可以通过返回的 using 来控制消息流,例如。什么时候 完成读取,出现错误或取消时,将再次释放所有绑定的资源。​​ReactiveRedisTemplate​​​​Flux​​​​take(Duration)​

redisTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> {
// message processing ...
}).subscribe();

11.6. 响应式脚本

您可以使用反应式基础架构运行 Redis 脚本,最好通过 进行访问。​​ReactiveScriptExecutor​​​​ReactiveRedisTemplate​

public class Example {

@Autowired
private ReactiveRedisTemplate<String, String> template;

public Flux<Long> theAnswerToLife() {

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("META-INF/scripts/42.lua"));
script.setResultType(Long.class);

return reactiveTemplate.execute(script);
}
}

有关脚本命令的更多详细信息,请参阅脚本部分。

12. 瑞迪斯集群

使用 Redis集群需要 RedisServer 版本 3.0+。有关详细信息,请参阅群集教程。

12.1. 启用 Redis 集群

集群支持基于与非集群通信相同的构建块,扩展,处理与 Redis 集群的通信,并将错误转换为 Spring DAO 异常层次结构。实例是使用 创建的,必须与关联的实例一起设置,如以下示例所示:​​RedisClusterConnection​​​​RedisConnection​​​​RedisClusterConnection​​​​RedisConnectionFactory​​​​RedisClusterConfiguration​

例 5.RedisConnectionFactory Configuration for Redis 集群的示例

@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class ClusterConfigurationProperties {

/*
* spring.redis.cluster.nodes[0] = 127.0.0.1:7379
* spring.redis.cluster.nodes[1] = 127.0.0.1:7380
* ...
*/
List<String> nodes;

/**
* Get initial collection of known cluster nodes in format {@code host:port}.
*
* @return
*/
public List<String> getNodes() {
return nodes;
}

public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}

@Configuration
public class AppConfig {

/**
* Type safe representation of application.properties
*/
@Autowired ClusterConfigurationProperties clusterProperties;

public @Bean RedisConnectionFactory connectionFactory() {

return new LettuceConnectionFactory(
new RedisClusterConfiguration(clusterProperties.getNodes()));
}
}


​RedisClusterConfiguration​​​也可以通过和具有以下属性来定义:​​PropertySource​



配置属性

  • ​spring.redis.cluster.nodes​​:以逗号分隔的主机:端口对列表。
  • ​spring.redis.cluster.max-redirects​​:允许的群集重定向数。


初始配置将驱动程序库指向一组初始群集节点。实时群集重新配置产生的更改仅保留在本机驱动程序中,不会写回配置。

12.2. 使用 Redis 集群连接

如前所述,Redis 集群的行为与单节点 Redis 甚至 Sentinel 监控的主副本环境不同。这是因为自动分片将密钥映射到分布在节点上的 16384 个插槽之一。因此,涉及多个密钥的命令必须断言所有密钥映射到完全相同的插槽,以避免跨插槽错误。 单个群集节点仅提供一组专用密钥。针对一个特定服务器发出的命令仅返回该服务器提供的密钥的结果。作为一个简单的示例,请考虑命令。当颁发到群集环境中的服务器时,它仅返回请求发送到的节点提供的密钥,而不一定返回群集中的所有密钥。因此,要获取集群环境中的所有密钥,您必须从所有已知的主节点读取密钥。​​KEYS​

虽然驱动程序库处理特定密钥到相应槽服务节点的重定向,但更高级别的功能(例如跨节点收集信息或向群集中的所有节点发送命令)则包含在内。从前面的密钥示例中选取密钥,这意味着该方法选取集群中的每个主节点,并同时在每个主节点上运行命令,同时选取结果并返回累积的键集。仅请求单个节点的密钥为这些方法提供重载(例如,)。​​RedisClusterConnection​​​​keys(pattern)​​​​KEYS​​​​RedisClusterConnection​​​​keys(node, pattern)​

A可以从中获取,也可以通过使用主机和端口或节点ID来构造。​​RedisClusterNode​​​​RedisClusterConnection.clusterGetNodes​

以下示例显示了在群集中运行的一组命令:

例 6.跨群集运行命令的示例

redis-cli@127.0.0.1:7379 > cluster nodes

6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460
7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-10922
164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-16383
b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected
RedisClusterConnection connection = connectionFactory.getClusterConnnection();

connection.set("thing1", value);
connection.set("thing2", value);

connection.keys("*");

connection.keys(NODE_7379, "*");
connection.keys(NODE_7380, "*");
connection.keys(NODE_7381, "*");
connection.keys(NODE_7382, "*");

服务插槽 0 到 5460 的主节点复制到位于 7382 的副本

服务插槽 5461 到 10922 的主节点

服务插槽 10923 到 16383 的主节点

在 7379 处保存主节点副本的副本节点

请求路由到位于 7381 服务槽 12182 的节点

请求路由到位于 7379 服务插槽 5061 的节点

请求路由到 7379、7380、7381 → [事物 1、事物 2] 的节点

请求路由到位于 7379 → [thing2] 的节点

请求路由到位于 7380 → [] 的节点

请求路由到位于 7381 → [thing1] 的节点

请求路由到位于 7382 → [thing2] 的节点

当所有密钥映射到同一槽时,本机驱动程序库会自动处理跨槽请求,例如。但是,一旦不是这种情况,就会对插槽服务节点运行多个并行命令,并再次返回累积结果。这比单插槽方法的性能差,因此应谨慎使用。如有疑问,请考虑通过在大括号(如 and)中提供前缀将密钥固定到同一插槽,这两个前缀都将映射到相同的插槽编号。以下示例显示了跨槽请求处理:​​MGET​​​​RedisClusterConnection​​​​GET​​​​{my-prefix}.thing1​​​​{my-prefix}.thing2​

例 7.跨槽请求处理示例

redis-cli@127.0.0.1:7379 > cluster nodes

6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460
7bb...
RedisClusterConnection connection = connectionFactory.getClusterConnnection();

connection.set("thing1", value); // slot: 12182
connection.set("{thing1}.thing2", value); // slot: 12182
connection.set("thing2", value); // slot: 5461

connection.mGet("thing1", "{thing1}.thing2");

connection.mGet("thing1", "thing2");

与前面示例中的配置相同。

密钥映射到同一插槽 → 127.0.0.1:7381 MGET 事物1 {事物1}.thing2

密钥映射到不同的插槽,并被拆分为路由到相应节点

的单个插槽→ 127.0.0.1:7379 GET thing2

→ 127.0.0.1:7381 GET thing1

前面的示例演示了 Spring Data Redis 遵循的一般策略。请注意,某些操作可能需要将大量数据加载到内存中才能计算所需的命令。此外,并非所有跨槽请求都可以安全地移植到多个单槽请求,如果误用(例如,),则会出现错误。​​PFCOUNT​

12.3. 使用和​​RedisTemplate​​​​ClusterOperations​

有关 的常规用途、配置和用法的信息,请参阅通过RedisTemplate 处理对象部分。​​RedisTemplate​

使用任何 JSON 进行设置时要小心,因为更改 JSON 结构会对哈希槽计算产生直接影响。​​RedisTemplate#keySerializer​​​​RedisSerializers​

​RedisTemplate​​通过接口提供对特定于集群的操作的访问,可以从中获取。这允许您在群集内的单个节点上显式运行命令,同时保留为模板配置的序列化和反序列化功能。它还提供管理命令(例如)或更高级的操作(例如,重新分片)。​​ClusterOperations​​​​RedisTemplate.opsForCluster()​​​​CLUSTER MEET​

以下示例演示如何访问:​​RedisClusterConnection​​​​RedisTemplate​

例 8.访问方式​​RedisClusterConnection​​​​RedisTemplate​

ClusterOperations clusterOps = redisTemplate.opsForCluster();
clusterOps.shutdown(NODE_7379);

关闭 7379 处的节点并交叉手指有一个可以接管的副本。

13. 红地思存储库

使用 Redis 存储库,您可以在 Redis 哈希中无缝转换和存储域对象、应用自定义映射策略以及使用二级索引。

Redis 存储库至少需要 Redis Server 版本 2.8.0,并且不适用于事务。 确保使用禁用事务支持​。​​RedisTemplate​

13.1. 用法

Spring Data Redis 可让您轻松实现域实体,如以下示例所示:

例 9.示例人员实体

@RedisHash("people")
public class Person {

@Id String id;
String firstname;
String lastname;
Address address;
}

我们这里有一个非常简单的域对象。 请注意,它对其类型有注释,并且有一个属性命名为注释。 这两项负责创建用于保留哈希的实际密钥。​​@RedisHash​​​​id​​​​org.springframework.data.annotation.Id​

批注的属性以及命名的属性被视为标识符属性。 那些有注释的人比其他人更受青睐。​​@Id​​​​id​

现在要真正拥有一个负责存储和检索的组件,我们需要定义一个存储库接口,如以下示例所示:

例 10.用于保留人员实体的基本存储库接口

public interface PersonRepository extends CrudRepository<Person, String> {

}

随着我们的存储库扩展,它提供了基本的 CRUD 和查找器操作。 我们需要在两者之间将东西粘合在一起的是相应的 Spring 配置,如以下示例所示:​​CrudRepository​

例 11.JavaConfig for Redis Repository

@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

@Bean
public RedisConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}

@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

给定前面的设置,我们可以注入到组件中,如以下示例所示:​​PersonRepository​

例 12.对个人实体的访问权限

@Autowired PersonRepository repo;

public void basicCrudOperations() {

Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address("emond's field", "andor"));

repo.save(rand);

repo.findOne(rand.getId());

repo.count();

repo.delete(rand);
}

生成一个 newif 当前值 isor 重用已设置的值,并将类型的属性存储在 Redis 哈希中,其键的模式为 — 在这种情况下,可能是。​​id​​​​null​​​​id​​​​Person​​​​keyspace:id​​​​people:5d67b7e1-8640-4475-beeb-c666fab4c0e5​

使用提供的检索存储在 中的对象。​​id​​​​keyspace:id​

计算键空间中可用的实体总数,由 BY 定义。​​people​​​​@RedisHash​​​​Person​

从 Redis 中删除给定对象的键。

13.2. 对象映射基础

本节介绍了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。 请注意,本节仅适用于不使用底层数据存储的对象映射(如 JPA)的 Spring 数据模块。 此外,请务必查阅特定于存储的部分,了解特定于存储的对象映射,例如索引、自定义列或字段名称等。

Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。 这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例。
  2. 实例填充以具体化所有公开的属性。

13.2.1. 对象创建

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  2. 如果只有一个构造函数,则使用它。
  3. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  4. 如果类型是 Java,则使用规范构造函数。Record
  5. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像要填充属性一样执行,包括映射中的所有自定义(不同的数据存储列或字段名称等)。 这还需要类文件中可用的参数名称信息或构造函数上存在的枚举注释。​​@ConstructorProperties​

值解析可以通过使用特定于商店的SpEL表达式使用Spring Framework的值注释来自定义。 有关更多详细信息,请参阅商店特定映射部分。​​@Value​

对象创建内部

为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,该工厂类将直接调用域类构造函数。 即对于此示例类型:

class Person {
Person(String firstname, String lastname) { … }
}

我们将在运行时创建一个语义等同于此工厂类的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}

这为我们提供了大约 10% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 它不能是私有类
  • 它不能是非静态内部类
  • 它不能是 CGLib 代理类
  • Spring Data 要使用的构造函数不得是私有的

如果这些条件中的任何一个匹配,Spring 数据将通过反射回退到实体实例化。

13.2.2. 财产人口

创建实体的实例后,Spring Data 将填充该类的所有剩余持久属性。 除非已由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充标识符属性以允许解析循环对象引用。 之后,将在实体实例上设置构造函数尚未填充的所有非瞬态属性。 为此,我们使用以下算法:

  1. 如果属性是不可变的,但公开了 amethod(见下文),我们使用该方法创建一个具有新属性值的新实体实例。with…with…
  2. 如果定义了属性访问(即通过 getter 和 setter 的访问),我们将调用 setter 方法。
  3. 如果属性是可变的,我们直接设置字段。
  4. 如果属性是不可变的,我们将使用持久性操作要使用的构造函数(请参阅对象创建)来创建实例的副本。
  5. 默认情况下,我们直接设置字段值。

属性人口内部

与对象构造中的优化类似,我们还使用 Spring 数据运行时生成的访问器类与实体实例进行交互。

class Person {

private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;

Person() {
this.id = null;
}

Person(Long id, String firstname, String lastname) {
// Field assignments
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}

void setLastname(String lastname) {
this.lastname = lastname;
}
}

例 13.生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

private static final MethodHandle firstname;

private Person person;

public void setProperty(PersistentProperty property, Object value) {

String name = property.getName();

if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("id".equals(name)) {
this.person = person.withId((Long) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}
}
}

PropertyAccessor 保存基础对象的可变实例。这是为了启用其他不可变属性的突变。

默认情况下,Spring 数据使用字段访问来读取和写入属性值。根据字段的可见性规则,用于与字段进行交互。​​private​​​​MethodHandles​

该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。调用创建一个新对象。所有后续突变都将发生在新实例中,而之前的突变保持不变。​​withId(…)​​​​withId(…)​​​​Person​

使用属性访问允许直接调用方法,而无需使用。​​MethodHandles​

这为我们提供了大约 25% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 类型不得驻留在默认值或包下。java
  • 类型及其构造函数必须是public
  • 内部类的类型必须是。static
  • 使用的 Java 运行时必须允许在原始文件中声明类。Java 9 及更高版本施加了某些限制。ClassLoader

默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看一下以下实体:

例 14。示例实体

class Person {

private final @Id Long id;
private final String firstname, lastname;
private final LocalDate birthday;
private final int age;

private String comment;
private @AccessType(Type.PROPERTY) String remarks;

static Person of(String firstname, String lastname, LocalDate birthday) {

return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}

Person(Long id, String firstname, String lastname, LocalDate birthday, int age) {

this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}

void setRemarks(String remarks) {
this.remarks = remarks;
}
}

标识符属性是最终的,但在构造函数中设置为。 该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。 创建新实例时,原始实例保持不变。 相同的模式通常应用于存储管理的其他属性,但可能必须更改持久性操作。 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,设置属性将转换为创建应用了新标识符值的新实例。​​null​​​​withId(…)​​​​Person​

和属性是普通的不可变属性,可能通过 getter 公开。​​firstname​​​​lastname​

属性是不可变的,但派生自属性。 按照所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。 即使意图是首选计算,重要的是此构造函数也采用 as 参数(可能忽略它),否则属性填充步骤将尝试设置 age 字段并失败,因为它是不可变的并且不存在任何方法。​​age​​​​birthday​​​​age​​​​with…​

属性是可变的,通过直接设置其字段来填充。​​comment​

属性是可变的,并通过直接设置 thefield 或通过调用 setter 方法来填充​​remarks​​​​comment​

该类公开用于创建对象的工厂方法和构造函数。 这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过构造函数消除歧义的需要。 相反,属性的默认值在工厂方法中处理。 如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用 注释。​​@PersistenceCreator​​​​@PersistenceCreator​

13.2.3. 一般建议

  • 尝试坚持使用不可变对象 - 不可变对象很容易创建,因为具体化对象只需调用其构造函数即可。 此外,这可以避免域对象充斥着允许客户端代码操作对象状态的 setter 方法。 如果需要这些,最好使它们受到包保护,以便只能由有限数量的共存类型调用它们。 仅构造函数具体化比属性填充快 30%。
  • 提供全参数构造函数 — 即使不能或不想将实体建模为不可变值,提供将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
  • 使用工厂方法而不是重载的构造函数来避免​​@PersistenceCreator​​ — 使用最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等内容。 这是一种既定模式,而是使用静态工厂方法来公开 all-args 构造函数的这些变体。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束
  • 对于要生成的标识符,仍将最终字段与全参数持久性构造函数(首选)或​​with​​...方法
  • 使用 Lombok 避免样板代码 — 由于持久性操作通常需要构造函数获取所有参数,因此它们的声明变成了对字段分配的繁琐重复,而使用 Lombok 可以最好地避免这些参数。@AllArgsConstructor
覆盖属性

Java允许灵活设计域类,其中子类可以定义已在其超类中声明具有相同名称的属性。 请考虑以下示例:

public class SuperType {

private CharSequence field;

public SuperType(CharSequence field) {
this.field = field;
}

public CharSequence getField() {
return this.field;
}

public void setField(CharSequence field) {
this.field = field;
}
}

public class SubType extends SuperType {

private String field;

public SubType(String field) {
super(field);
this.field = field;
}

@Override
public String getField() {
return this.field;
}

public void setField(String field) {
this.field = field;

// optional
super.setField(field);
}
}

这两个类都使用可赋值类型定义。 根据类设计,使用构造函数可能是唯一的默认设置方法。 或者,调用二传手可以设置。 所有这些机制在某种程度上都会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 如果类型不可分配,则 Spring Data 将跳过超类型属性。 也就是说,重写属性的类型必须可分配给其超类型属性类型才能注册为重写,否则超类型属性被视为暂时性属性。 我们通常建议使用不同的属性名称。​​field​​​​SubType​​​​SuperType.field​​​​SuperType.field​​​​super.setField(…)​​​​field​​​​SuperType​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 不能使用,因为如果不对 setter 实现进行任何进一步的假设,通常无法设置超级属性。​​@AccessType(PROPERTY)​

13.2.4. Kotlin 支持

Spring Data 调整了 Kotlin 的细节,以允许对象创建和更改。

Kotlin 对象创建

Kotlin 类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果存在带有注释的构造函数,则使用它。@PersistenceCreator
  2. 如果类型是Kotlin 数据 cass,则使用主构造函数。
  3. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  4. 如果只有一个构造函数,则使用它。
  5. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  6. 如果类型是 Java,则使用规范构造函数。Record
  7. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

请考虑以下类:​​data​​​​Person​

data class Person(val id: String, val name: String)

上面的类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数来自定义此类并对其进行注释以指示构造函数首选项:​​@PersistenceCreator​

data class Person(var id: String, val name: String) {

@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}

Kotlin 通过允许在未提供参数时使用默认值来支持参数可选性。 当 Spring Data 检测到参数默认值的构造函数时,如果数据存储不提供值(或只是返回),则这些参数将保留为不存在,以便 Kotlin 可以应用参数默认值。请考虑以下应用参数默认值的类​​null​​​​name​

data class Person(var id: String, val name: String = "unknown")

每次参数不是结果的一部分或其值是时,则默认为。​​name​​​​null​​​​name​​​​unknown​

Kotlin 数据类的属性填充

在 Kotlin 中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。 请考虑以下类:​​data​​​​Person​

data class Person(val id: String, val name: String)

此类实际上是不可变的。 它允许在 Kotlin 生成方法时创建新实例,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。​​copy(…)​

Kotlin 覆盖属性

Kotlin 允许声明属性覆盖以更改子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
SuperType(field) {
}

这种安排呈现两个具有名称的属性。 Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。 实际上,代码如下所示:​​field​

public class SuperType {

private int field;

public SuperType(int field) {
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}

public final class SubType extends SuperType {

private int field;

public SubType(int field) {
super(field);
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}

吉特和二传手仅发病,不发病。 在这种安排中,使用构造函数是唯一要设置的默认方法。 可以添加方法toto setvia,但不属于支持的约定。 属性重写在某种程度上会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 我们通常建议使用不同的属性名称。​​SubType​​​​SubType.field​​​​SuperType.field​​​​SuperType.field​​​​SubType​​​​SuperType.field​​​​this.SuperType.field = …​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 使用不能使用,因为无法设置超级属性。@AccessType(PROPERTY)

13.3. 对象到哈希映射

Redis 存储库支持将对象保存到哈希。 这需要由 a 完成的对象到哈希转换。 默认实现用于将属性值映射到 Redis 本机和从 Redis 本机映射属性值。​​RedisConverter​​​​Converter​​​​byte[]​

给定前面部分中的类型,默认映射如下所示:​​Person​

_class = org.example.Person                 
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
address.city = emond's field
address.country = andor

该属性包含在根级别以及任何嵌套接口或抽象类型中。​​_class​

简单属性值按路径映射。

复杂类型的属性由其点路径映射。

默认映射规则如下表所示:

表 13.默认映射规则

类型

样本

映射值

简单类型
(例如字符串)

字符串名字 = “rand”;

名字 = “兰德”

字节数组 (​​byte[]​​)

byte[] image = “rand”.getBytes();

图像 = “兰德”

复杂类型
(例如地址)

地址地址 = 新地址(“埃蒙德字段”);

地址城市 = “埃蒙德的田野”

简单类型列表

列表<字符串>昵称 = asList(“龙重生”、“猥亵”);

昵称。[0] = “龙重生”,
昵称。[1] = “卢斯·特林”

简单类型的地图

Map<String, String> atts = asMap({“eye-color”, “grey”}, {“...

阿茨。[眼睛颜色] = “灰色”,
atts。[头发颜色] = “...

复杂类型列表

列表<地址> 地址 = asList(新地址(“em...

地址。[0].city = “埃蒙德的字段”,
地址。[1].城市 =“...

复杂类型地图

Map<String, Address> address = asMap({“home”, new Address(“em...

地址。[home].city = “Emond's field”,
地址。[工作].城市 =“...

由于平面表示结构,Map 键需要是简单的类型,例如 asor。​​String​​​​Number​

可以通过注册相应的映射来自定义映射行为。 这些转换器可以负责从单到单以及转换。 第一个适用于(例如)将复杂类型转换为(例如)仍使用默认映射哈希结构的二进制 JSON 表示形式。 第二个选项提供对结果哈希的完全控制。​​Converter​​​​RedisCustomConversions​​​​byte[]​​​​Map<String,byte[]>​

将对象写入 Redis 哈希会从哈希中删除内容并重新创建整个哈希,因此尚未映射的数据将丢失。

以下示例显示了两个示例字节数组转换器:

例 15。采样字节[] 转换器

@WritingConverter
public class AddressToBytesConverter implements Converter<Address, byte[]> {

private final Jackson2JsonRedisSerializer<Address> serializer;

public AddressToBytesConverter() {

serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
serializer.setObjectMapper(new ObjectMapper());
}

@Override
public byte[] convert(Address value) {
return serializer.serialize(value);
}
}

@ReadingConverter
public class BytesToAddressConverter implements Converter<byte[], Address> {

private final Jackson2JsonRedisSerializer<Address> serializer;

public BytesToAddressConverter() {

serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
serializer.setObjectMapper(new ObjectMapper());
}

@Override
public Address convert(byte[] value) {
return serializer.deserialize(value);
}
}

使用前面的字节数组生成类似于以下内容的输出:​​Converter​

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
address = { city : "emond's field", country : "andor" }

以下示例显示了转换器的两个示例:​​Map​

例 16。示例映射<字符串,字节[]>转换器

@WritingConverter
public class AddressToMapConverter implements Converter<Address, Map<String,byte[]>> {

@Override
public Map<String,byte[]> convert(Address source) {
return singletonMap("ciudad", source.getCity().getBytes());
}
}

@ReadingConverter
public class MapToAddressConverter implements Converter<Map<String, byte[]>, Address> {

@Override
public Address convert(Map<String,byte[]> source) {
return new Address(new String(source.get("ciudad")));
}
}

使用前面的 Map生成类似于以下内容的输出:​​Converter​

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
ciudad = "emond's field"

自定义转化对索引解析没有影响。即使对于自定义转换类型,仍会创建二级索引。

13.3.1. 自定义类型映射

如果要避免将整个 Java 类名编写为类型信息,而是希望使用键,则可以对要持久化的实体类使用注释。 如果您需要进一步自定义映射,请查看TypeInformationMapper界面。 可以在 上配置该接口的实例,可以在其上配置。​​@TypeAlias​​​​DefaultRedisTypeMapper​​​​MappingRedisConverter​

下面的示例演示如何为实体定义类型别名:

例 17.为实体定义​​@TypeAlias​

@TypeAlias("pers")
class Person {

}

生成的文档包含字段中的值。​​pers​​​​_class​

配置自定义类型映射

以下示例演示如何配置自定义:​​RedisTypeMapper​​​​MappingRedisConverter​

例 18。配置自定义通过 Spring Java Config​​RedisTypeMapper​

class CustomRedisTypeMapper extends DefaultRedisTypeMapper {
//implement custom type mapping here
}
@Configuration
class SampleRedisConfiguration {

@Bean
public MappingRedisConverter redisConverter(RedisMappingContext mappingContext,
RedisCustomConversions customConversions, ReferenceResolver referenceResolver) {

MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver,
customTypeMapper());

mappingRedisConverter.setCustomConversions(customConversions);

return mappingRedisConverter;
}

@Bean
public RedisTypeMapper customTypeMapper() {
return new CustomRedisTypeMapper();
}
}

13.4. 键空间

密钥空间定义用于为 Redis 哈希创建实际密钥的前缀。 默认情况下,前缀设置为 。 您可以通过在聚合根级别上设置或通过设置编程配置来更改此默认值。 但是,带注释的密钥空间将取代任何其他配置。​​getClass().getName()​​​​@RedisHash​

以下示例显示如何使用注释设置密钥空间配置:​​@EnableRedisRepositories​

例 19。密钥空间设置方式​​@EnableRedisRepositories​

@Configuration
@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class)
public class ApplicationConfig {

//... RedisConnectionFactory and RedisTemplate Bean definitions omitted

public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {

@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
}
}
}

下面的示例演示如何以编程方式设置密钥空间:

例 20。编程密钥空间设置

@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

//... RedisConnectionFactory and RedisTemplate Bean definitions omitted

@Bean
public RedisMappingContext keyValueMappingContext() {
return new RedisMappingContext(
new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration()));
}

public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {

@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
}
}
}

13.5. 二级索引

二级索引用于启用基于本机 Redis 结构的查找操作。 值在每次保存时写入相应的索引,并在对象被删除或过期时删除。

13.5.1. Simple Property Index

Given the sample entity shown earlier, we can create an index for by annotating the property with , as shown in the following example:​​Person​​​​firstname​​​​@Indexed​

例 21。注释驱动的索引

@RedisHash("people")
public class Person {

@Id String id;
@Indexed String firstname;
String lastname;
Address address;
}

索引是为实际属性值构建的。 保存两个人(例如,“rand”和“aviendha”)会导致设置类似于以下内容的索引:

SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e
SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

也可以在嵌套元素上建立索引。 假设具有注释的属性。 在这种情况下,onceis 不是,我们为每个城市都有 Sets,如以下示例所示:​​Address​​​​city​​​​@Indexed​​​​person.address.city​​​​null​

SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

此外,编程设置允许您在映射键和列表属性上定义索引,如以下示例所示:

@RedisHash("people")
public class Person {

// ... other properties omitted

Map<String,String> attributes;
Map<String Person> relatives;
List<Address> addresses;
}

​SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e​

​SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e​

​SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e​

无法在引用上解析索引。

与键空间一样,您可以配置索引,而无需注释实际的域类型,如以下示例所示:

例 22。使用@EnableRedisRepositories进行索引设置

@Configuration
@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class)
public class ApplicationConfig {

//... RedisConnectionFactory and RedisTemplate Bean definitions omitted

public static class MyIndexConfiguration extends IndexConfiguration {

@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
}
}
}

同样,与键空间一样,您可以通过编程方式配置索引,如以下示例所示:

例 23。程序化索引设置

@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

//... RedisConnectionFactory and RedisTemplate Bean definitions omitted

@Bean
public RedisMappingContext keyValueMappingContext() {
return new RedisMappingContext(
new MappingConfiguration(
new KeyspaceConfiguration(), new MyIndexConfiguration()));
}

public static class MyIndexConfiguration extends IndexConfiguration {

@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
}
}
}

13.5.2. 地理空间索引

假设类型包含保存特定地址的地理坐标的类型属性。 通过对属性进行注释,Spring Data Redis 使用 Rediscommand 添加这些值,如以下示例所示:​​Address​​​​location​​​​Point​​​​@GeoIndexed​​​​GEO​

@RedisHash("people")
public class Person {

Address address;

// ... other properties omitted
}

public class Address {

@GeoIndexed Point location;

// ... other properties omitted
}

public interface PersonRepository extends CrudRepository<Person, String> {

List<Person> findByAddressLocationNear(Point point, Distance distance);
List<Person> findByAddressLocationWithin(Circle circle);
}

Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));

repository.save(rand);

repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200));

对嵌套属性的查询方法声明,usingand。​​Point​​​​Distance​

对嵌套属性的查询方法声明,用于在内部搜索。​​Circle​

​GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e​

​GEORADIUS people:address:location 15.0 37.0 200.0 km​

在前面的示例中,经度和纬度值是通过使用对象作为成员名称来存储的。 查找器方法允许使用oforcomb来查询这些值。​​GEOADD​​​​id​​​​Circle​​​​Point, Distance​

不可能与其他标准结合使用。​​near​​​​within​

13.6. 按示例查询

13.6.1. 简介

本章介绍按示例查询并说明如何使用它。

按示例查询 (QBE) 是一种用户友好的查询技术,具有简单的界面。 它允许创建动态查询,并且不需要您编写包含字段名称的查询。 事实上,按示例查询根本不要求您使用特定于存储的查询语言编写查询。

13.6.2. 用法

按示例查询 API 由四个部分组成:

  • 探测器:具有填充字段的域对象的实际示例。
  • ​ExampleMatcher​​:包含有关如何匹配特定字段的详细信息。 它可以在多个示例中重复使用。ExampleMatcher
  • ​Example​​:由探头和探头组成。 它用于创建查询。ExampleExampleMatcher
  • ​FetchableFluentQuery​​:提供流畅的 API,允许进一步自定义从 . 使用流畅的 API 可以为查询指定排序投影和结果处理。FetchableFluentQueryExample

按示例查询非常适合多种用例:

  • 使用一组静态或动态约束查询数据存储。
  • 频繁重构域对象,无需担心中断现有查询。
  • 独立于基础数据存储 API 工作。

按示例查询也有几个限制:

  • 不支持嵌套或分组属性约束,例如。firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。

在开始使用按示例查询之前,您需要有一个域对象。 首先,请为存储库创建一个接口,如以下示例所示:

例 24。示例人员对象

public class Person {

@Id
private String id;
private String firstname;
private String lastname;
private Address address;

// … getters and setters omitted
}

前面的示例显示了一个简单的域对象。 您可以使用它来创建. 默认情况下,将忽略具有值的字段,并使用特定于存储的默认值匹配字符串。​​Example​​​​null​

将属性包含在“按示例查询”条件中基于可空性。 始终包含使用基元类型 (,, ...) 的属性,除非ExampleMatcher忽略属性路径​。​​int​​​​double​

可以使用工厂方法或使用ExampleMatcher.is 不可变来构建示例。 下面的清单显示了一个简单的示例:​​of​​​​Example​

例 25。简单示例

Person person = new Person();                         
person.setFirstname("Dave");

Example<Person> example = Example.of(person);

创建域对象的新实例。

设置要查询的属性。

创建。​​Example​

您可以使用存储库运行示例查询。 为此,请让您的存储库界面扩展。 以下清单显示了界面的摘录:​​QueryByExampleExecutor<T>​​​​QueryByExampleExecutor​

例 26。这​​QueryByExampleExecutor​

public interface QueryByExampleExecutor<T> {

<S extends T> S findOne(Example<S> example);

<S extends T> Iterable<S> findAll(Example<S> example);

// … more functionality omitted.
}

13.6.3. 匹配器示例

示例不限于默认设置。 您可以使用 指定自己的字符串匹配、null 处理和特定于属性的设置的默认值,如以下示例所示:​​ExampleMatcher​

例 27。具有自定义匹配的示例匹配器

Person person = new Person();                          
person.setFirstname("Dave");

ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("lastname")
.withIncludeNullValues()
.withStringMatcher(StringMatcher.ENDING);

Example<Person> example = Example.of(person, matcher);

创建域对象的新实例。

设置属性。

创建 anto 期望所有值都匹配。 即使没有进一步的配置,它也可以在此阶段使用。​​ExampleMatcher​

构造一个新忽略属性路径。​​ExampleMatcher​​​​lastname​

构造一个新以忽略属性路径并包含空值。​​ExampleMatcher​​​​lastname​

构造一个 newto 忽略属性路径,以包含 null 值,并执行后缀字符串匹配。​​ExampleMatcher​​​​lastname​

创建一个基于域对象和配置的 new。​​Example​​​​ExampleMatcher​

默认情况下,期望探测器上设置的所有值都匹配。 如果要获取与隐式定义的任何谓词匹配的结果,请使用。​​ExampleMatcher​​​​ExampleMatcher.matchingAny()​

您可以为单个属性指定行为(例如“名字”和“姓氏”,或者对于嵌套属性,可以指定“address.city”)。 您可以使用匹配选项和区分大小写来调整它,如以下示例所示:

例 28。配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}

配置匹配器选项的另一种方法是使用 lambda(在 Java 8 中引入)。 此方法创建一个回调,要求实现者修改匹配器。 您无需返回匹配器,因为配置选项保存在匹配器实例中。 以下示例显示了使用 lambda 的匹配器:

例 29。使用 lambda 配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}

使用配置的合并视图创建的查询。 可以在级别设置默认匹配设置,而可以将单个设置应用于特定属性路径。 设置的设置由属性路径设置继承,除非显式定义它们。 属性修补程序上的设置具有比默认设置更高的优先级。 下表描述了各种设置的范围:​​Example​​​​ExampleMatcher​​​​ExampleMatcher​​​​ExampleMatcher​

表 14.设置的范围​​ExampleMatcher​

设置

范围

空处理

​ExampleMatcher​

字符串匹配

​ExampleMatcher​​和属性路径

忽略属性

属性路径

区分大小写

​ExampleMatcher​​和属性路径

价值转型

属性路径

13.6.4. 流畅的接口

​QueryByExampleExecutor​​提供了另一种方法,到目前为止我们没有提到: 与其他方法一样,它执行从 . 但是,使用第二个参数,您可以控制该执行的某些方面,否则无法动态控制。 为此,可以在第二个参数中调用各种方法。用于指定结果的排序。用于指定要将结果转换为的类型。限制查询的属性。,,,,,,,,并定义获得的结果类型以及当可用结果数超过预期数时查询的行为方式。​​<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)​​​​Example​​​​FetchableFluentQuery​​​​sortBy​​​​as​​​​project​​​​first​​​​firstValue​​​​one​​​​oneValue​​​​all​​​​page​​​​stream​​​​count​​​​exists​

例 30。使用流畅的 API 获取可能许多结果中的最后一个,按姓氏排序。

Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);

13.6.5. 运行示例

以下示例对存储库使用“按示例查询”:

例 31。使用存储库按示例查询

interface PersonRepository extends QueryByExampleExecutor<Person> {
}

class PersonService {

@Autowired PersonRepository personRepository;

List<Person> findPeople(Person probe) {
return personRepository.findAll(Example.of(probe));
}
}

Redis 存储库及其二级索引支持 Spring Data 的“按示例查询”功能的子集。 特别是,仅使用精确值、区分大小写和非 null 值来构造查询。

二级索引使用基于集合的操作(设置交集、设置联合)来确定匹配键。向查询添加未编制索引的属性不会返回任何结果,因为不存在索引。按示例查询支持检查索引配置,以仅包含索引涵盖的查询中的属性。这是为了防止意外包含非索引属性。

不区分大小写的查询和不受支持的实例在运行时被拒绝。​​StringMatcher​

以下列表显示了支持的“按示例查询”选项:

  • 区分大小写,精确匹配简单属性和嵌套属性
  • 任意/所有匹配模式
  • 条件值的值转换
  • 从标准中排除值null

以下列表显示了“按示例查询”不支持的属性:

  • 不区分大小写的匹配
  • 正则表达式,前缀/包含/后缀字符串匹配
  • 查询关联、集合和类似映射的属性
  • 包含标准中的值null
  • ​findAll​​带排序

13.7. 生存时间

存储在 Redis 中的对象可能仅在一定时间内有效。 这对于在 Redis 中持久化生存期较短的对象特别有用,而无需在对象生命周期结束时手动删除它们。 可以使用和使用(请参阅密钥空间)设置以秒为单位的过期时间。​​@RedisHash(timeToLive=…)​​​​KeyspaceSettings​

可以通过对数值属性或方法使用注释来设置更灵活的过期时间。 但是,不要同时应用于同一类中的方法和属性。 下面的示例演示对属性和方法的注释:​​@TimeToLive​​​​@TimeToLive​​​​@TimeToLive​

例 32。过期时间

public class TimeToLiveOnProperty {

@Id
private String id;

@TimeToLive
private Long expiration;
}

public class TimeToLiveOnMethod {

@Id
private String id;

@TimeToLive
public long getTimeToLive() {
return new Random().nextLong();
}
}

对属性进行注释会显式保留 Redis 的实际值。-1 表示对象没有关联的过期时间。​​@TimeToLive​​​​TTL​​​​PTTL​

存储库实现可确保通过以下方式订阅Redis 密钥空间通知。​​RedisMessageListenerContainer​

当过期设置为正值时,将运行相应的命令。 除了保留原始副本外,虚拟副本还保留在 Redis 中,并设置为在原始副本后五分钟过期。 这样做是为了使存储库支持能够发布,每当密钥过期时,将过期的值保存在 Spring 中,即使原始值已被删除。 在使用 Spring Data Redis 存储库的所有已连接应用程序上都会收到到期事件。​​EXPIRE​​​​RedisKeyExpiredEvent​​​​ApplicationEventPublisher​

默认情况下,初始化应用程序时禁用密钥到期侦听器。 启动模式可以调整为使用应用程序启动侦听器,或者在首次插入带有 TTL 的实体时启动侦听器。 有关可能的值,请参阅启用密钥空间事件。​​@EnableRedisRepositories​​​​RedisKeyValueAdapter​

保存过期域对象的副本以及密钥。​​RedisKeyExpiredEvent​

延迟或禁用过期事件侦听器启动影响发布。 禁用的事件侦听器不会发布过期事件。 由于侦听器初始化延迟,延迟启动可能会导致事件丢失。​​RedisKeyExpiredEvent​

密钥空间通知消息侦听器会更改 Redis 中的设置(如果尚未设置)。 现有设置不会被覆盖,因此您必须正确设置这些设置(或将其留空)。 请注意,在 AWS ElastiCache 上禁用了此功能,启用侦听器会导致错误。 若要变通解决此问题,请将参数设置为空字符串。 这会阻止命令使用。​​notify-keyspace-events​​​​CONFIG​​​​keyspaceNotificationsConfigParameter​​​​CONFIG​

Redis 发布/订阅消息不是持久性的。 如果密钥在应用程序关闭时过期,则不会处理过期事件,这可能会导致二级索引包含对过期对象的引用。

​@EnableKeyspaceEvents(shadowCopy = OFF)​​​禁用幻像副本的存储并减小 Redis 中的数据大小,将仅包含过期的密钥。​​RedisKeyExpiredEvent​​​​id​

13.8. 持久引用

标记属性允许存储简单的键引用,而不是将值复制到哈希本身。 从 Redis 加载时,引用会自动解析并映射回对象,如以下示例所示:​​@Reference​

例 33。示例属性参考

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

引用存储被引用对象的整个键 ()。​​keyspace:id​

保存引用对象时,不会保留引用的对象。 必须单独保留对引用对象的更改,因为仅存储引用。 不会解析在引用类型的属性上设置的索引。

13.9. 持久化部分更新

在某些情况下,您不需要加载和重写整个实体,只是为了在其中设置一个新值。 上次活动时间的会话时间戳可能是您想要更改一个属性的情况。允许您对现有对象进行定义和操作,同时负责更新实体本身和索引结构的潜在过期时间。 以下示例显示了部分更新:​​PartialUpdate​​​​set​​​​delete​

例 34。示例部分更新

PartialUpdate<Person> update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.set("firstname", "mat")
.set("address.city", "emond's field")
.del("age");

template.update(update);

update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.set("address", new Address("caemlyn", "andor"))
.set("attributes", singletonMap("eye-color", "grey"));

template.update(update);

update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.refreshTtl(true);
.set("expiration", 1000);

template.update(update);

将简单属性设置为。​​firstname​​​​mat​

将简单的“address.city”属性设置为“emond's field”,而无需传入整个对象。 注册自定义转换时,此操作不起作用。

删除该属性。​​age​

设置复杂属性。​​address​

设置值映射,这将删除以前存在的映射并将值替换为给定的值。

更改生存时间时自动更新服务器过期时间。

更新复杂对象以及映射(或其他集合)结构需要与 Redis 进一步交互以确定现有值,这意味着重写整个实体可能会更快。

13.10. 查询和查询方法

查询方法允许从方法名称自动派生简单的查找器查询,如以下示例所示:

例 35。示例存储库查找器方法

public interface PersonRepository extends CrudRepository<Person, String> {

List<Person> findByFirstname(String firstname);
}

请确保查找器方法中使用的属性已设置为索引。

Redis 存储库的查询方法仅支持对具有分页的实体和实体集合的查询。

使用派生查询方法可能并不总是足以对要运行的查询进行建模。提供了对索引结构甚至自定义索引的实际匹配的更多控制。 为此,请提供返回单个值或值集的 a,如以下示例所示:​​RedisCallback​​​​RedisCallback​​​​Iterable​​​​id​

例 36。使用 RedisCallback 的样本查找器

String user = //...

List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>() {

public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
return connection
.sMembers("sessions:securityContext.authentication.principal.username:" + user);
}}, RedisSession.class);

下表概述了 Redis 支持的关键字,以及包含该关键字的方法实质上转换为什么:

表 15.方法名称中支持的关键字

关键词

样本

红人代码段

​And​

​findByLastnameAndFirstname​

​SINTER …:firstname:rand …:lastname:al’thor​

​Or​

​findByLastnameOrFirstname​

​SUNION …:firstname:rand …:lastname:al’thor​

​Is, Equals​

​findByFirstname​​​, , ​​findByFirstnameIs​​​​findByFirstnameEquals​

​SINTER …:firstname:rand​

​IsTrue​

​FindByAliveIsTrue​

​SINTER …:alive:1​

​IsFalse​

​findByAliveIsFalse​

​SINTER …:alive:0​

​Top,First​

​findFirst10ByFirstname​​​,​​findTop5ByFirstname​

13.10.1. 排序查询方法结果

Redis 存储库允许使用各种方法来定义排序顺序。 Redis 本身在检索哈希或集合时不支持动态排序。 因此,Redis 存储库查询方法构造 a 在返回结果之前应用于结果。 让我们看一下以下示例:​​Comparator​​​​List​

例 37。对查询结果进行排序

interface PersonRepository extends RedisRepository<Person, String> {

List<Person> findByFirstnameOrderByAgeDesc(String firstname);

List<Person> findByFirstname(String firstname, Sort sort);
}

从方法名称派生的静态排序。

使用方法参数进行动态排序。

13.11. 集群上运行的 Redis 存储库

您可以在集群 Redis 环境中使用 Redis 存储库支持。 有关配置详细信息,请参阅“Redis 集群”部分。 尽管如此,还必须进行一些额外的配置,因为默认密钥分发将实体和二级索引分布在整个集群及其插槽中。​​ConnectionFactory​

下表显示了集群上数据的详细信息(基于前面的示例):

钥匙

类型


节点

人员:E2C7DCEE-B8CD-4424-883E-736CE564363E

哈希的 ID

15171

127.0.0.1:7381

员工:A9D4B3A0-50D3-4538-A2FC-F7FC2581EE56

哈希的 ID

7373

127.0.0.1:7380

人:名字:兰德

指数

1700

127.0.0.1:7379

某些命令(如 and)只能在所有涉及的密钥映射到同一插槽时在服务器端处理。 否则,必须在客户端完成计算。 因此,将密钥空间固定到单个插槽很有用,这样就可以立即使用 Redis 服务器端计算。 下表显示了执行此操作时发生的情况(请注意插槽列中的更改和节点列中的端口值):​​SINTER​​​​SUNION​

钥匙

类型


节点

{people}:e2c7dcee-b8cd-4424-883e-736ce564363e

哈希的 ID

2399

127.0.0.1:7379

{人}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

哈希的 ID

2399

127.0.0.1:7379

{人}:名字:兰德

指数

2399

127.0.0.1:7379

使用 Redis 群集时,通过使用特定插槽来定义和固定密钥空间。​​@RedisHash("{yourkeyspace}")​

13.12. CDI 集成

存储库接口的实例通常由容器创建,在使用 Spring 数据时,Spring 是最自然的选择。 Spring 提供了创建 Bean 实例的复杂性。 Spring Data Redis 附带了一个自定义的 CDI 扩展,允许您在 CDI 环境中使用存储库抽象。 扩展是 JAR 的一部分,因此,要激活它,请将 Spring Data Redis JAR 放入您的类路径中。

然后,您可以通过为 and 实现 CDI 创建器来设置基础结构,如以下示例所示:​​RedisConnectionFactory​​​​RedisOperations​

class RedisOperationsProducer {


@Produces
RedisConnectionFactory redisConnectionFactory() {

LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration());
connectionFactory.afterPropertiesSet();

return connectionFactory;
}

void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception {

if (redisConnectionFactory instanceof DisposableBean) {
((DisposableBean) redisConnectionFactory).destroy();
}
}

@Produces
@ApplicationScoped
RedisOperations<byte[], byte[]> redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) {

RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();

return template;
}

}

必要的设置可能会有所不同,具体取决于您的 JavaEE 环境。

Spring Data Redis CDI 扩展将所有可用的存储库作为 CDI bean 进行拾取,并在容器请求存储库类型的 Bean 时为 Spring 数据存储库创建代理。 因此,获取 Spring 数据存储库的实例是声明属性的问题,如以下示例所示:​​@Injected​

class RepositoryClient {

@Inject
PersonRepository repository;

public void businessMethod() {
List<Person> people = repository.findAll();
}
}

Redis 存储库需要沙粒实例。 如果未找到提供的 bean,则这些 bean 由 Spring Data CDI 扩展创建和管理。 但是,您可以提供自己的 bean 来配置 and 的特定属性。​​RedisKeyValueAdapter​​​​RedisKeyValueTemplate​​​​RedisKeyValueAdapter​​​​RedisKeyValueTemplate​

13.13. Redis 存储库剖析

Redis 作为存储本身提供了一个非常狭窄的低级 API,将更高级别的功能(如二级索引和查询操作)留给用户。

本节提供存储库抽象发出的命令的更详细视图,以便更好地了解潜在的性能影响。

请考虑以下实体类作为所有操作的起点:

例 38。示例实体

@RedisHash("people")
public class Person {

@Id String id;
@Indexed String firstname;
String lastname;
Address hometown;
}

public class Address {

@GeoIndexed Point location;
}

13.13.1. 插入新的

repository.save(new Person("rand", "al'thor"));
HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9" "firstname" "rand" "lastname" "al'thor" 
SADD "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9"
SADD "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9"
SADD "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand"

将平展的条目另存为哈希。

将 <1> 中写入的哈希的键添加到同一键空间中实体的帮助程序索引中。

将用 <2> 编写的哈希键添加到具有属性值的名字的二级索引中。

将索引 <3> 添加到要进入的帮助程序结构集中,以跟踪要在删除/更新时清理的索引。

13.13.2. 替换现有的

repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor"));
DEL       "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"                           
HMSET "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor"
SADD "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"
SMEMBERS "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"
TYPE "people:firstname:rand"
SREM "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"
DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"
SADD "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"
SADD "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn"

删除现有哈希以避免可能不再存在的哈希键的剩余部分。

将平展的条目另存为哈希。

将 <1> 中写入的哈希的键添加到同一键空间中实体的帮助程序索引中。

获取可能需要更新的现有索引结构。

检查索引是否存在以及它是什么类型(文本、地理等)。

从索引中删除可能存在的键。

删除保存索引信息的帮助程序。

将 <2> 中添加的哈希键添加到具有属性值的名字的二级索引中。

将 <6> 的索引添加到要进入的帮助程序结构集中,以跟踪要在删除/更新时清理的索引。

13.13.3. 保存地理数据

地理索引遵循与基于普通文本的索引相同的规则,但使用地理结构来存储值。 保存使用地理索引属性的实体将生成以下命令:

GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b"  
SADD "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location"

将已保存条目的键添加到地理索引。

跟踪索引结构。

13.13.4. 使用简单索引查找

repository.findByFirstname("egwene");
SINTER  "people:firstname:egwene"                     
HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3"
HGETALL ...

获取二级索引中包含的键。

分别获取按 <1> 返回的每个密钥。

13.13.5. 使用地理索引查找

repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS));
GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" 
HGETALL "people:76900e94-b057-44bc-abcf-8126d51a621b"
HGETALL ...

获取二级索引中包含的键。

分别获取按 <1> 返回的每个密钥。