• 8. Redis Repositories

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

Redis Repositories至少需要Redis服务器版本2.8.0。

8.1. 用法

要访问存储在Redis中的域实体,您可以利用存储库支持,从而轻松实现这些实现。

示例5.Person实体示例

@RedisHash("persons")
public class Person {

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

我们在这里有一个非常简单的域对象。 请注意,它有一个名为id的属性,用org.springframework.data.annotation.Id注解,并在其类型上注释@RedisHash。 这两个负责创建用于保存散列的实际Key。

用@Id注解的属性以及那些名为id的属性被视为标识符属性。 那些带有注释的人比其他人更喜欢。

现在实际上有一个负责存储和检索的组件,我们需要定义一个存储库接口。

示例6.坚持人员实体的基本知识库接口

public interface PersonRepository extends CrudRepository<Person, String> {

}

由于我们的知识库扩展了CrudRepository,它提供了基本的CRUD和查找器操作。 我们之间需要粘合在一起的东西是相应的Spring配置。

示例7. 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注入到组件中。

例子8. 访问 Person 实体

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

如果当前值为Null,则生成一个新的ID或重用已经设置的ID值并存储Person类型的属性

(1)里面的Redis的哈希键模式keyspace:id在这种情况下,例如。persons:5d67b7e1-8640-4475-BEEB-c666fab4c0e5。

(2) 使用提供的ID检索存储在keyspace:id处的对象

(3) 计算@RedisHash在Person上定义的键(key)空间人员中可用实体的总数。

(4)从Redis中移除给定对象的键(key)。

8.2. 对象到哈希映射

Redis Repository支持持久化Hashes中的对象。 这需要由RedisConverter完成的对象哈希转换。 默认实现使用Converter将属性值映射到和来自Redis本地byte[]

从前面的部分给出Person类型,默认的映射如下所示:

_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) 简单的属性值由路径映射。

(3) 复杂类型的属性由它们的点路径映射。

表7.默认映射规则

Type Sample Mapped Value
Simple Type (eg. String) String firstname = "rand"; firstname = "rand"
Complex Type (eg. Address) Address adress = new Address("emond’s field"); address.city = "emond’s field"
List of Simple Type List<String> nicknames = asList("dragon reborn", "lews therin"); nicknames.[0] = "dragon reborn", nicknames.[1] = "lews therin"
Map of Simple Type Map<String, String> atts = asMap({"eye-color", "grey"}, {"…​ atts.[eye-color] = "grey", atts.[hair-color] = "…​
List of Complex Type List<Address> addresses = asList(new Address("em…​ addresses.[0].city = "emond’s field", addresses.[1].city = "…​
Map of Complex Type Map<String, Address> addresses = asMap({"home", new Address("em…​ addresses.[home].city = "emond’s field", addresses.[work].city = "…​

映射行为可以通过在RedisCustomConversions中注册相应的Converter来定制。 这些转换器可以处理单个字节[]以及Map <String,byte []>的转换,而第一个适用于例如。 将一个复杂类型转换为例如。 二进制JSON表示仍然使用默认映射哈希结构。 第二个选项提供完全控制结果散列。 将对象写入Redis哈希将从哈希中删除内容并重新创建整个哈希,因此不会丢失映射的数据。

例9.byte[] 转换器

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

使用上述byte[] Converter, 例如:

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

示例10. Map<String,byte []> Converter

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

使用以上 MapConverter,例如:

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

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

8.3. Keyspaces

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

示例11.通过@EnableRedisRepositories进行Keyspace设置

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

例子12.编程的Keyspace设置

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

8.4. 二级索引

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

8.4.1.简单属性指数

鉴于示例Person实体,我们可以通过使用@Indexed注释属性来为firstname创建一个索引。

示例13.注解驱动的索引

@RedisHash("persons")
public class Person {

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

索引是为实际属性值而建立的。 保存两个Persons,例如。 “rand”和“aviendha”导致设置如下的索引。

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

在嵌套元素上也可以有索引。 假设地址有一个用@Indexed注解的城市属性。 在这种情况下,一旦person.address.city不为空,我们就为每个城市设置一个集合。

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

此外,编程设置允许定义映射键和列表属性上的索引。

@RedisHash("persons")
public class Person {

  // ... other properties omitted

  Map<String,String> attributes;      (1)
  Map<String Person> relatives;       (2)
  List<Address> addresses;            (3)
}

(1) SADD persons:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e

(2) SADD persons:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e

(3) SADD persons:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

Indexes will not be resolved on References.

与keyspaces 相同,可以配置索引而不需要注解实际的域类型。

示例14.通过@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("persons", "firstname"));
    }
  }
}

示例15.编程索引设置

@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("persons", "firstname"));
    }
  }
}

8.4.2. 地理空间索引(Geospatial Index)

假定地址类型包含一个类型为Point的属性位置,该位置保存特定地址的地理坐标。 通过使用@GeoIndexed注释属性,将使用Redis GEO命令添加这些值。

@RedisHash("persons")
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);     (1List<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) 使用点和距离查询嵌套属性的方法声明。

(2) 使用Circle在内搜索嵌套属性的查询方法声明。

(3) GEOADD persons:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e

(4) GEORADIUS persons:address:location 15.0 37.0 200.0 km

在上面的例子中,使用对象id作为成员的名字,使用GEOADD存储lon / lat值。 查找方法允许使用圆或点,距离组合来查询这些值。

不可能将 near/within 与其他标准组合在一起。

8.5. 存活时间(TTL)

存储在Redis中的对象只能在一段时间内有效。 这对于在Redis中保存短暂的对象特别有用,而不必在达到其寿命时手动删除它们。 以秒为单位的到期时间可以通过@RedisHash(timeToLive = ...)以及通过KeyspaceSettings来设置(请参阅Keyspaces)。

可以通过在数字属性或方法上使用@TimeToLive注释来设置更灵活的到期时间。 但是,不要在同一个类中的方法和属性上应用@TimeToLive。

例子16.过期

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

用@TimeToLive显式注释一个属性会从Redis回读实际的TTL或PTTL值。 -1表示该对象没有过期关联。

repository 实现确保通过RedisMessageListenerContainer订阅Redis keyspace 通知。

当到期被设置为正值时,执行相应的EXPIRE命令。 除了保留原始文件外,幻影副本在Redis中仍然存在,并在原始文件后5分钟到期。 这样做是为了使存储库支持在密钥过期时通过Springs ApplicationEventPublisher发布RedisKeyExpiredEvent持有过期值,即使原始值已经消失。 所有连接的应用程序将使用Spring Data Redis存储库接收到期事件。

默认情况下,初始化应用程序时,Key到期监听器被禁用。 可以在@EnableRedisRepositories或RedisKeyValueAdapter中调整启动模式,以启动应用程序的侦听器,或者在首次插入具有TTL的实体时启动侦听器。 有关可能的值,请参阅EnableKeyspaceEvents。

RedisKeyExpiredEvent将保存实际到期的域对象的副本以及Key。

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

keyspace 通知消息侦听器将改变Redis中的notify-keyspace-events设置(如果尚未设置的话)。 现有的设置不会被覆盖,所以留给用户的时候不要将它们留空。 请注意,在AWS ElastiCache上禁用了CONFIG,并使监听器导致错误。

Redis Pub / Sub消息不是持久的。 如果在应用程序关闭期间某个键过期,将不会处理到期事件,这可能会导致二级索引包含对已过期对象的静态引用。

8.6. 持久化

使用@Reference标记属性允许存储简单的键引用,而不是将值复制到散列本身。 在从Redis加载时,引用会自动解析并映射回对象。

示例17.示例属性参考

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

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

引用对象在保存引用对象时不会保留更改。 请确保分开保存对引用对象的更改,因为只有引用将被存储。 在引用类型的属性上设置的索引将不会被解析。

8.7. 持久化部分更新

在某些情况下,不需要加载和重写整个实体,只需在其中设置一个新的值即可。 最后一次活动时间的会话时间戳可能是这种情况下,你只是想改变一个属性。 PartialUpdate允许定义对现有对象的设置和删除操作,同时考虑更新实体本身以及索引结构的潜在到期时间。

示例18.示例部分更新

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的字段,而不必传入整个对象。 这在注册自定义转换时不起作用。

(3)删除属性 age

(4)设置复杂属性address

(5)设置一个map/collection的值删除以前存在的地图/集合,并用给定的值替换值。

(6)更改存活时间时自动更新服务器到期时间。

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

8.8. 查询和查询方法

查询方法允许从方法名称自动派生简单的查找器查询。

Example 19.Repository 查询方法

public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByFirstname(String firstname);
}

请确保在查询方法中使用的属性设置为索引。

Redis repositories 的查询方法仅支持查询具有分页的实体和实体集合。

使用派生查询方法可能并不总是足以对要执行的查询建模。 RedisCallback提供了对索引结构的实际匹配或者甚至自定义添加的更多控制。 所需要的就是提供一个RedisCallback,它返回一个或一组Iterable的id值。

Example 20. 使用 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支持的关键字的概述,以及包含关键字本质的转换方法。

Keyword Sample Redis snippet
And findByLastnameAndFirstname SINTER …:firstname:rand …:lastname:al’thor
Or findByLastnameOrFirstname SUNION …:firstname:rand …:lastname:al’thor
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals SINTER …:firstname:rand
Top,First findFirst10ByFirstname,findTop5ByFirstname

表8.方法名称内的支持的关键字

8.9. Redis Repositories running on Cluster

在集群的Redis环境中使用Redis存储库支持很好。 有关ConnectionFactory配置详细信息,请参阅Redis群集部分。 仍然需要考虑一些因素,因为默认的Key分配会将实体和二级索引分散到整个集群及其插槽中(slots)。

key type slot node
persons:e2c7dcee-b8cd-4424-883e-736ce564363e id for hash 15171 127.0.0.1:7381
persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 id for hash 7373 127.0.0.1:7380
persons:firstname:rand index 1700 127.0.0.1:7379

当所有相关Key映射到同一个插槽时,像SINTER和SUNION这样的命令只能在服务器端进行处理。 否则,计算必须在客户端完成。 因此,将Key固定到单个插槽(slot)是非常有用的,它允许立即使用Redis服务器计算。

key type slot node
{persons}:e2c7dcee-b8cd-4424-883e-736ce564363e id for hash 2399 127.0.0.1:7379
{persons}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 id for hash 2399 127.0.0.1:7379
{persons}:firstname:rand index 2399 127.0.0.1:7379

在使用Redis群集时,通过`@RedisHash(“{yourkeyspace}”)将keyspaces 定义并固定到特定的插槽(slot)。

8.10. CDI 集成

repository 接口的实例通常由一个容器创建,Spring是使用Spring Data时最自然的选择。 有复杂的支持来轻松设置Spring来创建bean实例。 Spring Data Redis附带一个自定义CDI扩展,允许在CDI环境中使用repository 抽象。 该扩展是JAR的一部分,所以您只需要将Spring Data Redis JAR放入类路径即可。

您现在可以通过为RedisConnectionFactory和RedisOperations实现CDI Producer来设置基础结构:

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环境而有所不同。

Spring Data Redis CDI扩展将拾取作为CDI bean提供的所有存储库,并在容器请求存储库类型的bean时为Spring Data存储库创建代理。 因此,获取Spring Data存储库的一个实例是声明一个@Injected属性的问题:

class RepositoryClient {

  @Inject
  PersonRepository repository;

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

Redis Repository 需要RedisKeyValueAdapter和RedisKeyValueTemplate实例。 如果没有找到提供的bean,则这些bean由Spring Data CDI扩展创建和管理。 但是,您可以提供自己的Bean来配置RedisKeyValueAdapter和RedisKeyValueTemplate的特定属性。

results matching ""

    No results matching ""