内容


Java 开发 2.0

现实世界中的 Redis

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

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: Java 开发 2.0

敬请期待该系列的后续内容。

此内容是该系列的一部分:Java 开发 2.0

敬请期待该系列的后续内容。

之前,我已在本系列中讨论过 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 是一个不错的选择。其中的一个例子为股票查询服务,需要另外访问数据库获取相对静态数据,如股票名称或价格信息。

但是 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 的快速原子操作完成。


相关主题


评论

添加或订阅评论,请先登录注册

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