在 介绍 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 服务器的时候遇到了问题,请参阅我 以前的文章 。
本文将使用 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 存储库中。
我们需要创建一个 Spring 模板项目,以便我们可以以它为基础构建我们的简单应用。而要创建一个模板项目,我们要打开 STS 并打开 File - New - Spring Template Project - Simple Spring Spring Template Project,然后在弹出的提示框里点击 Yes,接着输入项目名称和默认包名称并确认。这将在 STS 工作区中创建一个由我们命名的的简单模板项目。我接下来使用的项目名称为 “dictionary”,默认包名是 “com.redis.dictionary”。
我们在上面创建的项目没把 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 存储库 中拿到。