在本系列之前,我已经讨论过NoSQL的概念,并介绍了各种与Java平台兼容的NoSQL数据存储,包括Google的Bigtable和Amazon的SimpleDB。 我还讨论了更常规的基于服务器的数据存储,例如MongoDB和CouchDB。 每个数据存储都有优点和缺点,尤其是应用于特定域方案时。
本月的Java开发2.0焦点是Redis,这是一种轻量级的键值数据存储。 大多数NoSQL实现本质上都是键值,但是Redis支持异常丰富的值集,包括字符串,列表,集和哈希。 因此,Redis通常被标记为数据结构服务器 。 Redis还以极快的速度而闻名,这使其成为特定类别用例的最佳选择。
在尝试了解新事物时,将其与您已经熟悉的事物进行比较可能会有所帮助,因此我们将通过考虑Redis与memcached的相似性来开始对Redis的探索。 然后,我将演示Redis的关键功能,这些功能可以使它在某些应用程序场景中优于内存缓存。 最后,我将向您展示如何将Redis用作模型对象的传统数据存储。
Redis和Memcached
Memcached是一种众所周知的内存中对象缓存系统,其工作原理是将目标键和值放入内存缓存中。 因此,Memcached避免了读取访问磁盘时发生的I / O开销。 将Memcached粘贴在Web应用程序和数据库之间可以提高读取性能。 因此,对于需要快速查找数据的应用程序,Memcached是一个不错的选择。 一个例子是股票查询服务,否则它将在数据库中查找相当静态的数据,例如股票代号或价格信息。
但是memcached有一些限制,包括其所有值都是简单字符串的事实。 Redis作为Memcached的替代品,支持更丰富的功能集。 一些基准测试还表明,Redis比memcached快得多。 与Memcached相比,Redis丰富的数据类型使在内存中存储更复杂的数据成为可能。 与memcached不同,Redis可以保留其数据。
Redis是一个很棒的缓存解决方案,但是其丰富的功能集导致了其他用途。 由于Redis能够在磁盘上存储数据并在节点之间复制数据,因此可以将其用作传统数据模型的数据存储库(也就是说,可以像使用RDBMS一样使用Redis)。 Redis也经常被用作排队系统。 在此用例中,Redis是支持工作队列的持久存储的基础,该队列利用Redis的列表类型。 GitHub是以这种方式使用Redis的大规模基础架构的一个示例。
获取Redis并开始!
为了开始使用Redis,您必须具有访问权限,可以通过本地安装或托管提供程序进行访问。 如果您使用的是Mac,安装过程再简单不过了。 如果您使用Windows®,则需要安装Cygwin 。 无论您如何访问Redis,您都可以按照本文后面的示例进行操作。 不过,我应该指出,使用托管Redis提供程序进行缓存可能不是一个很好的缓存解决方案,因为网络延迟可能会抵消任何性能提升。
您通过命令与Redis进行交互,这意味着没有类似SQL的查询语言。 使用Redis非常类似于使用传统的map
数据结构-每个东西都有一个键和一个值,每个值都有一组与之关联的丰富数据类型。 每种数据类型也都有其自己的命令集。 例如,如果您计划使用简单的数据类型(例如某种缓存方案),则可以使用set
和get
命令。
您可以通过命令行外壳与Redis实例进行交互。 还有多种客户端实现可通过编程方式与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
检索的值只能是一个字符串。 例如,如果键的值是列表,则必须使用特定于列表的命令来检索列表的元素。 (请注意,有一些命令可以查询值的类型。)
与Jedis的Java集成
对于希望将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场景中一样),然后在清单的底部返回该连接。 在连接池逻辑之间,我使用键"foo"
设置值"bar"
,该键通过get
命令检索。
与memcached类似,Redis允许您将到期时间与值相关联。 因此,我可以设置一个值(例如股票的临时交易价格),该值最终将从Redis缓存中清除。 如果要在Jedis中设置到期时间,请在发出set
调用后通过将其与到期时间相关联来进行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
后, get
null
将返回null
。
Redis中的数据类型
使用Redis数据类型(例如列表和哈希)需要专门的命令用法。 例如,我可以通过将值附加到键来创建列表。 在清单4中的代码,我发出rpush
命令,它附加一个值到一个列表的右侧或尾部 。 (相应的lpush
命令将值添加到列表的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流量。
马格努斯高速缓存!
清单5中的第一步是通过get
调用来确定Redis中是否有传入的帐户名(这是键)。 调用get
将返回帐户ID作为值或返回null
。 如果返回值, acctId
其用作我的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数据存储在本地运行,因此Magnus在查找后续帐户ID时将大大提高速度。
可是等等! 为什么我同时需要MongoDB 和 Redis? 我不能只使用一个吗?
适用于ORM的Node.js
许多项目为Redis提供了类似ORM的映射,包括一个很有影响力的基于Ruby的替代方案,称为Ohm。 我签出了该项目的基于Java的派生类(称为JOhm),但最终决定使用为Node编写的变体。 Ohm及其衍生项目的优点在于,它们允许您将对象模型映射到基于Redis的数据结构中。 因此,您的模型对象在读取情况下既持久又(在大多数情况下)非常快。
使用Nohm,我可以用JavaScript快速重写我的Magnus应用程序,并可以快速保存Location
对象。 在清单6中,我定义了一个Location
模型,其中包含三个属性。 (请注意,我通过使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可用于各种数据存储方案,并且由于它可以将数据持久保存到磁盘(并且因为它支持丰富的数据集),因此有时是内存缓存的有力竞争者。 在对您的域有意义的情况下,可以将Redis用作数据模型和队列的后备存储。 Redis客户端实现已被移植到几乎每种编程语言中。
Redis并不是RDMBS的完全替代品,也不是重量级的存储,它具有像MongoDB这样的查询功能。 但是,在许多情况下,它可以与这些技术并存。 正如我在本文中所展示的,对于需要大量数据查找或可以通过Redis快速的原子操作完成实时统计的应用程序,Redis可以是一个很好的独立数据存储解决方案。
翻译自: https://www.ibm.com/developerworks/java/library/j-javadev2-22/index.html