IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology | Open source  >

memcached 和 Grails,第 2 部分:将 memcached 集成到 Grails

Grails 应用程序中的高效缓存

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

英文原文

英文原文


级别: 初级

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 对象。首先,在您的开发环境中执行以下命令:

grails create-app

输入应用程序名称,在本例中,输入 contactmanager。Grails 创建这个应用程序后,cdcontactmanager 目录并启动应用程序:

cd contactmanager
grails run-app

打开您的浏览器并导航到 http://localhost:8080/contactmanager。如果一切正常,您将看到 Grails 欢迎页面。

创建域

下面,您需要定义 Contact 域类。为简便起见,我们只向该域添加 3 个属性:firstNamelastNameemail。添加这些属性后,您的 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 个联系人。现在重新启动应用程序并浏览一下,这样,您将对您正在进行的工作有所了解。

HSQLDB

HSQLDB 是 Grails 包自带的默认数据库。默认情况下,它配置为一个内存数据库,这使其非常适合部署和测试,但它不适用生产环境。参见 “参考资料” 部分 了解关于 HSQLDB 的更多信息

向 ContactController 添加逻辑

创建这个 Grails 样例应用程序的最后一步是向 ContactController 添加简单的逻辑以输出查询运行时。稍后,您将使用这个信息来了解改进应用程序性能的缓存量。在向这个 Controller 添加逻辑之前,您首先需要告知 Grails 为 Contact 生成所有支架代码。为此,执行 grails generate-all 命令:

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,它设置为当前系统时间,单位为毫秒。然后,该代码执行两个查询,用于获取一列 ContactContact 的总数。运行这两个查询后,您将再次获得一个当前时间(以毫秒为单位),将其设置为 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 应用程序,这些准则清楚地表明:您需要缓存分页浏览联系人时返回的数据。还记得吗,这个数据在 ContactControllerlist 闭包中查询过。

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

这个请求包含两个参数:offsetmax。这个请求的结果是一个包含 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 数据发生变化的 “方法/闭包” 中。这个应用程序中的闭包包含 deletesave 方法。清单 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 命令:

flush_all
OK

下面,打开您的浏览器并开始分页浏览 Contacts 列表。与此同时,观察控制台,您将看到用于在缓存中存储 Contact 的键。复制一些键以备后用,然后添加、删除并编辑一些 Contact。首先,您将看到数据在修改时能够正确显示。最后,添加一个新联系人并停止浏览 Show Contact 页面。

返回您的 telnet 会话,在您先前复制的键 - contactKeyListcontactInstanceTotal - 上执行一个 get。您将注意到,所有这些值都已从缓存删除,缓存处于适当状态以迎接所有即将到来的请求。





回页首


结束语

在本文中,我介绍了一种将缓存合并到您的 Grails 应用程序的有效方法。如本文的示例所示,使用 memcached 来缓存单独的请求结果将有助于发挥 Grails 的所有内置分页功能的魔力。另一种方法是缓存用户的所有联系人,然后编写您的所有分页功能代码。这种完整方法有时也很有用,例如,我曾在处理一个来回传递 JSON 的 GWT/Grails 应用程序时使用过这种方法。将缓存数据存储在 JSON 表示中能使其比以前更快,因为我不必将结果转换为 JSON。但是,对于多数情况,本文介绍的快捷方法更有效。



参考资料

学习

获得产品和技术

讨论


关于作者

James Goodwill

James Goodwill 是一位知名作家和技术人员,居住在美国的 Rocky Mountain 地区。他主要致力于扩展 Java 和 Grails Web 应用程序。他发布的文章包括 Developing Java ServletsMastering Jakarta StrutsMastering JSP Custom Tags and Tag Libraries




对本文的评价










回页首


Java 和所有基于 Java 的商标是 Sun Microsystems, Inc. 在美国和/或其他国家的商标。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。

IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款