让我们从“你如何使用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”。
该@Indexed
注释意味着它将被索引以供搜索。如果不指定该注解,则该字段将保存在数据库中,但搜索该字段将返回空结果。您可以根据需要添加此注释。数据库中已有的数据将被异步索引;新数据将同步索引。
接下来,我们将创建一个存储库,它的主要作用是从数据库中获取数据。
关系数据库的示例:
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 允许您使用@Query
注释自己编写查询,就像在 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,我们只需指定该“WHERE”
部分的条件即可。没有必要指出需要附加哪些字段,因为它们总是从数据库中提取。但是,我们无法提取所有字段,而是使用附加“returnFields”
参数指定我们到底需要什么。您还可以指定排序、限制和偏移量 - 顺便说一下,后者在 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 的一些细微差别:
@id
:{2e5af82m\-02af\-553b\-7961\-168878aa521е}
还有一件事:如果您在RedisDocumentRepository
存储库中搜索,则没有任何效果,因为代码中有这样一个表达式将删除所有屏幕:
String regex = "(\\$" + key + ")(\\W+|\\*|\\+)(.*)";
因此,为了按这些字段进行搜索,您必须直接在 RediSearch 中编写查询。我有一个如何在演示项目中执行此操作的示例。
RedisDocumentRepository
方法时,如果您期望一个集合,则必须传递一个Pageable
指示期望行的大小或在@Query
;中指定大小的参数。否则,您最多将收到 10 条记录。FT.SEARCH (@Query)
方法仅支持一个参数进行排序。这是通过编写查询来解决的FT.AGGREGATE (@Aggregation)
。上述列表并不详尽。在使用这些库时,我发现了许多不同的东西,但这只是数据库实现的特殊性。最后,我没有在本文中放入有关 Redis 插件的信息,也没有谈论 Redis OM Spring 的所有功能;不然这篇文章会很大,不可读。
我展示了目前,Redis 允许您存储具有大嵌套的对象,并允许您搜索该对象的字段。如果您通过存储库中的抽象来处理数据,那么有些人可能看不出与 Spring Data JPA 有任何区别,特别是如果您使用一些简单的查询,例如 、 、Save
等delete
,findAllBy
以及通过方法名称进行查询。