级别: 初级 James G. Goodwill, 应用程序架构师, Evite.com
2009 年 11 月 10 日 James Goodwill 使用一个样例 Grails 应用程序和一个基于 Java™ 的 memcached 客户端完成了他的分为两个部分的 memcached 和 Grails 集成简介。了解如何将 Spymemcached 集成到您用 Grails 构建的联系人管理应用程序,然后尝试使用 memcached 缓存单独的请求结果。您还将使用 第 1 部分 介绍的 memcached 客户端命令来测试您的新缓存的效果。
Grails 是一个 Web 应用程序框架,它利用 Groovy 的动态语法和 Java 平台的后端力量。memcached 是一个通用分布式内存缓存系统,用于一些流量最繁重的 Web 站点中。结合使用上述二者就有可能快速构建响应性和伸缩性都极好的 Web 应用程序。
在这个 memcached 和 Grails 简介系列的第二部分中,我将带领您完成将 memcached 集成到一个现有 Grails 应用程序的过程。首先,我将向您介绍这个 Grails 应用程序,并帮助您将它建立在您的开发环境中。然后,我将向您介绍 memcached 客户端 API,并指导您创建一个 Grails 服务来包装它。
 |
了解 Grails
基本 Grails 主题(如安装、语法等)不属于本系列探讨的范围。如果您需要了解 Grails 的基础知识,请参阅 developerWorks 系列 精通 Grails 的第 1 篇文章。
|
|
如果您拥有在您的 Grails 使用 memcached 的所有组件,您也许想知道如何最好地利用 memcached。本文将探讨将缓存注入一个现有应用程序的最有效的区域,还将展示一些用于从缓存存取值的技术。最后,您将逐步执行一个实践练习,在样例应用程序的一个 Grails 控制器中实现缓存并测试结果。
样例 Grails 应用程序
为了查看 memcached 是如何发挥作用的,您需要一个样例 Grails 应用程序。这个应用程序不必太复杂,因此您可以构建一个简单的联系人管理应用程序,允许用户管理一个联系人集合和他们的相关信息。这个微型 CRUD 应用程序的惟一功能是允许用户使用 memcached 来存储和获取 Contact 对象。首先,在您的开发环境中执行以下命令:
输入应用程序名称,在本例中,输入 contactmanager。Grails 创建这个应用程序后,cd 到 contactmanager 目录并启动应用程序:
cd contactmanager
grails run-app
|
打开您的浏览器并导航到 http://localhost:8080/contactmanager。如果一切正常,您将看到 Grails 欢迎页面。
创建域
下面,您需要定义 Contact 域类。为简便起见,我们只向该域添加 3 个属性:firstName、lastName 和 email。添加这些属性后,您的 Contact 域应该如清单 1 所示:
清单 1. Contact 域
class Contact {
def String firstName
def String lastName
def String email
static constraints = {
firstName(maxLength: 50, blank: false)
lastName(maxLength: 50, blank: false)
email(blank: false, nullable: false, email:true)
}
}
|
下面,在您的 contactmanager/grails-app/domain 目录中创建这个 Groovy 类。
创建 Controller
下一步,创建应用程序 Controller,它用于提供 Contact。首先执行以下命令:
grails create-controller
Contact
|
现在打开新创建的 ContactController 并将其更改为如清单 2 所示:
清单 2. 向 Controller 添加支架(scaffolding)
class ContactController {
def scaffold = Contact
}
|
这将添加默认的 Grails 支架。现在,需要添加一些内容。打开 BootStrap.groovy(用于在启动时初始化 Grails 应用程序)并如清单 3 所示修改它。数据库初始化、动态配置和向 Groovy 类注入元更改(meta-change)都是 BootStrap.groovy 执行的常见任务。我还经常用它来将我的开发数据库配置为默认状态。
清单 3. 修改 Bootstrap.groovy
class BootStrap {
def init = {servletContext ->
(1..10000).each {i ->
def Contact contact =
new Contact(firstName: "Bob",
lastName: "Johnson${i}",
email: "bob.johnson${i}@email.com").save()
}
}
def destroy = {
}
}
|
清单 3 的目的是在这个 Grails 应用程序的默认 HSQLDB 数据库中创建 10,000 个联系人。现在重新启动应用程序并浏览一下,这样,您将对您正在进行的工作有所了解。
向 ContactController 添加逻辑
创建这个 Grails 样例应用程序的最后一步是向 ContactController 添加简单的逻辑以输出查询运行时。稍后,您将使用这个信息来了解改进应用程序性能的缓存量。在向这个 Controller 添加逻辑之前,您首先需要告知 Grails 为 Contact 生成所有支架代码。为此,执行 grails generate-all 命令:
现在打开文件 contactmanager/grails-app/controllers/ContactController.groovy。您将看到支持您的 Contact 域所需要的所有闭包(closure)。找出 list 闭包并向它添加定时语句,如清单 4 所示。
清单 4. 带有定时语句的 list 闭包
def list = {
params.max = Math.min( params.max ? params.max.toInteger() : 10, 100)
def startTime = new Date().getTime()
def contactInstanceList = Contact.list(params)
def contactInstanceTotal = Contact.count()
def endTime = new Date().getTime()
println "Transaction time was ${(endTime - startTime) / 1000} seconds."
[contactInstanceList: contactInstanceList, contactInstanceTotal:
contactInstanceTotal]
}
|
清单 4 中的代码添加了一个 startTime,它设置为当前系统时间,单位为毫秒。然后,该代码执行两个查询,用于获取一列 Contact 和 Contact 的总数。运行这两个查询后,您将再次获得一个当前时间(以毫秒为单位),将其设置为 endTime 变量的值。最后,您将两个时间的差除以 1,000(单位变为秒),以了解这两个查询花费的时间。
使用 grails run-app 重新启动应用程序,当您逐页浏览联系人列表结果时查看控制台。
将 memcached 注入 Grails 应用程序
要将 memcached 客户端添加到您的 Grails 应用程序,首先需要下载适当的 jar 文件并将其复制到 contactmanager/lib 目录。例如,我使用的是 Spymemcached,memcached 的一个 Java 客户端。接下来 下载 JAR 文件,本文写作时的最新版本是 2.3.1。
将这个 jar 文件放到 contactmanager/lib 目录后,下一步是创建一个用于公开这个 API 的 Groovy 类。对于这个实现,我选择使用一个 Grails 服务,原因有二:其一,所有 Grails 服务都作为 Spring bean 管理,因此可以被自动注入到您的Controller 中;其二,作为一个 Spring bean,这个 Grails 服务允许您访问 org.springframework.beans.factory.InitializingBean 接口,该接口允许您在其他所有属性都设置后初始化该服务。
下面,让我们创建这个服务。打开任意 IDE 或编辑器,在 contactmanager/grails-app/services 目录中创建这个 Groovy 类,如清单 5 所示。
清单 5. MemcachedService
import net.spy.memcached.AddrUtil
import net.spy.memcached.MemcachedClient
import org.springframework.beans.factory.InitializingBean
class MemcachedService implements InitializingBean {
static final Object NULL = "NULL"
def MemcachedClient memcachedClient
def void afterPropertiesSet() {
memcachedClient = new MemcachedClient(AddrUtil.getAddresses("localhost:11211"))
}
def get(String key) {
return memcachedClient.get(key)
}
def set(String key, Object value) {
memcachedClient.set(key, 600, value)
}
def delete(String key) {
memcachedClient.delete(key)
}
def clear() {
memcachedClient.flush()
}
def update(key, function) {
def value = function()
if (value == null) value = NULL
set(key, value)
return value
}
def get(key, function) {
def value = get(key)
if (value == null) {
value = update(key, function)
}
return (value == NULL) ? null : value;
}
}
|
清单 5 中的大部分方法可能都是您意料之中的 —
get()、set()、delete() 和 clear()
— 但也有一些不太常见的元素。让我们看看它们:
首先,查看下面这行:
static final Object NULL = "NULL"
|
只要 memcached 中正在存储一个 null,就需要使用这个值。使用这一行的原因是 null 不能序列化,而 memcached 中放置的所有对象都必须是可序列化的。
下面检查 afterPropertiesSet() 方法。如前所述,这种方法在所有属性都设置好后由 Spring 调用。注意,在这个方法中,我还添加了代码以创建 MemcachedClient 的一个新实例并连接到 memcached 服务器。
最后两个值得一提的方法是 update() 和 get(),它们都接受属性 key 和一个 function。这些方法展示了一种使用一个名为 memoization 的缓存的有趣方法。
我所做的是传递要查询的项目的 key 和应该执行的 function,如果 get() 没有发现 key 的话。这种技术用于避免重复计算先前处理过的输入的结果。相反,在代码中使用一个简单的 get() 调用既能获取想要的对象,又能在缓存中没有该对象时创建对象。另外,它还能存储结果。稍后您将了解到 memoization 如何简化缓存交互。
MemcachedService 和 ContactControler
要将新创建的 MemcachedService 添加到您的 ContactController 中,在您的 ContactController 中添加下面的行:
class ContactController {
def memcachedService
...
}
|
这个简单的代码行将把 MemcachedService 的一个实例自动注入到 Controller 中。现在这个服务在 Controller 中已经可用,但 Controller 应该缓存什么呢?
使用 memcached
当您确定要缓存哪些数据时,最好牢记两个简单的准则。这些准则并不是一成不变的,也并非适合所有情况,但它们为确定要缓存的数据奠定了一个良好的基础。
- 不要缓存频繁改变的数据。如果要缓存的数据频繁改变,您需要不断修改存储在缓存内的值,这将限制缓存的价值。
- 如果您拥有直接识别一个值的 ID,则您不必缓存那个值。数据库能够使用某个值的 ID 非常快速地查询该值。
对于 Contact Manager 应用程序,这些准则清楚地表明:您需要缓存分页浏览联系人时返回的数据。还记得吗,这个数据在 ContactController 的 list 闭包中查询过。
list 闭包返回两个值:
contactInstanceList 是从数据库获取的一列 Contact。
contactInstanceTotal 表示数据库中的 Contact 的总数。
这两个项都应该缓存,让我们从 contactInstanceTotal 开始。
缓存联系人实例总数
要缓存联系人实例数据,首先需要向 ContactController 添加一个方法,这将缓存所有 Contact:
清单 6. getContactInstanceTotal()
def getUsername() {
// dumb method to return a username
// you are not implementing any security in this example
return "my.username"
}
def getContactInstanceTotal(username) {
def contactInstanceTotal = memcachedService.get("${username}:contactInstanceTotal") {
def contactInstanceTotal = Contact.count()
return contactInstanceTotal
}
return contactInstanceTotal
}
|
getUsername() 是一个 “伪方法(dumb method)”。之所以需要这个方法,只是因为安全性超出了本文的范围,因此样例应用程序没有用户的概念。与缓存的真实交互正是从 getContactInstanceTotal() 中开始的。这个方法使用 “my.username:contactInstanceTotal” 键调用 memcachedService.get() 方法。如果能够在缓存中找到该键,那么值就能返回调用者。否则,传递到 get() 的闭包将被调用,返回的值将使用传入的键存储在缓存中。将以上代码添加到 ContactController,然后使用一个对 getContactInstanceTotal() 的调用:
def contactInstanceTotal = getContactInstanceTotal(getUsername())
|
代替以下代码:
def contactInstanceTotal = Contact.count()
|
现在重新启动应用程序,telnet 进入缓存,然后执行 flush_all 命令。这将把缓存设置为一个干净的状态:
telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
flush_all
OK
|
现在打开您的浏览器并导航到 http://localhost:8080/contactmanager/contact/list。
导航到这个链接后,telnet 进入 memcached 并在 getContactInstanceTotal() 使用的键上执行一个 get,如下所示:
get my.username:contactInstanceTotal
VALUE my.username:contactInstanceTotal 512 2
'
END
|
您将看到一个值被存储到缓存中。但是,您看到的值并不一定是您所预期的,这是因为该值已经被序列化,而不会是 String 形式。
生成键
要缓存 Contact Manager 应用程序的 Contact,需要为每个请求创建一个惟一的键。如果您只使用单一键(就像 contactInstanceTotal 的情况一样),那么您最终将使用每个新请求覆盖缓存数据。您还需要一种方法来确保能够使用每个匹配的请求重新生成该键。
最简单且最可靠的生成键的方法是使用传入到请求的参数的摘要(digest)以及预先前置到键上的 username。假定两次接收到相同的参数,那么得到的结果应是相同的(假定数据没有改变,下一节将介绍数据改变的情况)。这种技术还保证您能够通过生成一个参数摘要生成一个统一的键。
缓存数据
现在您已经准备好缓存数据了。如前所述,您将缓存对 list 闭包的每个请求的结果。list 闭包非常简单,用于分页浏览数据库中的一个 Domain 对象集合。当您对这个闭包发送一个请求时,一个参数集合将被传递到该闭包。这些参数表明 Grails 查询数据库的方式。当您分页浏览样例应用程序的 Contact 时,您可以通过观察 URL 所发生的变化来查看这些参数的一些示例。您还可以在以下 URL 中看到一些参数:
http://localhost:8080/contactmanager/contact/list?offset=10&max=10
|
这个请求包含两个参数:offset 和 max。这个请求的结果是一个包含 10 个 Contact 的列表。这个列表从第 10 个 Contact 开始,到第 19 个 Contact 结束。使用这些参数,您将总是从数据库得到相同的结果,除非数据发生改变。因此,您可以从这个请求中提取 params,生成一个参数摘要,然后在缓存中存储这个 “键/值” 对。清单 7 展示了我为实现上述目的所创建的方法:
清单 7. getCachedContactInstanceList()
def getCachedContactInstanceList(username) {
params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)
println "PARAMS == ${params.toString()}"
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(params.toString().getBytes('UTF-8'))
def key = username + new BASE64Encoder().encode(md.digest())
println "Using key ${key}."
def cachedContactInstanceList = memcachedService.get(key) {
def contactInstanceList = Contact.list(params)
def serializableList = new ArrayList()
contactInstanceList.each {
serializableList.add([id: it.id, firstName: it.firstName, lastName:
it.lastName, email: it.email])
}
return serializableList
}
return cachedContactInstanceList
}
|
这个 getCachedContactInstanceList() 方法相当简单。该方法接收传递到这个 Controller 的参数,将其 String 值转换为一个字节数组,然后将它传递到 MessageDigest。然后,通过预先附着到 key 上的 username,该方法使用 BASE64Encoder 来生成一个键 — 这样,只要使用同一组 params,您就能获得一个可以重新生成的键。
清单 7 还有一点值得一提,那就是对结果的迭代,这个迭代将每个 Contact 的内容转化为一个映射图并将每个映射图存储到一个 ArrayList 中。这是因为 Grails Domain 对象是不可序列化的,而映射图可以序列化。
将这个方法添加到您的样例 Grails 应用程序后,用一个对 getCachedContactInstanceList() 的调用:
def cachedContactInstanceList = getCachedContactInstanceList(getUsername())
|
代替以下代码:
def contactInstanceList = Contact.list(params)
|
重新启动 Grails 并多次分页浏览 Contact,重复几个请求。与此同时,监控控制台 stdout。您将注意到,当您重复请求时,响应时间减少了。
缓存失效
获取缓存数据的响应时间大大减少了,但当您更新应用程序的联系人时会发生什么情况呢?在添加一个新的 Contact 之前,浏览到最后一页,查看一下那里的 Contact 列表。现在,添加两个新的 Contact,然后再浏览到最后一页。您将注意到新的 Contact 没有列示出来。问题在于您已经缓存了本应该返回的请求的结果。
解决办法是使缓存失效。但如何操作呢,您甚至没有已经存储在缓存中的键的记录。首要任务是确保能够查询所有需要使其失效的键。一个简单的方法是缓存一个键列表并将其关联到用户。
将清单 8 中的代码添加到 getCachedContactInstanceList() 方法,正好在返回缓存的 Contact 之前。
清单 8. 更新后的 getCachedContactInstanceList()
def getCachedContactInstanceList(username) {
...
// before I return the contacts, I need to add this key to the user's keyList
def contactKeyList = memcachedService.get(username + ":contactKeyList")
if (!contactKeyList) {
contactKeyList = []
}
contactKeyList.add(key)
memcachedService.set(username + ":contactKeyList", contactKeyList)
return cachedContactInstanceList
}
|
下面,添加这段代码以使缓存失效。使缓存失效的方法必须执行以下 4 个步骤:
- 检索缓存的键
- 删除与检索到的键关联的 “键/值” 对
- 删除缓存的键的数组
- 删除表示
contactInstanceTotal 的 “键/值” 对
清单 9 展示使缓存失效的完整方法:
清单 9. 更新后的 invalidateContacts()
def invalidateContacts(username) {
// delete the cached contacts
def contactKeyList = memcachedService.get(username + ":contactKeyList")
contactKeyList.each {
memcachedService.delete(it)
}
// delete the list of keys
memcachedService.delete(username + ":contactKeyList")
// delete the contactInstanceTotal
memcachedService.delete(username + ":contactInstanceTotal")
}
|
这段代码本身相当直观,它完成 4 个必要步骤,删除与 username 关联的 Contact 的所有信息。对这个方法的调用必须被添加到所有导致 Contact 数据发生变化的 “方法/闭包” 中。这个应用程序中的闭包包含 delete 和 save 方法。清单 10 展示了对这些闭包的修改:
清单 10. 更新后的 delete 和 save 闭包
def delete = {
def contactInstance = Contact.get(params.id)
if (contactInstance) {
try {
contactInstance.delete(flush: true)
flash.message = "Contact ${params.id} deleted"
invalidateContacts(getUsername())
redirect(action: list)
}
catch (org.springframework.dao.DataIntegrityViolationException e) {
flash.message = "Contact ${params.id} could not be deleted"
redirect(action: show, id: params.id)
}
}
else {
flash.message = "Contact not found with id ${params.id}"
redirect(action: list)
}
}
def save = {
def contactInstance = new Contact(params)
if (!contactInstance.hasErrors() && contactInstance.save()) {
flash.message = "Contact ${contactInstance.id} created"
invalidateContacts(getUsername())
redirect(action: show, id: contactInstance.id)
}
else {
render(view: 'create', model: [contactInstance: contactInstance])
}
}
|
注意,在清单 10 中,在确认数据成功更改之前,不应该使缓存的 Contact 失效。在这些闭包的开始部分就使缓存失效将面临缓存过早失效的风险。
测试结果
进行更改后,重新启动您的 Contact Manager 应用程序,telnet 进入 memcached,您将在其中检查更新后的结果。您首先需要将缓存重置为一种完全空白状态,因此,执行 flush_all 命令:
下面,打开您的浏览器并开始分页浏览 Contacts 列表。与此同时,观察控制台,您将看到用于在缓存中存储 Contact 的键。复制一些键以备后用,然后添加、删除并编辑一些 Contact。首先,您将看到数据在修改时能够正确显示。最后,添加一个新联系人并停止浏览 Show Contact 页面。
返回您的 telnet 会话,在您先前复制的键 - contactKeyList 和 contactInstanceTotal - 上执行一个 get。您将注意到,所有这些值都已从缓存删除,缓存处于适当状态以迎接所有即将到来的请求。
结束语
在本文中,我介绍了一种将缓存合并到您的 Grails 应用程序的有效方法。如本文的示例所示,使用 memcached 来缓存单独的请求结果将有助于发挥 Grails 的所有内置分页功能的魔力。另一种方法是缓存用户的所有联系人,然后编写您的所有分页功能代码。这种完整方法有时也很有用,例如,我曾在处理一个来回传递 JSON 的 GWT/Grails 应用程序时使用过这种方法。将缓存数据存储在 JSON 表示中能使其比以前更快,因为我不必将结果转换为 JSON。但是,对于多数情况,本文介绍的快捷方法更有效。
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | James Goodwill 是一位知名作家和技术人员,居住在美国的 Rocky Mountain 地区。他主要致力于扩展 Java 和 Grails Web 应用程序。他发布的文章包括 Developing Java Servlets、Mastering Jakarta Struts 和 Mastering JSP Custom Tags and Tag Libraries。 |
对本文的评价
|