使用 Spring Data 以 Redis 作为数据存储来构建应用 - 第 1 部分
在介绍 Redis 的系列文章的第一部分里面,我介绍了 Redis 数据存储是什么、Redis 支持的数据类型,以及 Redis 的使用方法。在本文里面,我将介绍 Java 开发者使用 Spring Data 访问 Redis 并执行操作的编程方式。Spring Data 是一个用于构建基于 Spring 的、使用各种新型数据访问技术(如非关系数据库,map-reduce 框架和基于云的数据服务)的应用程序的一个项目。Spring Data 有很多对特定数据存储提供支持的子项目。不过现在我们只会关注 spring-data-keyvalue 这一子项目,并且只会讨论其对 Redis 键值存储的支持。spring-data-keyvalue 还为另一个名为 Riak 的键值对存储提供了支持,但本文会将话题限制在 Redis 领域之内。
SDKV(spring-data-keyvalue)项目提供了对现有 Redis 客户端(如 Jedis 和 JRedis)的抽象。它简化了与 Redis 交互所需的模板代码,让使用 Redis 键值对存储变得非常容易。SDKV 还提供了一个名为 RedisTemplate
的用来和 Redis 交互的通用模板类,它与 JDBCTemplate
或 HibernateTemplate
非常类似。这减轻了开发人员学习初级 API 的难度。
准备工作
在构建应用之前,要先确保你有这些东西:
- Redis(Windows 用户也可以从 dmajkic git 仓库 下载Redis )
- Java V6 JDK
- Apache Maven V2.0.9 或更高版本
- Spring Tool Suite(STS)
- Git
如果你在安装 Redis 服务器的时候遇到了问题,请参阅我以前的文章。
构建 spring-data-keyvalue 源码
本文将使用 spring-data-keyvalue 项目的当前开发版本(1.0.0.M2)。要获得最新的源码,就必须通过以下 Git 命令拿到 spring-data-keyvalue 这个项目:
git clone git://github.com/SpringSource/spring-data-keyvalue.git
该命令会创建一个 spring-data-keyvalue 文件夹。这一文件夹将会包含所有源代码。我们只有把这个源代码构建起来,你的本地 maven 仓库里面才会有这个项目的构件(artifact)。在构建项目之前,还必须使用 redis-server 命令来启动 Redis。在 Redis 运行起来之后,对项目运行 mvn clean install 命令,项目才会构建起来。其中项目的构件会放入本地 maven 存储库中。
使用 STS(Spring Tool Suite)创建一个模板项目
我们需要创建一个 Spring 模板项目,以便我们可以以它为基础构建我们的简单应用。而要创建一个模板项目,我们要打开 STS 并打开 File - New - Spring Template Project - Simple Spring Spring Template Project,然后在弹出的提示框里点击 Yes,接着输入项目名称和默认包名称并确认。这将在 STS 工作区中创建一个由我们命名的的简单模板项目。我接下来使用的项目名称为 “dictionary”,默认包名是 “com.redis.dictionary”。
修改 pom.xml
我们在上面创建的项目没把 SDKV 导入为依赖项。为了导入 SDKV,这一项目的 pom.xml 就得改为以下这个样子。
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shekhar</groupId>
<artifactId>redis</artifactId>
<name>redis-dictionary</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.6</java-version>
<org.springframework-version>3.0.5.RELEASE</org.springframework-version>
<org.springframework.roo-version>1.0.2.RELEASE</org.springframework.roo-version>
<org.aspectj-version>1.6.9</org.aspectj-version>
<redis.version>1.0.0.M2-SNAPSHOT</redis.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- 在会导入 log4j 的前提下,我们便不需要再导入 common-logging(基于 SLF4J) 了 -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- JUnit 测试框架 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>${redis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue-core</artifactId>
<version>${redis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-maven-milestone</id>Springframework Maven Repository
<url>http://maven.springframework.org/milestone</url>
</repository>
<repository>
<id>spring-maven-snapshot</id>
<snapshots>
<enabled>true</enabled>
</snapshots>Springframework Maven SNAPSHOT Repository
<url>http://maven.springframework.org/snapshot</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java-version}</source>
<target>${java-version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>install</id>
<phase>install</phase>
<goals>
<goal>sources</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
构建字典应用
现在我们完成了使用 Redis 和 Spring Data 所需的所有初步工作。我们不妨用它们来构建一个小应用,来让我们理解使用 SDKV API 和 Redis 交互的方式。
我们要构建的应用是一个简单的字典应用程序。它需要我们在 Redis 数据存储上执行一些 CRUD(Create、Read、Update、Delete,增删改查)操作。字典是一个单词的集合,每个单词可以有多种含义。这一字典应用程序的数据可以很简单地归纳为 Redis 的 List 数据类型,其中由特定的单词作为列表的键,由这以单词的各种含义作为其值。比如我们可以将 “astonishing” 这个词作为一个键,将 “astounding”、“staggering” 作为它的值。如果你希望每个单词的含义应该是唯一的,你也可以用 Set 来代替 List。
我们先使用 redis-cli 创建一个简单的词汇表。我们要先执行 redis-server 来启动 Redis,然后执行 redis-cli 来启动 Redis 的客户端。
redis> RPUSH astonishing astounding(integer) 1
redis> RPUSH astonishing staggering(integer) 2
redis> LRANGE astonishing 0 -11) "astounding"2) "staggering"
上面的 redis 命令创建了一个名为 "astonishing" 的列表,并且将 "astounding"、"staggering" 这些 "意义" 加到了以 "astonishing" 为键的列表里面,并且在接下来使用了 LRANGE 命令读取了 "astonishing" 列表的值。
这样我们便见识到了使用 redis-cli 进行 CRUD 操作的方法。
我们再创建一个名为 DictionaryDao
的类,让这个类用 SDKV API 在 Redis 上执行 CRUD 操作来达到和我们在 redis-cli 进行的操作一样的效果。正如在之前所说的,SDKV 项目中的核心类是 RedisTemplate
,而我们也会在 Dictionary
类中注入 RedisTemplate
来实现各种操作:
import org.springframework.data.keyvalue.redis.core.RedisTemplate;
public class DictionaryDao {
private RedisTemplate<String, String> template;
public DictionaryDao(RedisTemplate template) {
this.template = template;
}
public Long addWordWithItsMeaningToDictionary(String word, String meaning) {
Long index = template.opsForList().rightPush(word, meaning);
return index;
}
}
RedisTemplate
提供了像 ValueOperations
,ListOperations
,SetOperations
,HashOperations
和 ZSetOperations
这些类型的键的操作。在以上代码中,我使用了 ListOperations
来把新单词存储在了 Redis 数据存储里面。由于我们正在使用 rightPush
操作,因此单词的意义会被添加到相应列表的末尾。另外,rightPush
方法会返回元素添加到列表中的索引,而我让这里的方法返回了这一索引值。
不妨为这个方法写个 JUnit 测试用例:
@Testpublic void shouldAddWordWithItsMeaningToDictionary() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setUsePool(true);
factory.setPort(6379);
factory.setHostName("localhost");
factory.afterPropertiesSet();
RedisTemplate<String, String> template = new RedisTemplate<String, String>(factory);
DictionaryDao dao = new DictionaryDao(template);
Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");
assertThat(index, is(notNullValue()));
assertThat(index, is(equalTo(1L)));
}
在以上测试用例里面,我们首先创建了 JedisConnectionFactory
,因为 RedisTemplate
需要一个用来连接到 Redis 的连接工厂类。还有另一个用于连接 Redis 的工厂类叫做 JRedisConnectionFactory
。
当此测试进行首次运行时,它应该会通过,并且会将新单词将存储在Redis 中。然而当再次运行该测试时,它将不会通过,因为这会将单词 "lollop" 的既有意义再次加到这一单词的列表里面,然后返回索引值 2。因此,我们应该在每次运行测试之后将 Redis 数据存储清理一遍。而要清理 Redis 数据存储,我们必须使用 flushAll()
方法或 flushDb 服务器命令。其中 flushAll()
和 flushDb 的不同之处在于前者将删除所有数据库里面的键值对,而 flushDb 只会删除当前数据库中的所有键值对。因此,我们可以把测试用例改成这个样子:
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.keyvalue.redis.core.RedisTemplate;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
public class DictionaryDaoIntegrationTest {
private RedisTemplate<String, String> template;
private DictionaryDao dao;
@Beforepublic
void setUp() throws Exception {
this.template = getRedisTemplate();
this.template.afterPropertiesSet();
dao = new DictionaryDao(template);
}
protected JedisConnectionFactory getConnectionFactory() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setUsePool(true);
factory.setPort(6379);
factory.setHostName("localhost");
factory.afterPropertiesSet();
return factory;
}
protected RedisTemplate<String, String> getRedisTemplate() {
return new RedisTemplate(getConnectionFactory());
}
@Afterpublic
void tearDown() throws Exception {
template.getConnectionFactory().getConnection().flushAll();
template.getConnectionFactory().getConnection().close();
}
@Testpublic
void shouldAddWordWithItsMeaningToDictionary() {
Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");
assertThat(index, is(notNullValue()));
assertThat(index, is(equalTo(1L)));
}
@Testpublic
void shouldAddMeaningToAWordIfItExists() {
Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");
assertThat(index, is(notNullValue()));
assertThat(index, is(equalTo(1L)));
index = dao.addWordWithItsMeaningToDictionary("lollop","To hang loosely; droop; dangle.");
assertThat(index, is(equalTo(2L)));
}
}
现在我们能将单词存储在 Redis 数据存储里面了。然后我们也理应编写一个读取特定单词的所有含义的功能。这可以使用列表类型的 range
操作来轻松处理。range()
方法有三个参数 —— 键的名称,范围的起始和结束点。为了获得一个单词的所有含义,我们可以用 0 作为起始点,并以 -1 作为结束点。
public List getAllTheMeaningsForAWord(String word) {
List<String> meanings = template.opsForList().range(word, 0, -1);
return meanings;
}
接着,我想在本文里面为项目添加最后一个功能:删除已有单词。这可以使用 RedisTemplate
类的 delete
操作完成。删除的操作会需要我们提供想要删除的一组键作为参数。
public void removeWords(String... words) {template.delete(Arrays.asList(words));}
让我们再为上面添加的读取和删除操作编写 JUnit 测试用例。
@Testpublic
void shouldGetAllTheMeaningForAWord() {
setupOneWord();
List allMeanings = dao.getAllTheMeaningsForAWord("lollop");
assertThat(allMeanings.size(), is(equalTo(2)));
assertThat(allMeanings, hasItems("To move forward with a bounding, drooping motion.","To hang loosely; droop; dangle."));
}
@Testpublic
void shouldDeleteAWordFromDictionary() throws Exception {
setupOneWord();
dao.removeWords("lollop");
List allMeanings = dao.getAllTheMeaningsForAWord("lollop");
assertThat(allMeanings.size(), is(equalTo(0)));
}
private void setupOneWord() {
dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");
dao.addWordWithItsMeaningToDictionary("lollop","To hang loosely; droop; dangle.");
}
第一部分结论
本文只是我们通过 Spring Data 使用 Redis 的功能的一个开始。我们在本文只是见到了由 SDKV 项目提供的一些 List 操作。在接下来的部分中,我将使用 MULTI-EXEC 块来讨论其他数据类型还有对发布 - 订阅模式的支持。
本系列的源代码可以在我的 github 存储库中拿到。