Spring Data Redis

©2011-2019 原作者。

Note

本文档的副本可以供您自己使用,也可以分发给其他人,但前提是您不对此类副本收取任何费用,并且还应确保每份副本均包含本版权声明(无论是印刷版本还是电子版本)。

目录

Preface

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

1.新功能

本节简要介绍了最新版本中的新内容和值得注意的内容。

1.1. Spring Data Redis 2.1 的新功能

  • 使用Lettuce的 Unix 域套接字连接。

  • 写给大师,从副本中读取支持使用 Lettuce。

  • 实例查询 integration.

  • @TypeAlias支持 Redis 存储库。

  • 在两个驱动程序支持的选定节点上,在整个群集范围内使用莴苣执行SCAN并执行SCAN

  • Reactive Pub/Sub发送和接收消息流。

  • BITFIELDBITPOSOBJECT命令支持。

  • BoundZSetOperations的返回类型与ZSetOperations对齐。

  • ReactiveSCANHSCANSSCANZSCAN支持。

  • 存储库查询方法中IsTrueIsFalse关键字的用法。

1.2. Spring Data Redis 2.0 的新功能

  • 升级到 Java 8.

  • 升级到 Lettuce5.0.

  • 删除了对 SRP 和 JRedis 驱动程序的支持。

  • 使用 Lettuce 的反应式连接支持.

  • 介绍RedisConnection的 Redis 功能特定的接口。

  • 使用JedisClientConfigurationLettuceClientConfiguration改进了RedisConnectionFactory配置。

  • 修订了RedisCache实施。

  • 为 Redis 3.2 添加SPOP和 count 命令。

1.3. Spring Data Redis 1.8 的新功能

  • 升级到 Jedis 2.9.

  • 升级到Lettuce 4.2(注意:Lettuce 4.2 需要 Java 8)。

  • 支持 Redis GEO命令。

  • 使用 Spring Data Repository 抽象支持地理空间索引(请参阅Geospatial Index)。

  • 基于MappingRedisConverterHashMapper实现(请参见Hash mapping)。

  • 在存储库中支持PartialUpdate(请参阅持续部分更新)。

  • SSL 支持与 Redis 集群的连接。

  • 使用 Jedis 时支持client nameConnectionFactory

1.4. Spring Data Redis 1.7 的新功能

1.5. Spring Data Redis 1.6 的新功能

  • Lettuce Redis 驱动程序从wg/lettuce切换到mp911de/lettuce

  • 支持ZRANGEBYLEX

  • ZSET的增强范围操作,包括+inf/-inf

  • RedisCache中的性能改进,现在可以更早地释放连接。

  • 通用 Jackson2 RedisSerializer利用了 Jackson 的多态反序列化。

1.6. Spring Data Redis 1.5 的新功能

  • 添加对 Redis HyperLogLog 命令的支持:PFADDPFCOUNTPFMERGE

  • 可配置的JavaType查找基于 Jackson 的RedisSerializers

  • 基于PropertySource的配置,用于连接到 Redis Sentinel(请参阅:Redis 前哨支持)。

Introduction

本文档是 Spring Data Redis(SDR)支持的参考指南。它解释了键-值模块的概念和语义以及各种 Store 名称空间的语法。

有关键值存储,Spring 或 Spring Data 示例的介绍,请参见Getting Started。本文档仅涉及 Spring Data Redis 支持,并假定用户熟悉键值存储和 Spring 概念。

2.为什么使用 Spring Data Redis?

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

NoSQL存储系统提供了传统 RDBMS 的替代产品,以实现水平可伸缩性和速度。在实现方面,键值存储代表 NoSQL 空间中最大(和最旧)的成员之一。

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

3. Requirements

Spring Data Redis 2.x 二进制文件要求 JDK 级别 8.0 和更高以及Spring Framework 5.1.5.RELEASE 和更高。

就键值存储而言,需要Redis 2.6.x 或更高。 Spring Data Redis 当前已针对最新的 4.0 版本进行了测试。

4.入门

本部分提供了易于使用的指南,以帮助您开始使用 Spring Data Redis 模块。

4.1. 第一步

为什么选择 Spring Data Redis?中所述,Spring Data Redis(SDR)在 Spring 框架和 Redis 键值存储之间提供集成。因此,您应该熟悉这两个框架。在整个 SDR 文档中,每个部分都提供了指向相关资源的链接。但是,在阅读本指南之前,您应该熟悉这些主题。

4.1.1. 学习之春

Spring Data 使用 Spring 框架的core功能,例如IoC容器,resource抽象和AOP基础结构。虽然了解 Spring API 并不重要,但了解它们背后的概念很重要。至少,IoC 背后的想法应该很熟悉。话虽这么说,您对 Spring 的了解越多,就能更快地获取 Spring Data Redis。除了 Spring Framework 的全面文档之外,还有很多有关此问题的文章,博客条目和书籍。Spring 指南home page提供了一个很好的起点。通常,这应该是想要尝试使用 Spring Data Redis 的开发人员的起点。

4.1.2. 学习 NoSQL 和键值存储

NoSQLStore 席卷了存储世界。它是一个广阔的领域,具有许多解决方案,术语和模式(更糟糕的是,即使术语本身也具有多个meanings)。尽管某些原则很普遍,但一定程度上要熟悉 SDR 支持的 Store,这一点至关重要。熟悉这些解决方案的最佳方法是阅读其文档并遵循其示例。通常不需要花费五到十分钟的时间来完成这些步骤,而且,如果您来自仅 RDMBS 的背景,那么很多时候这些练习可能会让您大开眼界。

4.1.3. 试用 samples

您可以在专用的 Spring 数据示例存储库http://github.com/spring-projects/spring-data-keyvalue-examples处找到键值存储的各种示例。对于 Spring Data Redis,您应该特别注意retwisj示例,该示例是在 Redis 之上构建的 Twitter 克隆,可以在本地运行或部署到云中。有关更多信息,请参见其documentation,以下博客entry

4.2. 需要帮忙?

如果您遇到问题或只是在寻求建议,请使用以下链接之一:

4.2.1. 社区支持

Stack Overflow上的 Spring Data 标签是一个留言板,供所有 Spring Data(不仅仅是 Redis)用户共享信息并互相帮助。请注意,注册仅 需要 才能发布。

4.2.2. 专业支持

Spring Data 和 Spring 背后的公司Pivotal Software,Inc.可提供专业的,源码支持的响应时间。

4.3. 后续 Developing

有关 Spring Data 源代码存储库,每夜构建和快照工件的信息,请参见 Spring Data 主页page

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

如果您遇到错误或要提出改进建议(包括本文档中的内容),请在 Spring Data 问题tracker上创建故障单。

要了解 Spring 生态系统中的最新新闻和公告,请订阅 Spring Community Portal

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

Reference Documentation

Document structure

参考文档的这一部分说明了 Spring Data Redis 提供的核心功能。

Redis support引入了 Redis 模块功能集。

5.支持 Redis

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

Note

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

Spring Data Redis 提供了简单的配置,并可以从 Spring 应用程序访问 Redis。它提供了与 Store 交互的低层和高层抽象,使用户摆脱了基础设施的困扰。

5.1. Redis 需求

Spring Redis 需要 Redis 2.6 或更高版本,并且 Spring Data Redis 与LettuceJedis集成,这是两个流行的 Redis 开源 Java 库。

5.2. Redis 支持高级视图

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

5.3. 连接到 Redis

使用 Redis 和 Spring 时的首要任务之一是通过 IoC 容器连接到 Store。为此,需要 Java 连接器(或绑定)。无论选择哪种库,您都只需要使用一组 Spring Data Redis API(在所有连接器上行为均一):org.springframework.data.redis.connection包及其RedisConnectionRedisConnectionFactory接口即可使用和检索与 Redis 的活动连接。

5.3.1. RedisConnection 和 RedisConnectionFactory

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

Note

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

有效的RedisConnection对象是通过RedisConnectionFactory创建的。另外,工厂充当PersistenceExceptionTranslator对象,这意味着一旦声明,它们就可以让您进行透明的异常转换。例如,您可以使用@Repository注解和 AOP 进行异常转换。有关更多信息,请参见 Spring Framework 文档中专用的section

Note

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

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

Tip

不幸的是,当前,并非所有连接器都支持所有 Redis 功能。在基础库不支持的 Connection API 上调用方法时,将引发UnsupportedOperationException

5.3.2. 配置莴苣连接器

Lettuce是基于Netty的开源连接器,由 Spring Data Redis 通过org.springframework.data.redis.connection.lettuce软件包支持。以下示例显示了如何创建新的 Lettuce 连接工厂:

@Configuration
class AppConfig {

  @Bean
  public LettuceConnectionFactory redisConnectionFactory() {

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

还可以调整一些 Lettuce 特定的连接参数。默认情况下,由LettuceConnectionFactory创建的所有LettuceConnection实例对于所有非阻塞和非事务性操作共享相同的线程安全本机连接。要每次使用专用连接,请将shareNativeConnection设置为falseLettuceConnectionFactory还可以配置为使用LettucePool来池化阻止和事务连接,或者将shareNativeConnection设置为false来缓冲所有连接。

Lettuce 与 Netty 的native transports集成,可让您使用 Unix 域套接字与 Redis 通信。确保包括与您的运行时环境匹配的适当的本机传输依赖项。以下示例显示了如何为/var/run/redis.sock的 Unix 域套接字创建 Lettuce Connection 工厂:

@Configuration
class AppConfig {

  @Bean
  public LettuceConnectionFactory redisConnectionFactory() {

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

Note

Netty 当前支持用于 OS 本地传输的 epoll(Linux)和 kqueue(BSD/macOS)接口。

5.3.3. 配置 Jedis 连接器

Jedis是社区驱动的连接器,由 Spring Data Redis 模块通过org.springframework.data.redis.connection.jedis软件包支持。 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);
  }
}

5.3.4. 写给大师,读副本

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

@Configuration
class WriteToMasterReadFromReplicaConfiguration {

  @Bean
  public LettuceConnectionFactory redisConnectionFactory() {

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

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

    return new LettuceConnectionFactory(serverConfig, clientConfig);
  }
}

Tip

对于通过INFO命令报告非公共地址的环境(例如,在使用 AWS 时),请使用RedisStaticMasterReplicaConfiguration而不是RedisStandaloneConfiguration

5.4. Redis 前哨支持

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

/**
 * 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);
}

/**
 * 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);
}

Configuration Properties

RedisSentinelConfiguration也可以使用PropertySource定义,您可以设置以下属性:

  • spring.redis.sentinel.master:主节点的名称。

  • spring.redis.sentinel.nodes:以逗号分隔的 host:port 对列表。

有时,需要与哨兵之一直接互动。使用RedisConnectionFactory.getSentinelConnection()RedisConnection.getSentinelCommands()可让您访问配置的第一个活动 Sentinel。

5.5. 通过 RedisTemplate 处理对象

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

此外,该模板提供了操作视图(按照 Redis 命令reference的分组),提供了丰富的通用接口,用于针对某种类型或某些键(通过KeyBound接口),如下表所述:

表 1.操作视图

InterfaceDescription
键类型操作
GeoOperationsRedis 地理空间操作,例如GEOADDGEORADIUS,…
HashOperationsRedis 哈希操作
HyperLogLogOperationsRedis HyperLogLog 操作,例如PFADDPFCOUNT,…
ListOperationsRedis 列表操作
SetOperationsRedis 设置操作
ValueOperationsRedis 字符串(或值)操作
ZSetOperationsRedis zset(或排序集)操作
关键绑定操作
BoundGeoOperationsRedis 键绑定地理空间操作
BoundHashOperationsRedis 哈希键绑定操作
BoundKeyOperationsRedis 按键绑定操作
BoundListOperationsRedis 列表键绑定操作
BoundSetOperationsRedis 设置键绑定操作
BoundValueOperationsRedis 字符串(或值)键绑定操作
BoundZSetOperationsRedis zset(或排序集)键绑定操作

配置后,该模板是线程安全的,并且可以在多个实例之间重用。

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

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:use-pool="true"/>
  <!-- redis template definition -->
  <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnectionFactory"/>
  ...

</beans>
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());
  }
}

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

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:use-pool="true"/>

  <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate" p:connection-factory-ref="jedisConnectionFactory"/>
  ...
</beans>
public class Example {

  @Autowired
  private StringRedisTemplate redisTemplate;

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

与其他 Spring 模板一样,RedisTemplateStringRedisTemplate使您可以通过RedisCallback接口直接与 Redis 对话。此功能直接与RedisConnection对话,因此可以完全控制您。请注意,当使用StringRedisTemplate时,回调将接收StringRedisConnection的实例。以下示例显示了如何使用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");
    }
   });
}

5.7. Serializers

从框架的角度来看,Redis 中存储的数据仅为字节。尽管 Redis 本身支持各种类型,但在大多数情况下,它们是指数据的存储方式而不是数据的表示方式。由用户决定是否将信息转换为字符串或任何其他对象。

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

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

  • 基于RedisSerializer的两路串行器。

  • 使用RedisElementReaderRedisElementWriter的元素读取器和写入器。

这些变体之间的主要区别在于,RedisSerializer主要序列化为byte[],而读写器使用ByteBuffer

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

  • JdkSerializationRedisSerializer,默认情况下用于RedisCacheRedisTemplate

  • StringRedisSerializer

但是,可以通过 Spring OXM支持将OxmSerializer用于对象/ XMLMap,或者将Jackson2JsonRedisSerializerGenericJackson2JsonRedisSerializer用于以JSON格式存储数据。

请注意,存储格式不仅限于值。它可以不受限制地用于键,值或哈希。

Warning

默认情况下,RedisCacheRedisTemplate被配置为使用 Java 本机序列化。众所周知,Java 本机序列化可允许由有效载荷引起的远程代码执行,有效载荷利用易受攻击的库和类注入未验证的字节码。在反序列化步骤中,操纵 Importing 可能导致应用程序中不需要的代码执行。因此,请勿在不受信任的环境中使用序列化。通常,我们强烈建议您使用其他任何消息格式(例如 JSON)。

如果您担心由 Java 序列化引起的安全漏洞,请考虑在核心 JVM 级别上使用通用序列化筛选器机制,该机制最初是为 JDK 9 开发的,但后来又移植到 JDK 8、7 和 6:

5.8. 哈希 Map

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

5.8.1. 哈希 Map 器

哈希 Map 器是 Map 对象到Map<K, V>并返回的转换器。 HashMapper适用于 Redis 哈希。

有多种实现方式:

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

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);
  }
}

5.8.2. Jackson2HashMapper

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

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

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

public class Person {
  String firstname;
  String lastname;
  Address address;
}

public class Address {
  String city;
  String country;
}

下表显示了上一类中的数据在法线 Map 中的显示方式:

表 2.法线 Map

Hash FieldValue
firstnameJon
lastnameSnow
address{ "city" : "Castle Black", "country" : "The North" }

下表显示了上一类中的数据在平面 Map 中的显示方式:

表 3.平面 Map

Hash FieldValue
firstnameJon
lastnameSnow
address.cityCastle Black
address.countryThe North

Note

拼合要求所有属性名称都不得干扰 JSON 路径。使用展平时,不支持在 Map 键中使用圆点或方括号或将其用作属性名称。生成的哈希不能 Map 回一个对象。

5.9. Redis 消息传递(Pub/Sub)

Spring Data 为 Redis 提供了专用的消息传递集成,其功能和命名与 Spring Framework 中的 JMS 集成相似。

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

  • 消息的发布或产生

  • 订阅或消费消息

这是通常称为“发布/订阅”(简称“发布/订阅”)的模式的示例。 RedisTemplate类用于消息生成。对于类似于 Java EE 的消息驱动 bean 样式的异步接收,Spring Data 提供了专用的消息侦听器容器,该容器用于创建消息驱动的 POJO(MDP),并为同步接收提供RedisConnection协定。

org.springframework.data.redis.connectionorg.springframework.data.redis.listener软件包提供 Redis 消息传递的核心功能。

5.9.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 = ...
template.convertAndSend("hello!", "world");

5.9.2. 订阅(接收消息)

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

在低层,RedisConnection提供subscribepSubscribe方法,它们 MapRedis 命令以分别按通道或按模式进行预订。注意,可以将多个通道或模式用作参数。要更改连接的预订或查询连接是否正在侦听,RedisConnection提供了getSubscriptionisSubscribed方法。

Note

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

如前所述,一旦订阅,连接即开始 await 消息。仅允许添加新订阅,修改现有订阅和取消现有订阅的命令。调用subscribepSubscribeunsubscribepUnsubscribe以外的任何东西都会引发异常。

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

邮件监听器容器

由于其阻塞性质,低级别订阅没有吸引力,因为它要求每个侦听器都进行连接和线程 Management。为了减轻这个问题,Spring Data 提供了RedisMessageListenerContainer,它可以完成所有繁重的工作。如果您熟悉 EJB 和 JMS,则应该熟悉这些概念,因为它被设计为尽可能接近 Spring Framework 及其消息驱动的 POJO(MDP)的支持。

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

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

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

The MessageListenerAdapter

MessageListenerAdapter类是 Spring 异步消息传递支持中的最后一个组件。简而言之,它使您几乎可以将任何**类公开为 MDP(尽管有一些约束)。

考虑以下接口定义:

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);
 }

请注意,尽管该接口未扩展MessageListener接口,但仍可以通过使用MessageListenerAdapter类将其用作 MDP。还请注意,如何根据各种消息处理方法可以接收和处理的各种Message类型的 内容 来强类型化。此外,可以将发送消息的通道或模式作为String类型的第二个参数传递给该方法:

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

请注意,上面的MessageDelegate接口(上面的DefaultMessageDelegate类)实现完全没有** Redis 依赖项。这确实是我们使用以下配置制作成 MDP 的 POJO:

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:redis="http://www.springframework.org/schema/redis"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd">

<!-- the default ConnectionFactory -->
<redis:listener-container>
  <!-- the method attribute can be skipped as the default method name is "handleMessage" -->
  <redis:listener ref="listener" method="handleMessage" topic="chatroom" />
</redis:listener-container>

<bean id="listener" class="redisexample.DefaultMessageDelegate"/>
 ...
<beans>

Note

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

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

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

<bean id="redisContainer" 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)在底层格式和所需对象类型之间进行转换。容器捕获并处理由方法调用引起的任何异常(默认情况下,异常会被记录)。

5.10. RedisTransaction

Redis 通过multiexecdiscard命令提供对transactions的支持。这些操作在RedisTemplate上可用。但是,不能保证RedisTemplate使用同一连接执行事务中的所有操作。

当需要对同一connection执行多项操作时(例如,使用 Redis 事务时),Spring Data Redis 提供了SessionCallback接口。下面的示例使用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方法,可让您传递自定义序列化程序以获取事务结果。

Note

从 1.1 版开始,对RedisConnectionRedisTemplateexec方法进行了重要更改。以前,这些方法直接从连接器返回事务处理的结果。这意味着数据类型通常不同于RedisConnection方法返回的数据类型。例如,zAdd返回一个布尔值,指示该元素是否已添加到排序集中。大多数连接器都会将此值返回为 long,而 Spring Data Redis 将执行转换。另一个共同的区别是,大多数连接器都会为set之类的操作返回状态回复(通常是字符串OK)。这些答复通常被 Spring Data Redis 丢弃。在 1.1 之前的版本中,未对exec的结果执行这些转换。另外,结果未在RedisTemplate中反序列化,因此它们通常包括原始字节数组。如果此更改使您的应用程序中断,请在RedisConnectionFactory上将convertPipelineAndTxResults设置为false以禁用此行为。

5.10.1. @Transaction 支持

默认情况下,事务支持是禁用的,必须通过设置setEnableTransactionSupport(true)为使用中的每个RedisTemplate显式启用。这样做会强制将当前RedisConnection绑定到触发MULTI的当前Thread。如果事务顺利完成,则调用EXEC。否则调用DISCARD。一旦进入MULTIRedisConnection就将写入操作排队。所有readonly操作(例如KEYS)都通过管道传递到新的(非线程绑定的)RedisConnection

以下示例显示如何配置事务 Management:

示例 1.启用事务 Management 的配置

@Configuration
@EnableTransactionManagement                                 (1)
public class RedisTxContextConfiguration {

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

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

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

  @Bean
  public DataSource dataSource() throws SQLException {
    // ...
  }
}
  • (1) 将 Spring Context 配置为启用声明式 TransactionManagement
  • (2) 通过将连接绑定到当前线程来配置RedisTemplate参与事务。
  • (3) TransactionManagement 需要PlatformTransactionManager。 Spring Data Redis 不附带PlatformTransactionManager实现。假设您的应用程序使用 JDBC,Spring Data Redis 可以使用现有的事务 Management 器参与事务。

以下示例每个都演示了使用约束:

例子 2.使用约束

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

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

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

5.11. Pipelining

Redis 提供对pipelining的支持,该支持涉及将多个命令发送到服务器,而无需 await 答复,然后一步即可读取答复。当您需要连续发送多个命令(例如将多个元素添加到同一 List)时,流水线可以提高性能。

Spring Data Redis 提供了几种RedisTemplate方法来执行管道中的命令。如果您不关心流水线操作的结果,则可以使用标准的execute方法,将true传递给pipeline参数。 executePipelined方法在管道中运行提供的RedisCallbackSessionCallback并返回结果,如以下示例所示:

//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方法,可让您传递自定义序列化程序以获取流水线结果。

请注意,从RedisCallback返回的值必须为 null,因为为了支持返回管道命令的结果而将其丢弃。

Note

从 1.1 版开始,对RedisConnectionRedisTemplateexec方法进行了重要更改。以前,这些方法直接从连接器返回事务处理的结果。这意味着数据类型通常不同于RedisConnection方法返回的数据类型。例如,zAdd返回一个布尔值,指示该元素是否已添加到排序集中。大多数连接器都会将此值返回为 long,而 Spring Data Redis 将执行转换。另一个共同的区别是,大多数连接器都会为set之类的操作返回状态回复(通常是字符串OK)。这些答复通常被 Spring Data Redis 丢弃。在 1.1 之前的版本中,未对exec的结果执行这些转换。另外,结果未在RedisTemplate中反序列化,因此它们通常包括原始字节数组。如果此更改使您的应用程序中断,请在RedisConnectionFactory上将convertPipelineAndTxResults设置为false以禁用此行为。

5.12. Redis 脚本

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

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

默认的ScriptExecutor通过检索脚本的 SHA1 并首先尝试运行evalsha来优化性能,如果 Redis 脚本缓存中还没有该脚本,则回退到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应该是LongBooleanList或反序列化值类型之一。如果脚本返回抛弃状态(特别是OK),则它也可以是null

Tip

理想的是在应用程序上下文中配置一个DefaultRedisScript实例,以避免在每次执行脚本时重新计算脚本的 SHA1.

然后,上面的checkAndSet方法运行脚本。脚本可以在SessionCallback内作为事务或管道的一部分运行。有关更多信息,请参见“ Redis Transactions”和“ Pipelining”。

Spring Data Redis 提供的脚本支持还允许您使用 Spring Task 和 Scheduler 抽象来调度 Redis 脚本以定期执行。有关更多详细信息,请参见Spring Framework文档。

5.13. 支持类

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

通过原子计数器,可以轻松包装 Redis 密钥增量,而集合则可以轻松 ManagementRedis 密钥,并最大程度地减少存储风险或 API 泄漏。特别是,RedisSetRedisZSet接口可轻松访问 Redis 支持的设置操作,例如intersectionunionRedisList在 Redis 上实现ListQueueDequeContract(及其等效的阻塞同级),将存储暴露为 FIFO(先进先出),LIFO(先入先出)或封顶只需最少的配置即可收集。以下示例显示了使用RedisList的 bean 的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="queue" class="org.springframework.data.redis.support.collections.DefaultRedisList">
    <constructor-arg ref="redisTemplate"/>
    <constructor-arg value="queue-key"/>
  </bean>

</beans>

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

public class AnotherExample {

  // injected
  private Deque<String> queue;

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

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

5.13.1. 支持 Spring Cache 抽象

Note

在 2.0 中更改

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

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

可以使用RedisCacheManagerBuilder配置RedisCacheManager行为,让您设置默认值RedisCacheConfiguration,事务行为和 sched 义的缓存。

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

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

RedisCacheConfiguration定义由RedisCacheManager创建的RedisCache的行为。该配置使您可以设置密钥到期时间,前缀和RedisSerializer实现,以与二进制存储格式进行相互转换,如以下示例所示:

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

RedisCacheManager默认为无锁RedisCacheWriter,用于读取和写入二进制值。无锁缓存提高了吞吐量。缺少条目锁定会导致putIfAbsentclean方法的重叠,非原子性命令重叠,因为这些命令需要将多个命令发送到 Redis。锁定对方通过设置显式锁定键并检查该键是否存在来防止命令重叠,这会导致其他请求和潜在的命令 await 时间。

可以选择采用以下锁定方式:

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

默认情况下,任何用于高速缓存条目的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);

下表列出了RedisCacheManager的默认设置:

表 4. RedisCacheManager默认值

SettingValue
Cache WriterNon-locking
Cache ConfigurationRedisCacheConfiguration#defaultConfiguration
Initial CachesNone
Trasaction AwareNo

下表列出了RedisCacheConfiguration的默认设置:

表 5. RedisCacheConfiguration 的默认值

Key ExpirationNone
快取nullYes
Prefix KeysYes
Default Prefix实际的缓存名称
Key SerializerStringRedisSerializer
Value SerializerJdkSerializationRedisSerializer
Conversion ServiceDefaultFormattingConversionService具有默认的缓存密钥转换器

6.响应式 Redis 支持

本节介绍了响应式 Redis 支持以及入门方法。响应式 Redis 支持自然与必要的 Redis 支持重叠。

6.1. Redis 需求

Spring Data Redis 当前与Lettuce集成为唯一的反应式 Java 连接器。 Project Reactor用作反应成分库。

6.2. 使用 Reactive 驱动程序连接到 Redis

使用 Redis 和 Spring 时的首要任务之一是通过 IoC 容器连接到 Store。为此,需要 Java 连接器(或绑定)。无论选择哪种库,都必须使用org.springframework.data.redis.connection软件包及其ReactiveRedisConnectionReactiveRedisConnectionFactory接口来与 Redis 一起使用并检索活动的connections

6.2.1. Redis 操作模式

Redis 可以使用Redis Sentinel或以Redis Cluster模式作为独立服务器运行。 Lettuce支持所有前面提到的连接类型。

6.2.2. ReactiveRedisConnection 和 ReactiveRedisConnectionFactory

ReactiveRedisConnection是 Redis 通信的核心,因为它处理与 Redis 后端的通信。它还会自动将基础驱动程序异常转换为 Spring 的一致 DAO 异常hierarchy,因此您可以在不更改任何代码的情况下切换连接器,因为操作语义保持不变。

ReactiveRedisConnectionFactory创建活动的ReactiveRedisConnection实例。另外,工厂充当PersistenceExceptionTranslator实例,这意味着一旦声明,它们就可以让您进行透明的异常转换-例如,通过使用@Repository注解和 AOP 进行异常转换。有关更多信息,请参见 Spring Framework 文档中专用的section

Note

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

Tip

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

6.2.3. 配置 Lettuce 连接器

Spring Data Redis 通过org.springframework.data.redis.connection.lettuce包支持Lettuce

您可以按照以下步骤为 Lettuce 设置ReactiveRedisConnectionFactory

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

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

@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);
}

有关 Client 端配置的更详细调整,请参阅LettuceClientConfiguration

6.3. 通过 ReactiveRedisTemplate 处理对象

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

此外,该模板还提供了操作视图(按照 Redis 命令reference进行分组之后),这些操作视图提供了丰富的通用接口来针对某种类型进行工作,如下表所述:

表 6.操作视图

InterfaceDescription
键类型操作
ReactiveGeoOperationsRedis 地理空间操作,例如GEOADDGEORADIUS等)
ReactiveHashOperationsRedis 哈希操作
ReactiveHyperLogLogOperationsRedis HyperLogLog 操作,例如(PFADDPFCOUNT等)
ReactiveListOperationsRedis 列表操作
ReactiveSetOperationsRedis 设置操作
ReactiveValueOperationsRedis 字符串(或值)操作
ReactiveZSetOperationsRedis zset(或排序集)操作

配置后,该模板是线程安全的,并且可以在多个实例之间重用。

ReactiveRedisTemplate使用大多数操作基于 Java 的序列化程序。这意味着通过RedisElementWriterRedisElementReader对模板写入或读取的任何对象进行序列化或反序列化。序列化上下文在构建时传递给模板,Redis 模块在org.springframework.data.redis.serializer包中提供了几种可用的实现。有关更多信息,请参见Serializers

以下示例显示了一个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());
  }
}

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

由于 Redis 中存储的键和值通常是java.lang.String,因此 Redis 模块为ReactiveRedisTemplateReactiveStringRedisTemplate提供了基于字符串的 extensions。对于密集的String操作,这是一种便捷的一站式解决方案。除了绑定到String键之外,该模板还使用基于字符串的RedisSerializationContext,这意味着存储的键和值是人类可读的(假设 Redis 和您的代码使用相同的编码)。以下示例显示了正在使用的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());
  }
}

6.5. Redis 消息传递/ PubSub

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

Redis 消息传递可以大致分为两个功能区域,即消息的产生或发布以及消息的消费或订阅,因此简称为 pubsub(发布/订阅)。 ReactiveRedisTemplate类用于消息生成。对于异步接收,Spring Data 提供了一个专用的消息侦听器容器,该容器用于消耗消息流。仅出于订阅ReactiveRedisTemplate的目的,提供了使用侦听器容器的简化选择。

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

6.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");

6.5.2. 接收/订阅消息

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

在低层,ReactiveRedisConnection提供subscribepSubscribe方法,它们 MapRedis 命令以分别按模式按通道进行订阅。注意,可以将多个通道或模式用作参数。要更改订阅,只需查询ReactiveSubscription的 Channels 和模式。

Note

Spring Data Redis 中的 Reactive 订阅命令是非阻塞的,可以终止而不会发出任何元素。

如上所述,一旦订阅,连接就会开始 await 消息。除了添加新订阅或修改/取消现有订阅之外,无法在其上调用其他命令。 subscribepSubscribeunsubscribepUnsubscribe以外的命令是非法的,并且会导致异常。

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

邮件监听器容器

Spring Data 提供ReactiveRedisMessageListenerContainer,它代表用户完成所有繁重的转换和订阅状态 Management。

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

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

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

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

Flux<ChannelMessage<String, String>> stream = container.receive(ChannelTopic.of("my-chanel"));
通过模板 API 订阅

如上所述,您可以直接使用ReactiveRedisTemplate订阅 Channels/模式。这种方法提供了一种直接但有限的解决方案,因为您松开了在初始订阅之后添加订阅的选项。不过,您仍然可以使用例如,通过返回的Flux控制消息流。 take(Duration)。完成读取后,一旦出错或取消,所有绑定的资源将再次释放。

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

6.6. 响应式脚本

通过反应式基础架构执行 Redis 脚本可以使用通过ReactiveRedisTemplate最佳访问的ReactiveScriptExecutor来完成。

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);
  }
}

有关脚本命令的更多详细信息,请参见scripting section

7. Redis 群集

使用Redis Cluster需要 Redis Server 3.0 版。有关更多信息,请参见Cluster Tutorial

7.1. 启用 Redis 集群

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

例子 3. Redis 集群的示例 RedisConnectionFactory 配置

@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 JedisConnectionFactory(
            new RedisClusterConfiguration(clusterProperties.getNodes()));
    }
}

Configuration Properties

RedisClusterConfiguration也可以通过PropertySource进行定义,并具有以下属性:

  • spring.redis.cluster.nodes:以逗号分隔的 host:port 对列表。

  • spring.redis.cluster.max-redirects:允许的群集重定向数。

Note

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

7.2. 使用 Redis 集群连接

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

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

RedisClusterNode可以从RedisClusterConnection.clusterGetNodes获得,也可以使用主机和端口或节点 ID 来构造。

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

例子 4.跨集群运行命令的 samples

[emailprotected]:7379 > cluster nodes

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

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

connection.keys("*");                                                          (7)

connection.keys(NODE_7379, "*");                                               (8)
connection.keys(NODE_7380, "*");                                               (9)
connection.keys(NODE_7381, "*");                                               (10)
connection.keys(NODE_7382, "*");                                               (11)
  • (1) 服务于插槽 0 到 5460 的主节点在 7382 被复制到副本
  • (2) 主节点服务于插槽 5461 至 10922
  • (3) 主节点服务于插槽 10923 至 16383
  • (4) 在 7379 处拥有主副本的副本节点
  • (5) 请求路由到 7381 服务插槽 12182 的节点
  • (6) 请求路由到 7379 服务于 5061 插槽的节点
  • (7) 请求已路由到 7379、7380、7381 的节点→[thing1,thing2]
  • (8) 请求已路由到 7379→[thing2]的节点
  • (9) 请求路由到 7380→[]的节点
  • (10) 请求路由到 7381→[thing1]的节点
  • (11) 请求已路由到 7382→[thing2]的节点

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

例子 5.跨槽请求处理的 samples

[emailprotected]:7379 > cluster nodes

6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460                      (1)
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");                                  (2)

connection.mGet("thing1", "thing2");                                           (3)
  • (1) 与之前的示例中的配置相同。
  • (2) 键 Map 到相同的插槽→127.0.0.1:7381 MGET Thing1{thing1} .thing2
  • (3) 密钥 Map 到不同的插槽,并拆分为单个插槽,并路由到相应的节点
    →127.0.0.1:7379 GET Thing2
    →127.0.0.1:7381 GET something1

Tip

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

7.3. 使用 RedisTemplate 和 ClusterOperations

有关RedisTemplate的一般用途,配置和用法的信息,请参见通过 RedisTemplate 处理对象部分。

Warning

使用任何 JSON RedisSerializers设置RedisTemplate#keySerializer时都要小心,因为更改 JSON 结构会立即影响哈希槽的计算。

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

以下示例显示了如何使用RedisTemplate访问RedisClusterConnection

例子 6.用RedisTemplate访问RedisClusterConnection

ClusterOperations clusterOps = redisTemplate.opsForCluster();
clusterOps.shutdown(NODE_7379);                                              (1)
  • (1) 在 7379 处关闭节点,然后用手指交叉放置一个可以接管的副本。

8. Redis 存储库

通过使用 Redis 信息库,您可以无缝地在 Redis 哈希中转换和存储域对象,应用自定义 Map 策略以及使用二级索引。

Tip

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

8.1. Usage

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

例子 7.samples 人实体

@RedisHash("people")
public class Person {

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

这里有一个非常简单的域对象。请注意,它的类型带有@RedisHashComments,并带有org.springframework.data.annotation.IdComments 的名为id的属性。这两个项目负责创建用于保留哈希的实际密钥。

Note

@IdComments 的属性以及名为id的属性被视为标识符属性。那些带有 Comments 的人比其他人更受青睐。

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

例子 8.持久人实体的基本存储库接口

public interface PersonRepository extends CrudRepository<Person, String> {

}

随着我们的存储库CrudRepository的扩展,它提供了基本的 CRUD 和 finder 操作。我们需要将它们粘合在一起的是对应的 Spring 配置,如以下示例所示:

例子 9.用于 Redis 存储库的 JavaConfig

@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

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

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

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

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

例子 10.访问个人实体

@Autowired PersonRepository repo;

public void basicCrudOperations() {

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

  repo.save(rand);                                         (1)

  repo.findOne(rand.getId());                              (2)

  repo.count();                                            (3)

  repo.delete(rand);                                       (4)
}
  • (1) 如果当前值为null或重新使用已经设置的id值并在 Redis 哈希中使用keyspace:id模式的键存储类型Person的属性,则生成一个新的id-在这种情况下,它可能会是people:5d67b7e1-8640-4475-beeb-c666fab4c0e5
  • (2) 使用提供的id检索存储在keyspace:id的对象。
  • (3)Person上的@RedisHash定义的键空间people中可用的实体总数进行计数。
  • (4) 从 Redis 移除给定对象的键。

8.2. 对象 Map 基础

本节介绍了 Spring Data 对象 Map,对象创建,字段和属性访问,可变性和不可变性的基础。请注意,本部分仅适用于不使用基础数据存储(例如 JPA)的对象 Map 的 Spring Data 模块。另外,请确保参考 Store 特定的部分以获取 Store 特定的对象 Map,例如索引,自定义列或字段名称等。

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

  • 使用公开的构造函数之一创建实例。

  • 实例填充以实现所有暴露的属性。

8.2.1. 对象创建

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

  • 如果有一个无参数的构造函数,则将使用它。其他构造函数将被忽略。

  • 如果只有一个构造函数接受参数,则将使用它。

  • 如果有多个构造函数采用参数,则必须由@PersistenceConstructorCommentsSpring Data 要使用的一个。

值解析假定构造函数参数名称与实体的属性名称匹配,即,解析将像要填充该属性一样执行,包括 Map 中的所有自定义项(不同的数据存储列或字段名称等)。这还需要类文件中可用的参数名称信息或构造器上存在的@ConstructorPropertiesComments。

可以使用特定于 Store 的 SpEL 表达式使用 Spring Framework 的@Value值 Comments 来自定义值分辨率。请参阅有关 Store 特定 Map 的部分以获取更多详细信息。

对象创建内部

为了避免反射的开销,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 Data 将通过反射回退到实体实例化。

8.2.2. 财产人口

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

  • 如果属性是不可变的,但是公开了一个凋零方法(请参见下文),我们将使用凋零来创建具有新属性值的新实体实例。

  • 如果定义了属性访问(即通过 getter 和 setter 的访问),那么我们正在调用 setter 方法。

  • 默认情况下,我们直接设置字段值。

房地产人口内部

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

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;
  }
}

例子 11.生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              (2)

  private Person person;                                    (1)

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             (2)
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            (3)
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              (4)
    }
  }
}
  • (1) PropertyAccessor 持有基础对象的可变实例。这是为了使原本不可变的属性发生突变。
  • (2) 默认情况下,Spring Data 使用字段访问来读取和写入属性值。根据private个字段的可见性规则,MethodHandles用于与字段进行交互。
  • (3) 该类公开了用于设置标识符的withId(…)方法,例如将实例插入数据存储区并已生成标识符时。调用withId(…)将创建一个新的Person对象。所有后续突变都将在新实例中发生,而先前的实例将保持不变。
  • (4) 使用属性访问可直接调用方法,而无需使用MethodHandles

这使我们的反射性能提高了约 25%。为了使域类有资格进行此类优化,它需要遵守一组约束:

  • 类型不得位于默认值或java包下。

  • 类型及其构造函数必须为public

  • 内部类的类型必须为static

  • 使用的 Java 运行时必须允许在原始ClassLoader中声明类。 Java 9 和更高版本强加了某些限制。

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

让我们看一下以下实体:

例子 12.一个 samples 实体

class Person {

  private final @Id Long id;                                                (1)
  private final String firstname, lastname;                                 (2)
  private final LocalDate birthday;
  private final int age; (3)

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

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

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

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

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

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

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
  • (1) 标识符属性是 final,但在构造函数中设置为null。该类公开了用于设置标识符的withId(…)方法,例如将实例插入数据存储区并已生成标识符时。创建新实例时,原始Person实例保持不变。通常将相同的模式应用于由存储 Management 的其他属性,但可能需要为持久性操作进行更改。
  • (2) firstnamelastname属性是可能通过 getter 公开的普通不可变属性。
  • (3) age属性是不可变的,但是从birthday属性派生的。按照所示的设计,由于 Spring Data 使用唯一声明的构造函数,因此数据库值将胜过默认值。即使意图是首选计算,此构造函数也必须将age作为参数(可能会忽略它),这很重要,否则属性填充步骤将尝试设置 age 字段并由于其不可变而失败,并且没有凋谢。
  • (4) 通过直接设置其comment属性是可变的。
  • (5) remarks属性是可变的,可通过直接设置comment字段或通过调用 setter 方法来填充
  • (6) 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过@PersistenceConstructor消除构造函数歧义的需要。相反,属性的默认设置是在工厂方法中处理的。

8.2.3. 一般建议

  • 尝试坚持不可变的对象-不可变的对象很容易创建,因为实现一个对象只需调用其构造函数即可。同样,这避免了用允许 Client 端代码操纵对象状态的 setter 方法乱扔您的域对象。如果需要它们,则最好使它们受到程序包保护,以便只能由有限数量的同一位置类型调用它们。仅限构造函数的实现比属性填充快 30%。

  • 提供一个全参数的构造函数-即使您无法或不希望将实体建模为不可变值,提供构造函数仍具有价值,该构造函数将实体的所有属性作为参数,包括可变的允许对象 Map 跳过属性填充以获得最佳性能。

  • *使用工厂方法而不是重载的构造函数来避免@PersistenceConstructor * —为了获得最佳性能,需要使用全参数构造函数,我们通常希望公开更多针对应用程序用例的特定构造函数,这些构造函数会忽略诸如自动生成的标识符等内容。而是使用静态工厂方法公开 all-args 构造函数的这些变体。

  • 确保您遵守允许使用生成的实例化器和属性访问器类的约束

  • 对于要生成的标识符,请结合使用 final 字段和凋灵方法

  • 使用 Lombok 避免样板代码-持久化操作通常需要构造函数接受所有参数,因此它们的声明成为对字段分配的样板参数的繁琐重复,最好使用 Lombok 的@AllArgsConstructor来避免。

8.2.4. Kotlin 支持

Spring Data 修改了 Kotlin 的细节以允许对象创建和变异。

Kotlin 对象创建

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

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

上面的类使用显式构造函数编译为典型类。我们可以通过添加另一个构造函数并使用@PersistenceConstructor对其进行 Comments 来表示该构造函数的首选项来自定义此类:

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

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

Kotlin 通过允许在未提供参数的情况下使用默认值来支持参数可选性。当 Spring Data 检测到带有参数默认值的构造函数时,如果数据存储区不提供值(或仅返回null),它将保留这些参数不存在,因此 Kotlin 可以应用参数默认值。考虑下面的类,该类对name应用参数默认值

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

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

Kotlin 数据类的属性人口

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

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

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

8.3. 对象到哈希的 Map

Redis 存储库支持将对象保留为哈希。这需要由RedisConverter完成的对象到哈希的转换。默认实现使用Converter将属性值 Map 到 Redis 本机byte[]或从 Redis 本机byte[]Map 属性值。

给定前面几节中的Person类型,默认 Map 如下所示:

_class = org.example.Person                 (1)
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand                            (2)
lastname = al'thor
address.city = emond's field                (3)
address.country = andor
  • (1) _class属性包括在根级别以及任何嵌套接口或抽象类型中。
  • (2) 简单属性值是按路径 Map 的。
  • (3) 复杂类型的属性按其点路径进行 Map。

下表描述了默认 Map 规则:

表 7.默认 Map 规则

TypeSampleMapped Value
Simple Type

(例如,String)
String firstname =“ rand”;firstname =“ rand”
Complex Type
(例如,地址)
地址地址=新地址(“ emond 的字段”);address.city =“ emond 的字段”
List
的简单类型
列表\ 昵称= asList(“ dragon reborn”,“ lews therin”);昵称。[0] =“龙再生”,
昵称。[1] =“留下 therin”
Map
类型
Map<String, String> atts = asMap(\ {"eye-color", "grey"},{“…atts。[eye-color] =” grey“,
atts。[hair-color] =“…
List
的复杂类型
列表\ 地址= asList(新地址(“ em…地址。[0] .city =” emond 的字段“,
地址。[1] .city =“…
Map
的复杂类型
Map\ <String, Address>地址= asMap({“ home”,新的地址(“ em…addresses。[home] .city =” emond 的字段“,
地址。[work] .city =“…

Warning

由于采用平面表示结构,因此 Map 键必须是简单的类型,例如StringNumber

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

Warning

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

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

例子 13.samplesbyte []转换器

@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转换器的两个示例:

例子 14.示例图\ <String,byte[]>转换器

@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<Address, Map<String, byte[]>> {

  @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"

Note

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

8.3.1. 自定义类型 Map

如果要避免将整个 Java 类名称写为类型信息,并且希望使用键,则可以在要保留的实体类上使用@TypeAlias注解。如果您需要进一步自定义 Map,请查看TypeInformationMapper界面。该接口的实例可以在DefaultRedisTypeMapper上配置,该实例可以在MappingRedisConverter上配置。

以下示例显示如何为实体定义类型别名:

例子 15.为一个实体定义@TypeAlias

@TypeAlias("pers")
class Person {

}

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

配置自定义类型 Map

以下示例演示了如何在MappingRedisConverter中配置自定义RedisTypeMapper

例子 16.通过 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();
  }
}

8.4. Keyspaces

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

以下示例显示了如何使用@EnableRedisRepositoriesComments 设置键空间配置:

例 17.通过@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"));
    }
  }
}

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

例子 18.程序化键空间设置

@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

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

  public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {

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

8.5. 次要 Metrics

Secondary indexes用于启用基于本地 Redis 结构的查找操作。每次保存时,值都会写入相应的索引,并在删除对象或expire时将其删除。

8.5.1. 简单属性索引

给定前面显示的示例Person实体,我们可以通过用@IndexedComments 属性来为firstname创建索引,如以下示例所示:

例子 19.Comments 驱动的索引

@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

也可以在嵌套元素上具有索引。假设Address具有city属性,并用@IndexedComments。在这种情况下,一旦person.address.city不是null,我们将为每个城市设置集合,如以下示例所示:

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

此外,通过编程设置,您可以定义 Map 键和列表属性的索引,如以下示例所示:

@RedisHash("people")
public class Person {

  // ... other properties omitted

  Map<String,String> attributes;      (1)
  Map<String Person> relatives;       (2)
  List<Address> addresses;            (3)
}
  • (1) SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e
  • (2) SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e
  • (3) SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

Warning

无法在References上解析索引。

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

例子 20.使用@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"));
    }
  }
}

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

例子 21.程序化索引设置

@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"));
    }
  }
}

8.5.2. 地理空间指数

假设Address类型包含Point类型的location属性,该属性保存特定地址的地理坐标。通过使用@GeoIndexedComments 属性,Spring Data Redis 使用 Redis 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);     (1)
  List<Person> findByAddressLocationWithin(Circle circle);                    (2)
}

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

repository.save(rand);                                                        (3)

repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); (4)
  • (1) 使用PointDistance在嵌套属性上查询方法声明。
  • (2) 嵌套属性上的查询方法声明,使用Circle在其中进行搜索。
  • (3) GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e
  • (4) GEORADIUS people:address:location 15.0 37.0 200.0 km

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

Note

不能将nearwithin与其他条件组合在一起。

8.6. 实例查询

8.6.1. Introduction

本章对“示例查询”进行了介绍,并说明了如何使用它。

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

8.6.2. Usage

按示例查询 API 包含三部分:

  • 探针:带有填充字段的域对象的实际示例。

  • ExampleMatcherExampleMatcher包含有关如何匹配特定字段的详细信息。可以在多个示例中重复使用它。

  • ExampleExample由探针和ExampleMatcher组成。它用于创建查询。

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

  • 使用一组静态或动态约束来查询数据存储。

  • 频繁重构域对象,而不必担心破坏现有查询。

  • 独立于基础数据存储区 API 进行工作。

按示例查询也有一些限制:

  • 不支持嵌套或分组属性约束,例如firstname = ?0 or (firstname = ?1 and lastname = ?2)

  • 仅支持字符串的开始/包含/结束/正则表达式匹配,以及其他属性类型的完全匹配。

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

例子 22. Sample Person 对象

public class Person {

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

  // … getters and setters omitted
}

前面的示例显示了一个简单的域对象。您可以使用它来创建Example。默认情况下,具有null值的字段将被忽略,并且使用 Store 特定的默认值来匹配字符串。可以使用of factory 方法或ExampleMatcher来构建示例。 Example是不可变的。以下清单显示了一个简单的示例:

例子 23.简单的例子

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

Example<Person> example = Example.of(person);         (3)
  • (1) 创建域对象的新实例。
  • (2) 设置要查询的属性。
  • (3) 创建Example

最好在存储库中执行示例。为此,让您的存储库界面扩展QueryByExampleExecutor<T>。以下清单显示了QueryByExampleExecutor界面的摘录:

例子 24. 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.
}

8.6.3. 示例匹配器

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

例子 25.具有定制匹配的例子匹配器

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

ExampleMatcher matcher = ExampleMatcher.matching()     (3)
  .withIgnorePaths("lastname")                         (4)
  .withIncludeNullValues()                             (5)
  .withStringMatcherEnding();                          (6)

Example<Person> example = Example.of(person, matcher); (7)
  • (1) 创建域对象的新实例。
  • (2) 设置属性。
  • (3) 创建一个ExampleMatcher以期望所有值都匹配。即使没有进一步的配置,它也可以在此阶段使用。
  • (4) 构造一个新的ExampleMatcher以忽略lastname属性路径。
  • (5) 构造一个新的ExampleMatcher以忽略lastname属性路径并包含空值。
  • (6) 构造一个新的ExampleMatcher来忽略lastname属性路径,包括空值并执行后缀字符串匹配。
  • (7) 基于域对象和配置的ExampleMatcher创建一个新的Example

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

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

例子 26.配置匹配器选项

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

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

例子 27.用 lambdas 配置匹配器选项

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

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

表 8. ExampleMatcher设置的范围

SettingScope
Null-handlingExampleMatcher
String matchingExampleMatcher和属性路径
Ignoring propertiesProperty path
Case sensitivityExampleMatcher和属性路径
Value transformationProperty path

8.6.4. 执行一个例子

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

例子 28.使用存储库按例子查询

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个值

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

  • Case-insensitive matching

  • 正则表达式,前缀/包含/后缀字符串匹配

  • 查询关联,集合和类似 Map 的属性

  • 从条件中包含null个值

  • findAll排序

8.7. 生存时间

Redis 中存储的对象可能仅在一定时间内有效。这对于在 Redis 中持久保存短寿命的对象特别有用,而无需在寿命到期时手动将其删除。可以使用@RedisHash(timeToLive=…)以及使用KeyspaceSettings(请参见Keyspaces)设置以秒为单位的到期时间。

可以通过在数字属性或方法上使用@TimeToLiveComments 来设置更灵活的到期时间。但是,不要对同一类中的方法和属性都应用@TimeToLive。以下示例显示了属性和方法上的@TimeToLive注解:

例子 29.到期

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();
  }
}

Note

使用@TimeToLive显式 Comments 属性会从 Redis 读取实际的TTLPTTL值。 -1 表示该对象没有关联的到期时间。

存储库实现可确保通过RedisMessageListenerContainer订阅Redis 键空间通知

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

默认情况下,初始化应用程序时禁用密钥过期侦听器。可以在@EnableRedisRepositoriesRedisKeyValueAdapter中调整启动模式,以通过应用程序或在第一次插入带有 TTL 的实体时启动侦听器。有关可能的值,请参见EnableKeyspaceEvents

RedisKeyExpiredEvent包含过期的域对象以及密钥的副本。

Note

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

Note

密钥空间通知消息侦听器会更改 Redis 中的notify-keyspace-events设置(如果尚未设置)。现有设置不会被覆盖,因此您必须正确设置这些设置(或将其保留为空)。请注意,在 AWS ElastiCache 上禁用了CONFIG,并且启用侦听器会导致错误。

Note

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

8.8. 持续引用

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

例子 30.samples 属性参考

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al'thor
mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56      (1)
  • (1) 引用存储引用对象的整个键(keyspace:id)。

Warning

保存引用对象时,引用对象不会保留。由于仅存储引用,因此您必须分别对引用的对象进行更改。无法解析在引用类型的属性上设置的索引。

8.9. 持续部分更新

在某些情况下,您无需加载和重写整个实体就可以在其中设置新值。最后一个活动时间的会话时间戳可能就是您要更改一个属性的情况。 PartialUpdate可让您在现有对象上定义setdelete操作,同时注意更新实体本身和索引结构的潜在失效时间。以下示例显示了部分更新:

例子 31.samples 部分更新

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

template.update(update);

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

template.update(update);

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

template.update(update);
  • (1) 将简单的firstname属性设置为mat
  • (2) 将简单的'address.city'属性设置为'emond's field',而不必传入整个对象。注册自定义转换时,此功能不起作用。
  • (3) 删除age属性。
  • (4) 设置复杂的address属性。
  • (5) 设置值的 Map,该 Map 将删除先前存在的 Map 并将值替换为给定的值。
  • (6) 更改生存时间时自动更新服务器到期时间。

Note

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

8.10. 查询和查询方法

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

例子 32.samples 库查找器方法

public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByFirstname(String firstname);
}

Note

请确保设置查找程序方法中使用的属性以进行索引。

Note

Redis 存储库的查询方法仅支持对实体的查询以及带分页的实体集合。

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

例子 33.使用 RedisCallback 的 samples 查找器

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 支持的关键字以及包含该关键字的方法的基本含义:

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

KeywordSampleRedis snippet
AndfindByLastnameAndFirstnameSINTER …:firstname:rand …:lastname:al'thor
OrfindByLastnameOrFirstnameSUNION …:firstname:rand …:lastname:al'thor
Is, EqualsfindByFirstname , findByFirstnameIs , findByFirstnameEqualsSINTER …:firstname:rand
IsTrueFindByAliveIsTrueSINTER …:alive:1
IsFalsefindByAliveIsFalseSINTER …:alive:0
Top,FirstfindFirst10ByFirstname , findTop5ByFirstname

8.11. 在集群上运行的 Redis 存储库

您可以在集群 Redis 环境中使用 Redis 存储库支持。有关ConnectionFactory配置的详细信息,请参见“ Redis Cluster”部分。但是,还必须进行一些其他配置,因为默认的密钥分发会在整个群集及其插槽中扩展实体和二级索引。

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

KeyTypeSlotNode
people:e2c7dcee-b8cd-4424-883e-736ce564363e哈希 ID15171127.0.0.1:7381
people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56哈希 ID7373127.0.0.1:7380
people:firstname:randindex1700127.0.0.1:7379

当所有涉及的键都 Map 到同一插槽时,某些命令(例如SINTERSUNION)只能在服务器端处理。否则,必须在 Client 端进行计算。因此,将键空间固定到单个插槽非常有用,这样可以立即利用 Redis 服务器端计算。下表显示了您执行此操作时发生的情况(请注意插槽列中的更改以及节点列中的端口值):

KeyTypeSlotNode
{people}:e2c7dcee-b8cd-4424-883e-736ce564363e哈希 ID2399127.0.0.1:7379
{people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56哈希 ID2399127.0.0.1:7379
{people}:firstname:randindex2399127.0.0.1:7379

Tip

使用 Redis 集群时,可以通过使用@RedisHash("{yourkeyspace}")定义键空间并将其固定到特定插槽。

8.12. CDI 整合

存储库接口的实例通常由容器创建,在使用 Spring Data 时,Spring 是最自然的选择。 Spring 提供了用于创建 bean 实例的高级工具。 Spring Data Redis 附带了一个自定义 CDI 扩展,使您可以在 CDI 环境中使用存储库抽象。该扩展是 JAR 的一部分,因此要激活它,请将 Spring Data Redis JAR 放到您的 Classpath 中。

然后,您可以通过为RedisConnectionFactoryRedisOperations实现 CDI 生产者来设置基础结构,如以下示例所示:

class RedisOperationsProducer {

  @Produces
  RedisConnectionFactory redisConnectionFactory() {

    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration());
    jedisConnectionFactory.afterPropertiesSet();

    return jedisConnectionFactory;
  }

  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 环境。

每当容器请求存储库类型的 bean 时,Spring Data Redis CDI 扩展都会将所有可用的存储库作为 CDI bean 来获取,并为 Spring Data 存储库创建代理。因此,获取 Spring Data 存储库的实例只需声明@Injected属性,如以下示例所示:

class RepositoryClient {

  @Inject
  PersonRepository repository;

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

Redis 存储库需要RedisKeyValueAdapterRedisKeyValueTemplate个实例。如果找不到提供的 bean,那么这些 bean 由 Spring Data CDI 扩展创建和 Management。但是,您可以提供自己的 bean 来配置RedisKeyValueAdapterRedisKeyValueTemplate的特定属性。

8.13. Redis 存储库解剖

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

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

将以下实体类视为所有操作的起点:

例子 34.例子实体

@RedisHash("people")
public class Person {

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

public class Address {

  @GeoIndexed Point location;
}

8.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" (1)
SADD  "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9"                           (2)
SADD  "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9"            (3)
SADD  "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand" (4)
  • (1) 将平展的条目另存为哈希。
  • (2) 将\ <1>中编写的哈希键添加到同一键空间中的实体的辅助索引中。
  • (3) 将\ <2>中编写的哈希键添加到具有属性值的名字的二级索引中。
  • (4) 将\ <3>的索引添加到帮助程序结构集中以供 Importing,以跟踪删除/更新时要清理的索引。

8.13.2. 替换现有的

repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor"));
DEL       "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"                           (1)
HMSET     "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor" (2)
SADD      "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"                         (3)
SMEMBERS  "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"                       (4)
TYPE      "people:firstname:rand"                                                 (5)
SREM      "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"          (6)
DEL       "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"                       (7)
SADD      "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" (8)
SADD      "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn" (9)
  • (1) 删除现有的散列,以避免可能不再存在的散列键剩余。
  • (2) 将扁平化条目保存为哈希。
  • (3) 将\ <1>中编写的哈希键添加到同一键空间中的实体的辅助索引中。
  • (4) 获取可能需要更新的现有索引结构。
  • (5) 检查索引是否存在及其类型(文本,geo 等)。
  • (6) 从索引中删除潜在的现有键。
  • (7) 删除帮助程序持有索引信息。
  • (8) 将\ <2>中添加的哈希键添加到具有属性值的名字的二级索引中。
  • (9) 将\ <6>的索引添加到帮助程序结构集中以供 Importing,以跟踪删除/更新时要清理的索引。

8.13.3. 保存地理数据

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

GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b"  (1)
SADD   "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location"               (2)
  • (1) 将保存的条目的键添加到 geo 索引。
  • (2) 跟踪索引结构。

8.13.4. 使用简单索引查找

repository.findByFirstname("egwene");
SINTER  "people:firstname:egwene"                     (1)
HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3" (2)
HGETALL ...
  • (1) 提取二级索引中包含的键。
  • (2) 分别获取\ <1>返回的每个键。

8.13.5. 使用地理位置索引查找

repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS));
GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" (1)
HGETALL   "people:76900e94-b057-44bc-abcf-8126d51a621b"         (2)
HGETALL   ...
  • (1) 提取二级索引中包含的键。
  • (2) 分别获取\ <1>返回的每个键。

Appendixes

附录文件结构

附录包含各种其他详细信息,它们补充了参考文档其余部分中的信息:

  • Schema”定义了 Spring Data Redis 提供的模式。

  • Command Reference”详细说明RedisTemplate支持哪些命令。

附录 A:架构

Spring Data Redis Schema(重新命名空间)

附录 B:命令参考

Supported Commands

表 10. RedisTemplate支持的 Redis 命令

CommandTemplate Support
APPENDX
AUTHX
BGREWRITEAOFX
BGSAVEX
BITCOUNTX
BITFIELDX
BITOPX
BLPOPX
BRPOPX
BRPOPLPUSHX
CLIENT KILLX
CLIENT GETNAMEX
CLIENT LISTX
CLIENT SETNAMEX
CLUSTER SLOTS-
COMMAND-
COMMAND COUNT-
COMMAND GETKEYS-
COMMAND INFO-
CONFIG GETX
CONFIG RESETSTATX
CONFIG REWRITE-
CONFIG SETX
DBSIZEX
DEBUG OBJECT-
DEBUG SEGFAULT-
DECRX
DECRBYX
DELX
DISCARDX
DUMPX
ECHOX
EVALX
EVALSHAX
EXECX
EXISTSX
EXPIREX
EXPIREATX
FLUSHALLX
FLUSHDBX
GETX
GETBITX
GETRANGEX
GETSETX
HDELX
HEXISTSX
HGETX
HGETALLX
HINCRBYX
HINCRBYFLOATX
HKEYSX
HLENX
HMGETX
HMSETX
HSCANX
HSETX
HSETNXX
HVALSX
INCRX
INCRBYX
INCRBYFLOATX
INFOX
KEYSX
LASTSAVEX
LINDEXX
LINSERTX
LLENX
LPOPX
LPUSHX
LPUSHXX
LRANGEX
LREMX
LSETX
LTRIMX
MGETX
MIGRATE-
MONITOR-
MOVEX
MSETX
MSETNXX
MULTIX
OBJECT-
PERSISTX
PEXIPREX
PEXPIREATX
PFADDX
PFCOUNTX
PFMERGEX
PINGX
PSETEXX
PSUBSCRIBEX
PTTLX
PUBLISHX
PUBSUB-
PUBSUBSCRIBE-
QUITX
RANDOMKEYX
RENAMEX
RENAMENXX
RESTOREX
ROLE-
RPOPX
RPOPLPUSHX
RPUSHX
RPUSHXX
SADDX
SAVEX
SCANX
SCARDX
SCRIPT EXITSX
SCRIPT FLUSHX
SCRIPT KILLX
SCRIPT LOADX
SDIFFX
SDIFFSTOREX
SELECTX
SENTINEL FAILOVERX
SENTINEL GET-MASTER-ADD-BY-NAME-
SENTINEL MASTER-
SENTINEL MASTERSX
SENTINEL MONITORX
SENTINEL REMOVEX
SENTINEL RESET-
SENTINEL SET-
SENTINEL SLAVESX
SETX
SETBITX
SETEXX
SETNXX
SETRANGEX
SHUTDOWNX
SINTERX
SINTERSTOREX
SISMEMBERX
SLAVEOFX
SLOWLOG-
SMEMBERSX
SMOVEX
SORTX
SPOPX
SRANDMEMBERX
SREMX
SSCANX
STRLENX
SUBSCRIBEX
SUNIONX
SUNIONSTOREX
SYNC-
TIMEX
TTLX
TYPEX
UNSUBSCRIBEX
UNWATCHX
WATCHX
ZADDX
ZCARDX
ZCOUNTX
ZINCRBYX
ZINTERSTOREX
ZLEXCOUNT-
ZRANGEX
ZRANGEBYLEX-
ZREVRANGEBYLEX-
ZRANGEBYSCOREX
ZRANKX
ZREMX
ZREMRANGEBYLEX-
ZREMRANGEBYRANKX
ZREVRANGEX
ZREVRANGEBYSCOREX
ZREVRANKX
ZSCANX
ZSCOREX
ZUNINONSTOREX