让我们从问题“你如何使用Redis?”开始。我相信大多数人将其用作服务的缓存。但是,我希望你知道它不仅仅可以用于缓存。最近,我在一篇文章中看到一份报告,介绍了如何将部分数据迁移到Redis,并将请求首先发送到Redis。现在,我想说的是不是我们如何应用它,而是在使用Spring及其抽象时,我们可能不会立即注意到的替代情况。 让我们尝试编写一个小的Spring应用程序,它将使用两个PostgreSQL和Redis数据库。我想指出的是,我们将在数据库中存储的不是某种扁平对象,而是一个带有嵌套字段(内连接)的关系数据库的完整对象。为此,我们需要安装在Redis中的插件,例如RedisJSON和RediSearch。第一个插件允许我们以JSON格式存储对象,而第二个插件允许我们按照对象的任何字段进行搜索,甚至是嵌套字段。 为了与关系数据库一起工作,我们将选择Spring Data JPA。而为了与Redis一起工作,我们将使用出色的Redis OM Spring库,该库允许在抽象级别上与数据库进行交互。这类似于Data JPA。在底层,Redis OM Spring具有与Spring和Jedis一起使用数据库所需的所有依赖项。
先看代码
我们先开始编写代码。假设我们需要将某个实体写入数据库。在这个实体中,我添加了其他对象,例如"downtime"、"place"、"reason"等。关系数据库的实体代码如下所示:
@Entity
@Table(schema = "test", name = "downtime")
public class Downtime {
@Id
private String id;
private LocalDateTime beginDate;
private LocalDateTime endDate;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "area")
private Place area;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "cause")
private Cause cause;
...
这段代码不需要注释。我们需要为Redis做同样的事情。对象为Redis:
@Document
public class DowntimeDoc {
@Id
@Indexed
private String id;
@Indexed
private LocalDateTime beginDate;
private LocalDateTime endDate;
@Indexed
private PlaceDoc area;
@Indexed
private CauseDoc cause;
....
这种情况下,我们使用@Entity
注解而不是@Document
注解。这个注解表示我们的对象是一个实体,它将以“包路径+类名+Idx”的键存储在数据库中。@Entity
注解表示该字段将被索引以供搜索。如果不指定此注解,那么该字段将保存在数据库中,但搜索它将返回一个空结果。您可以根据需要添加此注解。已经存在于数据库中的数据将异步进行索引;新数据将同步进行索引。接下来,我们将创建一个仓库,它主要用于从数据库中获取数据。关系数据库的示例代码如下所示:
public interface DowntimeRepository extends JpaRepository {
}
Redis的例子:
public interface DowntimeRedisRepository extends RedisDocumentRepository {
}
不同之处在于我们扩展了RedisDocumentRepository
接口,该接口扩展了Spring的标准CRUD接口。让我们添加一个方法来查找指定原因的第一个停机时间。
public interface DowntimeRepository extends JpaRepository {
Downtime findFirstByCauseIdOrderByBeginDate(String causeId);
}
Redis也是如此:
public interface DowntimeRedisRepository extends RedisDocumentRepository {
DowntimeDoc findTopByCause_IdOrderByBeginDateAsc(String causeId);
}
正如你注意到的,如果通过抽象层编写与数据库交互的代码,几乎看不出差异。此外,Redis OM Spring允许使用注解自己编写查询,就像在Spring Data JPA中一样。这是一个HQL查询的示例:
@Query("SELECT d FROM Downtime d" +
" JOIN FETCH d.area " +
" JOIN FETCH d.cause" +
" JOIN FETCH d.fixer" +
" JOIN FETCH d.area.level " +
" WHERE d.area IN ?1 AND (d.beginDate BETWEEN ?2 AND ?3 OR d.cause IN ?4) ")
List findAllByParams(List workPlace, LocalDateTime start, LocalDateTime end, List causes);
Redis也一样:
@Query("(@area_id:{$areas} ) & (@beginDate:[$start $end] | @cause_id:{$causes})")
Page findByParams(@Param("areas") List areas,
@Param("start") long start,
@Param("end") long end,
@Param("causes") List causes, Pageable pageable);
在Redis中,我们只需指定分段的条件。不需要指定需要附加的字段,因为它们总是从数据库中获取的。但是,我们可以通过附加参数来指定我们需要的字段,而不是拉取所有字段。您还可以指定排序、限制和偏移量 - 顺便说一句,HQL中无法指定偏移量。在这个示例中,我传递了Pageable
参数给方法,它将在数据库层面上工作,而不是将所有数据拉取到服务中,然后在服务中进行修剪(这与Hibernate的情况相反)。此外,Redis OM Spring还允许您使用EntityStream
编写查询,它类似于Stream API。以下是使用EntityStream
的上述查询的示例:
…
entityStream
.of(DowntimeDoc.class)
.filter(DowntimeDoc$.AREA_ID.in(filter.getWorkPlace().toArray(String[]::new)))
.filter(between + " | " + causes)
.map(mapper::toEntity)
.collect(Collectors.toList());
在这个示例中,我使用了一个过滤器,使用元模型将参数作为字符串传递给第二个过滤器,以显示这两种选项都是有效的。结果是:EntityStream
接受一组中间操作,并在调用终端操作时执行这组操作。
Redis OM Spring的细微差别
让我讲一些使用Redis OM Spring的细节:
- 无法将UUID用作主键。但是可以将其指定为字符串,并对其进行索引。但是在搜索时,需要转义空格
:@id
。
{2e5af82m-02af-553b-7961-168878aa521е}
还有一件事:如果通过存储库进行搜索,将无法正常工作,因为代码中有一个表达式将删除所有的转义字符:RedisDocumentRepository
。
String regex = "(\$" + key + ")(\W+|\*|\+)(.*)";
因此,为了按这些字段搜索,需要直接在RediSearch中编写查询。我在演示项目中有一个示例,可以演示如何做到这一点。
-
当通过方法搜索时,如果期望返回一个集合,则必须传递一个指示预期行数的参数或在
@Query
中指定大小;否则,将最多只能收到10条记录。 -
该方法仅支持一个参数进行排序。可以通过编写查询来解决这个问题,使用
.FT.SEARCH
(@Query
)或.FT.AGGREGATE
(@Aggregation
)方法。
上面的列表并不详尽。在使用这些库时,我发现了许多不同的东西,但这只是数据库实现的特定性。还有关于Redis插件的信息和讨论Redis OM Spring的所有功能。
结论
我展示了目前Redis允许存储具有大量嵌套的对象,并允许通过该对象的字段进行搜索。如果通过存储库中的抽象来处理数据,那么一些人可能看不出与Spring Data JPA的任何区别,特别是如果使用一些简单的查询,如save
、delete
、findAllBy
等,以及通过方法名称进行查询。
作者:Artem Artemev
更多技术干货请关注公众号“云原生数据库”
squids.cn,目前可体验全网zui低价RDS,免费的迁移工具DBMotion、SQL开发工具等。