Redis 可以用作关系数据库吗?

2023年 8月 18日 70.5k 0

​让我们从问题“你如何使用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的任何区别,特别是如果使用一些简单的查询,如savedeletefindAllBy等,以及通过方法名称进行查询。

作者:Artem Artemev

更多技术干货请关注公众号“云原生数据库”

squids.cn,目前可体验全网zui低价RDS,免费的迁移工具DBMotion、SQL开发工具等。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论