Spring Data Redis (SDR) 框架使编写使用 Redis 作为 Java 对象 (POJO) 存储的 Spring 应用程序变得容易,因为它消除了通过 Spring 的出色基础设施支持与存储进行交互所需的冗余任务和样板代码。
Redis OM Spring 在 SDR 之上构建,通过利用 Redis 丰富的模块生态系统来改善和优化与 Redis 的交互。对于使用 SDR 的 @RedisHash
注释映射的 Java 对象,我们通过以下方式增强了对象映射:
您将构建一个将 User
POJO (普通旧 Java 对象) 存储为 Redis 哈希的应用程序。
我们将使用 Spring Initializr创建基本 SpringBoot 应用程序。您可以使用此 预初始化的项目 并单击生成以下载 ZIP 文件。该项目已配置为适合本教程中的示例。
配置项目
roms-hashes.zip
),它是一个配置了您选择的 Web 应用程序的存档。包含的依赖项
如果您的 IDE 集成了 Spring Initializr,您可以在 IDE 中完成此过程。
您也可以从 Github 分叉项目,并在 IDE 或其他编辑器中打开它。
要使用 Redis OM Spring,请打开 pom.xml
文件,并将 Redis OM Spring Maven 依赖项添加到 pom.xml 文件的 dependencies
元素中
<dependency>
<groupId>com.redis.om</groupId>
<artifactId>redis-om-spring</artifactId>
<version>0.5.2-SNAPSHOT</version>
</dependency>
请查看官方的 Redis OM Spring GitHub 存储库 以获取最新版本信息
如果使用 gradle,请按如下方式添加依赖项
dependencies {
implementation 'com.redis.om.spring:redis-om-spring:0.1.0-SNAPSHOT'
}
生成的应用程序包含单个 Java 文件,即 @SpringBootApplication
注释的主应用程序。要启用 Spring Data Redis 存储库,我们还需要使用 @EnableRedisEnhancedRepositories
以及 @Configuration
注释来注释主类或配置类。
package com.redis.om;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.redis.om.spring.annotations.EnableRedisEnhancedRepositories;
@SpringBootApplication
@EnableRedisEnhancedRepositories(basePackages = "com.redis.om.hashes.*")
public class RomsHashesApplication {
public static void main(String[] args) {
SpringApplication.run(RomsHashesApplication.class, args);
}
}
Redis OM Spring 依赖于 Redis Stack 的强大功能。下面的 docker compose YAML 文件可以快速入门。您可以将其放在项目的根文件夹中,并将其命名为 docker-compose.yml
version: '3.9'
services:
redis:
image: 'redis/redis-stack:latest'
ports:
- '6379:6379'
volumes:
- ./data:/data
environment:
- REDIS_ARGS: --save 20 1
deploy:
replicas: 1
restart_policy:
condition: on-failure
要启动 docker compose 应用程序,请在命令行(或通过 Docker Desktop)中克隆此存储库,并运行(从根文件夹)
docker compose up
我们还将启动 Redis CLI 的一个实例,以便我们能够监视 ROMS 的操作。为此,我们将 Redis 以监视模式启动
redis-cli MONITOR
我们的应用程序中将只有一个类,即 User
类。我们将使用 lombok 来避免创建 getter 和 setter。我们将使用 lombok 注释 @Data
、@RequiredArgsConstructor
和 @AllArgsConstructor
。
最后,要将类标记为 JSON 文档,我们将使用 @Document
注释。
package com.redis.om.hashes.domain;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
import com.redis.om.spring.annotations.Bloom;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@RedisHash
public class User {
@Id
private String id;
@Indexed @NonNull
private String firstName;
@Indexed
private String middleName;
@Indexed @NonNull
private String lastName;
@NonNull
@Indexed
String email;
}
我们使用 Spring Data Redis @RedisHash
注释。名为 id
的属性使用 org.springframework.data.annotation.Id
注释。这两个项目负责创建用于在 Redis 中持久保存哈希的实际密钥。
该 User
类包含一个 firstName
、middleName
和 lastName
,以及一个 email
属性。
package com.redis.om.hashes.repositories;
import java.util.List;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.redis.om.hashes.domain.User;
@Repository
public interface UserRepository extends CrudRepository<User, String> {
}
让我们通过修改 RomsHashesApplication
类来添加一些 User
POJO 到应用程序启动时的 Redis 中,使用 @Autowired
注解包含新创建的 UserRepository
。然后我们将使用一个 CommandLineRunner
@Bean
注解的方法创建四个 User
POJO 并将它们保存到数据库中。
在 CommandLineRunner
中,我们执行以下步骤:
deleteAll
方法清除数据库(在生产环境中要小心!🙀)User
实例;我们将使用 Rage Against the Machine 的四名乐队成员。saveAll
方法批量保存所有 User
POJO。package com.redis.om.hashes;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.redis.om.hashes.domain.User;
import com.redis.om.hashes.repositories.UserRepository;
import com.redis.om.spring.annotations.EnableRedisEnhancedRepositories;
@SpringBootApplication
@Configuration
@EnableRedisEnhancedRepositories(basePackages = "com.redis.om.hashes.*")
public class RomsHashesApplication {
@Autowired
private UserRepository userRepo;
@Bean
CommandLineRunner loadTestData() {
return args -> {
userRepo.deleteAll();
User john = User.of("Zack", "de la Rocha", "zack@ratm.com");
User tim = User.of("Tim", "Commerford", "tim@ratm.com");
User tom = User.of("Tom", "Morello", "tom@ratm.com");
User brad = User.of("Brad", "Wilk", "brad@ratm.com");
userRepo.saveAll(List.of(john, tim, tom, brad));
};
}
public static void main(String[] args) {
SpringApplication.run(RomsHashesApplication.class, args);
}
}
由于我们使用的是 Spring Boot DevTools,如果你已经运行了应用程序,它应该已经重新启动/重新加载。如果没有,请使用 mvn
命令启动应用程序。
./mvnw spring-boot:run
如果一切按预期进行,你应该会看到熟悉的 Spring Boot 横幅飞过。
[INFO] --- spring-boot-maven-plugin:2.6.0-M1:run (default-cli) @ roms-documents ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.0-M1)
2021-11-30 09:45:58.094 INFO 36380 --- [ restartedMain] c.r.o.d.RomsDocumentsApplication : Starting RomsDocumentsApplication using Java 11.0.9 on BSB.lan with PID 36380 (/Users/bsb/Code/module-clients/java/high-level/redis-om/redis-om-spring/demos/roms-documents/target/classes started by briansam-bodden in /Users/bsb/Code/module-clients/java/high-level/redis-om/redis-om-spring/demos/roms-documents)
如果你在观察 Redis CLI 监视器,你应该会看到大量输出飞过。让我们分解它并使用另一个 Redis CLI 检查它,以便了解系统的内部工作原理。
在顶部你应该会看到 FT.CREATE
命令,它使用我们 POJO 中的注解来确定索引配方。由于我们的 POJO 用 @Document
注解,我们得到了一个针对以 com.redis.om.documents.domain.Company:
开头的任何键的 ON JSON
索引(这是 Spring Data Redis 和 ROMS 的默认键前缀)。
1638336613.156351 [0 172.19.0.1:63396] "FT.CREATE" "UserIdx" "ON" "HASH" "PREFIX" "1" "com.redis.om.hashes.domain.User:" "SCHEMA" "firstName" "AS" "firstName" "TAG" "middleName" "AS" "middleName" "TAG" "lastName" "AS" "lastName" "TAG" "email" "AS" "email" "TAG"
ROMS 使用用 @Indexed
或 @Searchable
注解的 POJO 字段来构建索引模式。在 User
POJO 的情况下,我们有 firstName
、middleName
、lastName
和 email
字段都被注解为“可索引的”,这意味着我们可以对这些字段进行精确搜索。
Spring Data Redis 创建一个 SET 来维护我们实体的主键,ROMS 从 SDR 继承了此功能。在索引创建后的 DEL
命令是由我们数据加载方法中对 userRepo.deleteAll();
的调用触发的。如果我们已经保存了任何对象,我们也会看到删除这些单独实例的调用。
对于每个 User
POJO,我们应该会看到一系列 REDIS 命令,如:
1638340447.180533 [0 172.19.0.1:63398] "SISMEMBER" "com.redis.om.hashes.domain.User" "01FNTB6JWTQHMK7NTEYA8725MP"
1638340447.198702 [0 172.19.0.1:63398] "DEL" "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
1638340447.200874 [0 172.19.0.1:63398] "HMSET" "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP" "_class" "com.redis.om.hashes.domain.User" "email" "zack@ratm.com" "firstName" "Zack" "id" "01FNTB6JWTQHMK7NTEYA8725MP" "lastName" "de la Rocha"
1638340447.203121 [0 172.19.0.1:63398] "SADD" "com.redis.om.hashes.domain.User" "01FNTB6JWTQHMK7NTEYA8725MP"
首先,SDR 使用 SISMEMBER
命令检查对象是否已存在于 Redis SET 的主键中。然后,会发出 DEL
命令来删除 Hash,接着使用 HMSET
命令写入新的或更新的 Hash。最后,使用 SADD
命令将对象的 id
属性添加到主键集中。
让我们使用 Redis CLI 检查数据。我们将从列出以 com.redis.om.hashes.domain.User
为前缀的键开始。
127.0.0.1:6379> SCAN 0 MATCH com.redis.om.hashes.domain.User*
1) "0"
2) 1) "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
2) "com.redis.om.hashes.domain.User:01FNTB6JZ2NSQNST3BBH1J1039"
3) "com.redis.om.hashes.domain.User:01FNTB6JYP4X15EAF08YBK55WR"
4) "com.redis.om.hashes.domain.User:01FNTB6JYXAZ6H7AJ9ZWHHW73H"
5) "com.redis.om.hashes.domain.User"
我们有 5 个匹配项,每个创建的 User
POJO 都有一个,再加上用于主键的 Redis SET。让我们检查一些值。
让我们检查存储在 com.redis.om.documents.domain.Company
键中的数据类型。
127.0.0.1:6379> TYPE "com.redis.om.hashes.domain.User"
set
知道了它是一个 Redis SET,让我们使用 SMEMBERS
命令检查它的内容。
127.0.0.1:6379> SMEMBERS "com.redis.om.hashes.domain.User"
1) "01FNTB6JZ2NSQNST3BBH1J1039"
2) "01FNTB6JWTQHMK7NTEYA8725MP"
3) "01FNTB6JYXAZ6H7AJ9ZWHHW73H"
4) "01FNTB6JYP4X15EAF08YBK55WR"
该集合包含我们所有用户的 Id。现在,让我们调查 com.redis.om.documents.domain.Company:01FNRW9V98CYQMV2YAB7M4KFGQ
键。
127.0.0.1:6379> TYPE "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
hash
存储的 Redis 数据类型是一个 hash
(Redis Hash)。让我们使用 HGETALL
命令检查它的内容。
127.0.0.1:6379> HGETALL "com.redis.om.hashes.domain.User:01FNTB6JWTQHMK7NTEYA8725MP"
1) "_class"
2) "com.redis.om.hashes.domain.User"
3) "email"
4) "zack@ratm.com"
5) "firstName"
6) "Zack"
7) "id"
8) "01FNTB6JWTQHMK7NTEYA8725MP"
9) "lastName"
10) "de la Rocha"
ROMS 最引人注目的功能是在运行时从存储库接口自动创建存储库实现。
让我们从 UserRepository
中的简单方法声明开始,该方法将根据用户姓氏查找唯一实例的 User
。
package com.redis.om.hashes.repositories;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.redis.om.hashes.domain.User;
@Repository
public interface UserRepository extends CrudRepository<User, String> {
Optional<User> findOneByLastName(String lastName);
}
ROMS 使用方法名称、参数和返回类型来确定要生成的正确查询,以及如何打包和返回结果。
findOneByLastName
返回一个 Optional
的 User
,这告诉 ROMS 如果未找到实体,则返回空有效负载。findOne
部分也强调即使有多个结果,我们也只对获取一个结果感兴趣。ROMS 解析方法名称以确定预期参数的数量,方法的 ByLastName
部分告诉我们我们期望一个名为 lastName
的参数。
让我们创建一个 REST 控制器来测试 findOneByLastName
方法。在 com.redis.om.hashes.controllers
包下创建 UserController
,如所示。
package com.redis.om.hashes.controllers;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.redis.om.hashes.domain.User;
import com.redis.om.hashes.repositories.UserRepository;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("name/{lastName}")
Optional<User> byName(@PathVariable("lastName") String lastName) {
return userRepository.findOneByLastName(lastName);
}
}
在我们的控制器中,我们包含了我们的 UserRepository
,并创建了一个简单的函数来响应 /api/users/name/{lastName}
上的 GET 请求,其中 {lastName}
将是我们作为 lastName
传递的字符串参数,用于查找。
让我们使用 CURL 测试端点,并传递精确的公司名称 Redis
。
➜ curl --location --request GET 'https://localhost:8080/api/users/name/Morello'
{"id":"01FNTB6JYXAZ6H7AJ9ZWHHW73H","firstName":"Tom","middleName":null,"lastName":"Morello","email":"tom@ratm.com"}
让我们格式化结果 JSON。
{
"id": "01FNTB6JYXAZ6H7AJ9ZWHHW73H",
"firstName": "Tom",
"middleName": null,
"lastName": "Morello",
"email": "tom@ratm.com"
}
检查 Redis CLI MONITOR,我们应该会看到发出的查询。
1638342334.137327 [0 172.19.0.1:63402] "FT.SEARCH" "UserIdx" "@lastName:{Morello} "
假设我们要根据 lastName
和 firstName
的组合查找 Users
,我们可以向存储库接口添加查询声明,如下所示。
List<User> findByFirstNameAndLastName(String firstName, String lastName);
在这种情况下,方法 findByFirstNameAndLastName
被解析,并且使用 And
关键字来确定该方法期望两个参数;firstName
和 lastName
。
为了测试它,我们可以向我们的控制器添加以下内容。
@GetMapping("/q")
public List<User> findByName(@RequestParam String firstName, @RequestParam String lastName) {
return userRepository.findByFirstNameAndLastName(firstName, lastName);
}
使用 CURL 测试我们
➜ curl --location --request GET 'https://localhost:8080/api/users/q?firstName=Brad&lastName=Wilk'
[{"id":"01FNTE5KWCZ5H438JGB4AZWE85","firstName":"Brad","middleName":null,"lastName":"Wilk","email":"brad@ratm.com"}]
格式化结果 JSON,我们可以看到 Brad Wilk
的记录作为 JSON 数组结果的唯一元素被返回。
[
{
"id": "01FNTE5KWCZ5H438JGB4AZWE85",
"firstName": "Brad",
"middleName": null,
"lastName": "Wilk",
"email": "brad@ratm.com"
}
]
回到 Redis CLI 监视器,我们可以看到由我们存储库方法生成的查询。
1638343589.454213 [0 172.19.0.1:63406] "FT.SEARCH" "UserIdx" "@firstName:{Brad} @lastName:{Wilk} "
Redis OM Spring 扩展了 Spring Data Redis,通过使用 Redis 本地的搜索和查询引擎,它提供了与 JPA 查询相媲美的搜索功能。