Java 开发 2.0: 现实世界中的 Redis

Redis 如何在包含大量读取操作的应用程序中战胜 memcached

Redis 与 memcached 有许多相似之处,但是它拥有更加丰富的功能集。在本期的 Java 开发 2.0 中,Andrew 尝试将 Redis 添加(通过基于 Java™ 的变体 Jedis 方法)到他自己的基于定位的移动应用程序。了解如何将 Redis 作为一个简单数据存储来使用,然后尝试再利用它进行超高速轻量级缓存。

Andrew Glover, 作家和开发人员, Beacon50

Andrew GloverAndrew Glover 是具有行为驱动开发、持续集成和敏捷软件开发激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发(Behavior-Driven Development,BDD)框架的创建者和三本书的合著者:持续集成Groovy 在行动Java 测试模式。您可以通过他的博客与他保持一致并在 Twitter(http://twitter.com/aglover)上关注他。



2012 年 1 月 30 日

关于本系列

自 Java 技术诞生以来,Java 开发格局已经发生了根本性的变化。这得益于成熟的开源框架和可靠的租赁部署基础架构,现在可以快速且低廉地组装、测试、运行、和维护 Java 应用程序。在 系列文章 中,Andrew Glover 探讨了使得这一新 Java 开发范例成为可能的技术与工具。

之前,我已在本系列中讨论过 NoSQL 的概念,也介绍了一些与 Java 平台兼容的 NoSQL 数据存储,其中包括 Google 的 Bigtable 和 Amazon 的 SimpleDB。此外,我还讨论过较为常见的基于服务器的数据存储,比如 MongoDB 和 CouchDB。每个数据存储都有其优势和劣势,特别是当应用于特定领域时。

本期的 Java 开发 2.0 关注的是 Redis,一种轻量级键值对数据存储。多数 NoSQL 实现本质上都是键值对,但是 Redis 支持非常丰富的值集,其中包括字符串、列表、集以及散列。因此,Redis 通常被称为数据结构服务器。Redis 也以异常快速而闻名,这使得它成为某一特定类型使用案例的最优选择。

当我们想要了解一种新事物时,将其同熟知的事物进行比较可能会有所帮助,因此,我们将通过对比其与 memcached 的相似性以开启 Redis 探索之旅。接着我们将介绍 Redis 的主要功能,这些功能可以使其在某些应用场景可以胜过 memcached。最后我将向您展示如何将 Redis 作为一个传统数据存储用于模型对象。

Redis 和 memcached

Memcached 是一个众所周知的内存对象缓存系统,通过将目标键和值导入内存缓存运行。因此,Memcached 能回避读取磁盘时发生的 I/O 成本问题。在 Web 应用程序和数据库之间粘贴 memcached 时会产生更好的读取性能。因此,对于那些需要快速数据查询的应用程序,Memcached 是一个不错的选择。其中的一个例子为股票查询服务,需要另外访问数据库获取相对静态数据,如股票名称或价格信息。

MemcacheDB

将 Redis 与 memcached 相比较并不公平,它与 MemcacheDB 相比要好的多,MemcacheDB 是一个分布式键值对存储系统,专为数据持久化而设计。MemcacheDB 与 Redis 较为相似,其新增优势可以使其轻松地与 memcached 实现的客户端进行通信。

但是 memcached 也有其局限性,其中一个事实就是它所有的值均是简单的字符串。Redis 作为 memcached 的替代者,支持更加丰富的功能集。一些基准 (benchmarks) 也表明 Redis 的速度要比 memcached 快很多。Redis 提供的丰富数据类型使其可以在内存中存储更为复杂的数据,这是使用 memcached 无法实现的。同 memcached 不一样,Redis 可以持久化其数据。

Redis 解决了一个重大的缓存问题,而其丰富的功能集又为其找到了其他用途。由于 Redis 能够在磁盘上存储数据以及跨节点复制数据,因而可以作为数据仓库用于传统数据模式(也就是说,您可以使用 Redis,就像使用 RDBMS 一样)。Redis 还经常被用作队列系统。在本用例中,Redis 是备份和工作队列持久化存储(利用 Redis 的列表类型)的基础。GitHub 是以此种方法使用 Redis 的大规模基础架构示例


准备好 Redis,立即开始!

要开始使用 Redis,您需要访问它,可以通过本地安装或者托管供应商来实现访问。如果您使用的 MAC,安装过程可能就不那么简单。如果您使用的是 Windows®,您需要先安装 Cygwin。如果您正在寻找一个托管供应商,Redis4You 拥有一个免费计划。不管您以何种方式访问,您都能够根据本文下列示例进行操作,但是我需要指出的是,使用一个托管供应商进行缓存可能并不是很好的缓存解决方案,因为网络延迟可能会抵消任何性能优势。

您需要通过命令与 Redis 进行交互,这就是说,这里没有 SQL 类查询语言。使用 Redis 工作非常类似于使用传统 map 数据结构,即所有的一切都拥有一个键和一个值,每个值都有多种与之关联的数据类型。每个数据类型都有其自己的命令集。例如,如果您计划使用简单数据类型,比如某种缓存模式,您可以使用命令 setget

您可以通过命令行 shell 与一个 Reids 实例进行交互。还有多个客户端实现,可以以编程方式与 Redis 进行交互。清单 1 展示了一个使用基础命令的简单命令行 shell 交互:

清单 1. 使用基础的 Redis 命令
redis 127.0.0.1:6379> set page registration
OK
redis 127.0.0.1:6379> keys *
1) "foo"
2) "page"
redis 127.0.0.1:6379> get page
"registration"

在这里,我通过 set 命令将键 "page" 与值 "registration" 相关联。接着,我发出 keys 命令(后缀 * 表示我想看到所有可用的实例键。keys 命令显示有一个 page 值和一个 foo,我可以通过 get 命令检索到与一个键关联的值。请记住,使用 get 检索到的值只能是一个字符串。如果一个键的值是一个列表,那么您必须使用一个特定列表的命令来检索列表元素。(注意,有可以查询值类型的命令)。


Java 与 Jedis 集成

对于那些想要将 Redis 集成到 Java 应用程序的编程人员,Redis 团队建议使用一个名为 Jedis 的项目,Jedis 是一个轻量级库,可以将本地 Redis 命令映射到 Java 方法。例如 Jedis 可以获取并设置简单值,如清单 2 所示:

清单 2. Java 代码中的基础 Redis 命令
JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
Jedis jedis = pool.getResource();

jedis.set("foo", "bar");
String foobar = jedis.get("foo");
assert foobar.equals("bar");

pool.returnResource(jedis);
pool.destroy();

在清单 2 中,我配置了一个连接池并捕获连接,(与您在典型 JDBC 场景中的操作非常相似)然后我在清单的底部设置了返回操作。在连接池逻辑之间,我设置了值 "bar" 和键 "foo",这是我通过 get 命令检索到的。

与 memcached 类似,Redis 允许您将过期(expiration)时间关联到一个值。因此我设置了这样一个值(比如,股票临时交易价格),最终将从 Redis 缓存中清除掉。如果我想在 Jedis 中设置一个过期时间,需要在发出 set 调用之后将其和一个过期时间关联。如清单 3 所示:

清单 3. Redis 值可以设置为终止
jedis.set("gone", "daddy, gone");
jedis.expire("gone", 10);
String there = jedis.get("gone");
assert there.equals("daddy, gone");

Thread.sleep(4500);

String notThere = jedis.get("gone");
assert notThere == null;

在清单 3 中,我使用了一个 expire 调用将 "gone" 的值设置为在 10 秒钟内终止。调用 Thread.sleep 之后,"gone" 的 get 调用会返回 null

Redis 中的数据类型

使用 Redis 数据类型,比如列表和散列需要专用命令用法。例如,我可以通过为键附加值来创建列表。在清单 4 的代码中,我发出一个 rpush 命令,将一个值附加至列表右侧或者尾端。(相应的 lpush 命令会将一个值添加到列表的前端。)

清单 4. Redis 列表
jedis.rpush("people", "Mary");
assert jedis.lindex("people", 0).equals("Mary");

jedis.rpush("people", "Mark");

assert jedis.llen("people") == 2;
assert jedis.lindex("people", 1).equals("Mark");

Redis 为数据类型提供许多命令;此外,每个数据类型都有其自己的命令集。这里就不再逐个讨论了,我将会在一个实际应用程序开发场景中介绍其中的一些。


使用 Redis 作为一个缓存解决方案

我之前提到过,Redis 可轻易地用作一个缓存解决方案,碰巧我现在正好需要这样一个!在该应用程序示例中,我将 Redis 集成到我基于定位的移动 Web 服务中,称之为 Magnus。

如果您没有关注本系列,那么我会先使用 Play 框架实现 Magnus,从那时起我就已经在各种实现中开发和重构它了。Magnus 是一个简单服务,可以通过 HTTP PUT 请求使用 JSON 文档。这些文档描述了特定帐号的位置,表示持有移动设备的人。

现在,我想要将缓存集成到 Magnus,也就是说我想要通过将不常更改的数据存储在内存中以减少 I/O 流量。

Magnus 缓存!

在清单 5 中的第一步中,可以通过 get 调用了解新引入的帐户名称(一个键)是否为 REdis 中的一个键。get 调用可以将帐户 ID 作为一个值返回,或者将返回 null。如果返回一个值,我将用其作为我的 acctId 变量。如果返回的是 null(表明该帐户名称不是 Redis 中一个键),那么我将在 MongoDB 查找该帐户值,并通过 set 命令将其添加到 Redis。

这里的优势是速度:接下来,被请求的帐户将提交一个位置,这样我就能够从 Redis 中获取其 ID(作为内存缓存),而不是转到 MongoDB 并带来额外读取 I/O 成本。

清单 5. 使用 Redis 作为内存缓存
"/location/:account" {
  put {
    def jacksonMapper = new ObjectMapper()
    def json = jacksonMapper.readValue(request.contentText, Map.class)
    def formatter = new SimpleDateFormat("dd-MM-yyyy HH:mm")
    def dt = formatter.parse(json['timestamp'])
    def res = [:]
    
    try{

      def jedis = pool.getResource()	
      def acctId = jedis.get(request.parameters['account'])

      if(!acctId){
        def acct = Account.findByName(request.parameters['account'])
        jedis.set(request.parameters['account'], acct.id.toString())
        acctId = acct.id
      }

      pool.returnResource(jedis)
      new Location(acctId.toString(), dt, json['latitude'].doubleValue(), 
      json['longitude'].doubleValue() ).save()
      res['status'] = 'success'
    }catch(exp){
      res['status'] = "error ${exp.message}"
    }
   response.json = jacksonMapper.writeValueAsString(res)
  }
}

注意,清单 5 中的 aMagnus 实现(使用 Groovy 编写)仍然使用一个 NoSQL 实现作为数据模型存储;它仅仅使用 Redis 作为一个缓存实现用于查询数据。因为我的主要帐户数据位于 MongoDB 中(事实上,它驻留在 MongoHQ.com 中),而我的 Redis 数据存储在本地运行。在随后查找帐户 ID 时,Magnus 速度将显著提升。

可是等等!我为什么同时需要 MongoDB Redis?难道我就不能单独使用一个吗?

ORM 的 Node.js

很多项目均提供 ORM 类映射用于 Redis,其中包括一个极富影响力的基于 Ruby 的备用方案,称为 Ohm。我检查了该项目基于 Java 的派生产品(称为 JOhm),但是最终决定使用一个为 Node 编写的派生产品。Ohm 及其派生项目的妙处在于他们允许您将一个对象模型映射到一个基于 Redis 的数据结构。因此,您的模型对象是持久性的,同时在大多数情况下其读取速度也非常之快。

有了 Nohm,我便能够使用 JavaScript 快速重写我的 Magnus 应用程序并能立即持久化 Location 对象。在清单 6 中,我已定义了一个 Location 模型,该模型包括 3 个属性。(注意,我通过将 timestamp 设置为一个字符串而不是一个真实的时间戳,从而简化我的示例。)

清单 6. Node.js 中的 Redis ORM
var Location = nohm.model('Location', {
	properties: {
	    latitude: {
	      type: 'float',
	      unique: false,
	      validations: [
	        ['notEmpty']
	      ]
	    },
		longitude: {
	      type: 'float',
	      unique: false,
	      validations: [
	        ['notEmpty']
	      ]
	    },
		timestamp: {
	      type: 'string',
	      unique: false,
	      validations: [
	        ['notEmpty']
	      ]
        }
     }
});

Node 的 Express 框架使 Nohm Location 对象的使用变得十分简单。在我的应用程序 PUT 实现中,我可以捕获正在进入的 JSON 值,并通过 Nohm 的 p 调用将其导入到一个 Location 实例。然后我再检查该示例是否有效,如果有效,我会对其进行持久化。

清单 7. 在 Node 的 Express.js 中使用 Nohm
app.put('/', function(req, res) {
  res.contentType('json');
	
  var location = new Location;
  location.p("timestamp", req.body.timestamp);
  location.p("latitude", req.body.latitude);
  location.p("longitude", req.body.longitude);
  
  if(location.valid()){	
  	location.save(function (err) {
	  	if (!err) {
		    res.send(JSON.stringify({ status: "success" }));
		  } else {		
		   res.send(JSON.stringify({ status: location.errors }));
		  }
	  });
  }else{
   res.send(JSON.stringify({ status: location.errors }));
  }
});

正如清单 7 所示,可以轻易地将 Redis 构建成一个极其快速的内存数据存储。在一些案例中,它甚至是一个比 memcached 更好的缓存!


结束语

Redis 对于许多数据存储场景非常有用,因为它可以将数据持久化到磁盘(还因为它支持一个丰富的数据集),有时候,它是 memcached 的有力竞争对手。有些情况下,对于您的领域也是很有意义的,您可以使用 Redis 作为数据模型和队列的一个备份存储。Redis 客户端实现几乎可被移植到任何编程语言中。

Redis 不是 RDMBS 的完全替代品,也不是一个重量级存储,但是和 MongoDB 一样拥有丰富的功能。然而,在很多情况下,它可与这些技术共存。正如本文所述,Redis 是一个良好的应用程序单机数据解决方案,可以运行大量数据查询,或者其中的实时统计可通过 Redis 的快速原子操作完成。

参考资料

学习

获得产品和技术

  • 下载 Redis 和 Jedis:Redis 是一个开源键值对存储和数据结构服务器;Jedis 是当前基于 Java 开发的推荐客户端。
  • Get Nohm:Redis 对象关系型映射的一个 Node.js 实现,Ohm。

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Cloud computing
ArticleID=790179
ArticleTitle=Java 开发 2.0: 现实世界中的 Redis
publish-date=01302012