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); (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) 使用点和距离查询嵌套属性的方法声明。
(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的特定属性。