Spring Data Redis ----Redis仓库----笔记6

时间:2022-05-22 15:11:57

继续。。。。。

官方文档:http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/

7.Redis Repositories(Redis 仓库)

Redis 仓库允许你无缝转换和存储域对象到Redis Hashes,应用自定义mapping策略和使用二级索引。

警告:Redis Repositories 要求Redis 服务器版本为2.8.0+

7.1.使用

为了获取存储在Redis的域对象,你可以利用库存支持很容易实现。

例子 Person实体类

@RedisHash("persons")
public class Person {

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


在这里我们已经有一个实体类,注意属性Id上的注解org.springframework.data.annotation.Id 和 @RedisHash代表对应的类型。它们目的就是创建一个真实的key存储到hash中。

提示:使用注解@Id,考虑到id作为唯一标识符。

为了现在有一个组件可以存储和取用数据,我们需要定义仓库接口。

例子6.简单仓库接口去持久化Person实体类

public interface PersonRepository extends CrudRepository<Person, String> {

}

我们仓库类继承CrudRepository类,它提供增删改查功能。这些事情需要通过配置来整合在一起。

例子7,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
}
注释:
1.如果当前值不为空的话会生成一个新的Id,或者重复使用已经存在id 值和存储Person类型的属性。Person在hash中将会如下展示

keyspace:id persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5

2.通过id取出Person对象

3.计算实体类的对象的数量,被@RedisHash注解的Person

4.移除对象。

7.2.Object to Hash Mapping (对象映射成HashMapping)

Redis仓库支持Hashs持久化对象。它需要将会对象转换成Hash的转换工作。它可以被RedisConverter完成。 默认实现使用Converter的属性值来自于原生Redis byte[]。


如上例子中给出一个Person类型,它默认mapping映射如下所示:

_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.复杂的属性类型可以通过路径.进行映射 例如:address.city

默认映射规则
类型 例子 映射值
简单类型(例如字符串) String firstname="rand" firstname="rand"
复杂类型(例如地址) Address address= new Address("emond's field") address.city="emond's field"
List简单例子 List<String> nicknames=asList("dragon reborn", "lewstherin") nicknames.[0] ="dragon reborn",
nicknames.[1] ="lews therin"
Map简单例子 Map<String, String> atts = asMap({"eye-color", "grey"}, {"…​ atts.[eye-color] = "grey",
atts.[hair-color] = "…​
List复杂例子 List<Address> addresses = asList(new Address("em… addresses.[0].city = "emond’s field",
addresses.[1].city = "…​
Map复杂例子 Map<String, Address> addresses = asMap({"home", new Address("em…​ addresses.[home].city = "emond’s field",
addresses.[work].city = "…​

映射行为可以自定义,只需在CustomConversions注册对应的Converter转换器。这些转换器关心的是把单一byte[]变成Map<String, byte[]>,因此适合将一个复杂类型转换成二进制JSON,但这个JSON还是使用默认的hash结构。第二个选项提供了全控制hash结果。写一个对象到Redis hash中,将会删除hash中原来的内容。然后重写整个hash值。所以没有映射的数据将会丢失。

例子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[]转换将会产生如下结果

_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[]> 转换器

@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 转换器将会产生如下结果

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

提示:

自定义转换器对索引没有影响,将会为自定义转换类型创建二级索引。

7.3.Keyspaces (键命名空间)

键命名空间为新创建的实际key添加前缀,默认情况下前缀是getClass().getName().这个默认的前缀将会通过@RedisHash 设置或者设置程序的配置。然而。注解的键命名空间会取缔任何其他配置的键命名空间。

例子11.通过@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, "persons"));
}
}
}

例子12.通过程序配置设置键命名空间

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

7.4.(Secondary Indexes)二级索引

二级索引被用来基于原生Redis结构进行检索。在每一次保存或者通过对象删除和过期而移除操作都会保存与之对应的索引。

7.4.1. 简单的属性索引

给出一个简单对象Person实体类,我们可以为firstname创建一个索引,只要在属性上添加@Indexed注解

例子13. 注解驱动索引

@RedisHash("persons")
public class Person {

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

索引将会为实际的属性值设置。保存两个Person对象。例如“rand”和“aviendha” 结果,索引保存的结果如下

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

也有可能有索引在内部元素中。假设Address有一个city的属性,然后它被注解@Indexed。在这种情况下。一旦 person.address.city不为null,我们将会为city如下设置

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

更多的通过程序进行设置。允许定义一个索引作为map的key。如下所示

@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

警告:索引将不会在引用解决。

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


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

假设Address类型包含一个属性location是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.查询方法使用Point 和 Distance声明内部属性

2.查询方法使用Circle声明内部属性。

3.添加地址位置: persons:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e

4.检索附近半径  persons:address:location 15.0 37.0 200.0 km


以上例子中经度/纬度使用GEOADD进行存储。把对象id作为成员的名称。查询方法使用Circle或Point,Distance查询它的值。

注意:这个不可能使用其他规则合并 near/within 。

7.5. Time To Live(多久失效)

对象存储在Redis中可能只是验证一段时间。这对于持久化有效时间短的对象特别有用,当时间到期不需要手动移除该对象。设置失效方式有两种,既可以通过@RedisHash(timeToLive=...),也可以通过KeyspaceSettings(参考Keyspaces)


更多灵活过期的时间可以通过@TimeToLive注解实现。这个注解放置到数字类型的属性上例如Long,然而不要应用@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表明这个对象永不过期。


这个仓库的实现确保通过RedisMessageListenerContainer订阅 Redis keyspace notifications


通过EXPIRE命令将会执行一个正值的过期时间。除了一个复制原始的对象被持久化设置的过期时间为比原始对象多5分钟。这样做确保仓库支持发布RedisKeyExpiredEvent 来获取失效的值,它是通过Spring 的ApplicationEventPublisher获取。这样就不管一个key失效,还是原始的值已经移除。使用Spring Data Redis 仓库,失效时间将会收到所有连接的应用。


在默认情况,当初始化应用的时候,key失效监听是禁用的。启动模式将会被调整。它通过@EnableRedisRepositories 或者 RedisKeyValueAdapter 在应用中启动监听器或者在第一次插入TTL修饰的实体类,参考EnableKeyspaceEvents所有可能的值。

RedisKeyExpiredEvent可以复制失效对象作为key。

提示: 延迟或者在启动时候失效事件监听器不可用都会影响到RedisKeyExpiredEvent发布。一个不可用的事件监听器将不会发布失效事件。延迟启动会导致事件的丢失。

提示;如果它没有设置notify-keyspace-events的值,keyspace信息监听将会改变notify-keyspace-events的设置。存在的设置将不会被重写。所有用户设置正确的值而不要为空。请注意CONFIG在AWS ElasticCache不可用,如果开启将会报错。

提示:Redis 订阅/发布信息将会被存储。如果一个键失效了,这个应用将会移除过期事件,这个将导致二级索引仍然会引用过期的对象

7.6. Persisting References(持久引用)

在属性添加注解@Reference允许存储一个简单的key去引用一个对象而不是复制对象。在从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.引用存储整个引用对象的key(keyspace:id)


警告:当保存这个引用的对象的时候不属于持久化的改变。只要引用被存储,请确保单独在引用对象进行持久化改变。引用类型的索引将不会改变。

7.7 Persisting Partial Updates(持久化更新部分内容)

在有些情况下,我们并不需要加载或重写整个实体对象。而只是设置一个新的值。你想保存最近会话活跃的时间戳属性。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's field而改变整个对象。当自定义转换器被注册的整个方法将会不可用

3.移除属性age

4.设置复杂类型address
5. 设置一个map集合的值,将会移除先前已经的存在的值。

6.当改变Time To Live 将会自动更新服务器的失效时间。

提示:

更新复杂对象也就是map集合结构要求更深Redis的交互,整个实体重写可能会比局部更新对象更快一些。

7.8.Queries and Query Methods ( 查询和查询方法)

查询方法允许从方法名自动衍生出一个简单查询器

例子19. 简单仓库查询方法

public interface PersonRepository extends CrudRepository<Person, String> {

List<Person> findByFirstname(String firstname);
}

提示:为了确保属性作为查询方法的参数,请为属性设置索引

提示:对于Redis 仓库查询方法只支持查询实体和包含实体的集合分页。

使用衍生查询方法可能并不一直有效。RedisCallback提供更多实际匹配索引的结构或者甚至是用户自定义添加的。所有这些都提供了一个RedisCallback,它将会返回单个或Iterable (id的集合)

例子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支持的关键字。方法包含关键字进行转换。

在方法支持的关键字
关键字 例子 Redis 片段
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  

7.9. Redis Repositories running on Cluster (Redis仓库运行在集群上)

在集群Redis环境使用Redis 仓库也是很好的。详情参考Redis Cluster章节去获取ConnectionFactory的配置详情。仍然有些需要考虑,例如默认分布式的key将会分配到实体和二级索引,它是通过整个集群和它slots完成的。

类型 slot 节点
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映射到同一个slot的时候,有些命令例如SINTER和SUNION将只能在服务端进行处理。所以不得不在客户端计算。因此可以在同一slot中使用pin keyspace(就是设置前缀),这样的话允许在服务端进行计算。

类型 slot 节点
{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

提示:当使用Redis集群的时候,定义设置pin keyspaces 可以通过“@RedisHash({yourkeyspace})” 定义具体的slots

7.10 CDI integration (CDI 整合)

实例化一个库存接口通过由容器进行创建。使用SpringData,那么Spring自然而然最好的选择。它很容器支持Spring去实例化bean。Spring Data Redis 装载自定义CDI扩展。它允许在CDI环境使用仓库抽象。这个扩展是jar一部分。所以你需要激活它,把Spring Data Redis jar 添加到你的classpath路径下。

CDI(Contexts And Dependency Injection)是JavaEE 6标准中一个规范,将依赖注入IOC/DI上升到容器级别, 它提供了Java EE平台上服务注入的组件管理核心,简化应该是CDI的目标,让一切都可以被注解被注入。

你现在可以通过实现一个CDI Producer 为RedisConnectionFactory和RedisOperations进行基础设置。

class RedisOperationsProducer {


@Produces
RedisConnectionFactory redisConnectionFactory() {

JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName("localhost");
jedisConnectionFactory.setPort(6379);
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 beans 和 为Spring Data 仓库创建代理。无论什么时候需要都可以调用。 所以获取一个Spring Data 仓库关键就是在属性上添加注解@Injected

class RepositoryClient {

@Inject
PersonRepository repository;

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

Redis 仓库要求 RedisKeyValueAdapter 和 RedisKeyValueTemplate实例。如果没有提供的话。Spring Data CDI扩展将会创建管理这两个bean。当然你可以提供你自己的Bean去配置RedsiKeyValueAdapterRedisKeyValueTemplate.


总结:除了附录,整个文档初略翻译完了,会有很多错误,大家可以讨论提出一下,相互学习。