使用 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 交互的通用模板类,它与 JDBCTemplateHibernateTemplate 非常类似。这减轻了开发人员学习初级 API 的难度。

准备工作

在构建应用之前,要先确保你有这些东西:

  1. Redis(Windows 用户也可以从 dmajkic git 仓库 下载Redis )
  2. Java V6 JDK
  3. Apache Maven V2.0.9 或更高版本
  4. Spring Tool Suite(STS)
  5. 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 提供了像 ValueOperationsListOperationsSetOperationsHashOperationsZSetOperations 这些类型的键的操作。在以上代码中,我使用了 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 存储库中拿到。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
使用 Spring Data 以 Redis 作为数据存储来构建应用 - 第 1 部分
在本文里面,我将介绍 Java 开发者使用 Spring Data 访问 Redis 并执行操作的编程方式。
<<上一篇
下一篇>>