Grails是一个Web应用程序框架,它利用了Groovy的动态语法和Java平台的后端功能。 memcached是一种通用的分布式内存缓存系统,用于Web上一些流量最大的站点。 将两者结合使用,可以快速构建响应Swift且可扩展的Web应用程序。
在我对memcached和Grails的介绍的第二部分中,我将引导您完成将memcached集成到现有Grails应用程序中的过程。 首先,向您介绍Grails应用程序,并在您的开发环境中进行设置。 然后,我将介绍memcached客户端API,并逐步引导您创建用于包装它的Grails服务。
在Grails应用程序中拥有使用memcached的所有组件之后,您可能想知道如何最好地利用它。 我将讨论最有效的区域,您可以在其中将缓存注入到现有应用程序中,并演示一些用于存储和从缓存中检索值的技术。 最后,您将逐步完成在应用程序的Grails控制器之一中实现缓存的动手练习,并逐步测试结果。
Grails应用程序
为了查看memcached的运行情况,您将需要一个示例Grails应用程序来进行工作。 该应用程序不必太复杂,因此您只需要构建一个简单的联系人管理应用程序即可使用户管理联系人及其相关信息的集合。 这个迷你CRUD应用程序的唯一功能是允许用户使用memcached存储和检索Contact
对象。 首先,在您的开发环境中执行以下命令:
grails create-app
输入应用程序名称。 在此示例中,使用名称contactmanager
。 一旦Grails创建了应用程序,就将cd
转到contactmanager
目录并启动该应用程序:
cd contactmanager
grails run-app
将浏览器打开到http:// localhost:8080 / contactmanager。 如果一切正常,您应该看到Grails欢迎页面。
创建域
接下来,您需要定义Contact
域类。 通过仅向域中添加三个属性,我们将使其非常简单: 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类。
创建控制器
接下来,您将创建将用于Contact
的应用程序Controller
。 首先执行以下命令:
grails create-controller
Contact
现在打开新创建的ContactController
并将其更改为清单2:
清单2.向控制器添加支架
class ContactController {
def scaffold = Contact
}
这将为您提供默认的Grails脚手架。 现在您要添加一些内容。 因此,打开BootStrap.groovy
(用于在启动时初始化Grails应用程序)并对其进行修改,如清单3所示。数据库初始化,动态配置以及将元更改注入Groovy类都是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
添加简单的逻辑以打印查询运行时。 稍后,您将使用此信息来查看多少缓存提高了应用程序的性能。 在将逻辑添加到控制器之前,您首先需要告诉Grails生成Contact
所有脚手架代码。 通过执行grails generate-all
命令来执行此操作:
grails generate-all
现在打开文件contactmanager/grails-app/controllers/ContactController.groovy
。 您应该看到支持Contact
域所需的所有关闭。 找到list
闭包并向其中添加计时语句,如清单4所示:
清单4.使用计时语句的列表关闭
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
,它设置为当前系统时间(以毫秒为单位)。 然后,它运行用来获取列表的两个查询Contacts
和总数Contacts
。 运行这两个查询后,您将再次获得当前时间(以毫秒为单位),并将其设置在endTime变量中。 最后,您将差异除以1,000,以秒为单位查看运行查询所花的时间。
使用grails run-app
重新启动应用grails run-app
并在您浏览联系人列表结果时观察控制台。
将Memcached注入Grails应用程序
将Memcached客户端添加到Grails应用程序中,要做的第一件事是下载适当的jar文件并将其复制到contactmanager / lib目录。 对于此示例,我使用Spymemcached ,这是memcached的Java客户端。 继续下载JAR ; 撰写本文时,最新版本为2.3.1。
将jar文件保存在contactmanager / lib中之后,下一步就是创建一个Groovy类,该类将公开API。 我选择将Grails服务用于此实现有两个很好的理由:首先,所有Grails服务都作为Spring bean管理,因此可以自动注入到Controller
。 其次,作为Spring bean,Grails服务使您可以访问org.springframework.beans.factory.InitializingBean
接口,该接口可让您在设置所有其他属性后初始化服务。
因此,让我们创建服务。 打开您使用的任何IDE或编辑器,并在contactmanager / grails-app / services目录中的清单5中创建Groovy类:
清单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()
,每个方法都带有properties key
和一个function
。 这些方法展示了一种使用缓存的有趣方法,称为备忘录 。
我要做的是传递要查找的项的key
,以及如果get()
未找到key
应执行的function
。 使用此技术可避免重复计算先前处理的输入的结果。 相反,在代码中进行简单的get()
调用既可以检索所需的对象,也可以在高速缓存中找不到该对象的情况下创建该对象。 然后,它也存储结果。 在本文的后面,您将学到更多关于备忘录如何简化缓存交互的信息。
MemcachedService符合ContactController
要将新创建的MemcachedService
添加到您的ContactController
,请将以下行添加到ContactController
:
class ContactController {
def memcachedService
...
}
添加此简单行会自动将MemcachedService
一个实例注入Controller
。 现在,该服务在Controller
可用,但是Controller
应该缓存什么?
使用memcached
在确定要缓存的应用程序数据时,最好记住两个简单的准则。 这些并不是一成不变的,不会适用于所有情况,但是它们为决定要缓存的内容提供了良好的基础:
- 不要缓存经常更改的数据 。 如果要缓存的数据经常更改,那么您将不断更改缓存中存储的值,这将限制缓存的值。
- 如果您有一个ID可以直接标识一个值,则无需缓存该值 。 数据库将能够非常快速地使用其ID查找值。
对于Contact Manager应用程序,这些准则使您明显希望缓存通过联系人分页时返回的数据。 请记住,此数据是在ContactController
的list
闭包中查询的。
list
闭包返回两个值:
-
contactInstanceList
是从数据库检索的Contacts
列表。 -
contactInstanceTotal
表示数据库中的Contacts
总数。
这两项都是缓存的候选者,但让我们从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()
是一个愚蠢的方法。 唯一必要的原因是安全性超出了本文的范围,因此示例应用程序没有用户的概念。 在getContactInstanceTotal()
,与缓存的第一个实际交互开始。 此方法使用键“ my.username:contactInstanceTotal
”调用memcachedService.get()
方法。 如果在缓存中找到密钥,则该值将返回给调用方。 否则,将调用传递给get()
的闭包,并将返回的值与传递的键一起存储在缓存中。 继续并将此代码添加到ContactController
,然后替换以下内容:
def contactInstanceTotal = Contact.count()
调用getContactInstanceTotal()
:
def contactInstanceTotal = getContactInstanceTotal(getUsername())
现在,重新启动应用程序,将telnet插入memcached,然后执行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
),则最终将在每个新请求中写入缓存的数据。 您还需要一种方法来确保您可以在每个匹配的请求中重现密钥。
生成密钥的最简单,最可靠的方法是使用传递给请求的参数的摘要,并在密钥前面添加username
。 假设两次收到相同的参数,结果应该是相同的(假设数据没有改变,我将在下一节中讨论)。 该技术还保证您将能够通过生成参数摘要来生成一致的密钥。
缓存数据
现在您准备开始将数据放入缓存。 如前所述,您将每个请求的结果缓存到list
闭包中。 list
关闭非常简单,用于在数据库中分页浏览Domain
对象的集合。 当请求此闭包时,会将参数集合传递给它。 这些参数指示Grails如何查询数据库。 您可以通过在应用程序的“ Contact
页面中翻页来观看URL的更改,从而看到这些参数的示例。 您还可以在以下URL中查看参数:
http://localhost:8080/contactmanager/contact/list?offset=10&max=10
该请求包含两个参数: offset
和max
。 该请求的结果是10个Contact
的列表。 该列表从第十个Contact
开始,一直持续到第十九个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()
方法非常简单。 它接受传递给控制器的参数,并将其String
值转换为字节数组,然后将其传递给MessageDigest
。 然后,它使用BASE64Encoder生成与关键username
预先计划的key
-从而保证你会得到一个可重复的钥匙给同一组的params
。
关于清单7唯一要注意的是对结果的迭代,将每个Contact
的内容转换为一个映射,并将每个映射存储在ArrayList
。 这是必需的,因为Grails Domain
对象不可序列化,而映射可序列化。
在将此方法添加到示例Grails应用程序中之后,请替换:
def contactInstanceList = Contact.list(params)
调用getCachedContactInstanceList()
:
def cachedContactInstanceList = getCachedContactInstanceList(getUsername())
重新启动Grails并翻阅“ Contact
几下,重复一些请求。 在执行此操作时,请监视控制台stdout
。 您会注意到,在重复请求时响应时间大大缩短了。
使缓存无效
缓存数据的响应时间大大缩短,但是更新应用程序的联系人时会发生什么? 在添加新Contact
之前,浏览至最后一页,并查看其中的Contact
列表。 现在添加几个新的Contacts
,然后再次浏览到最后一页。 您会注意到没有列出新的Contacts
。 问题是您已经缓存了将返回结果的请求结果。
解决方案是使缓存无效-但是如何呢? 您甚至没有记录已经放入缓存中的键的记录。 您需要做的第一件事是确保您可以查找所有需要失效的密钥。 一种简单的方法是缓存键列表并将该列表与用户相关联。
在返回缓存的Contacts
之前,尝试将清单8中的代码片段添加到getCachedContactInstanceList()
方法中:
清单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
}
接下来,添加代码以使缓存无效。 将使缓存无效的方法必须执行四个步骤:
- 检索缓存的密钥
- 删除与检索到的键关联的键/值对
- 删除缓存键的数组
- 删除代表
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")
}
代码本身非常简单。 它完成了四个必要步骤,并删除了与用户名关联的Contact
的所有缓存信息。 需要将此方法的调用添加到所有导致Contacts
数据发生更改的方法/闭包中。 此应用程序中的闭包包括delete
和save
。 清单10显示了对闭包的更改:
清单10.更新的删除和保存闭包
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
s存储在缓存中的键。 复制其中一些键以供以后使用,然后添加,删除和编辑一些Contact
。 首先,您将看到在修改数据时正确显示了数据。 作为您的最后一个动作,添加一个新联系人并使用“ Show Contact
页面停止浏览。
返回到telnet会话, get
复制的键, contactKeyList
和contactInstanceTotal
。 您会注意到所有这些值都已从缓存中删除,并且缓存对于所有即将到来的请求都处于正确的状态。
结论
在本文中,我写了一种将缓存有效地合并到Grails应用程序中的方法。 就像您在此处所做的那样,使用memcached缓存单个请求结果可以使Grails的所有内置分页魔术保持不变。 一种替代方法是缓存所有用户的联系人,然后编写您自己的所有分页代码。 这种长期的做法在某些情况下是有意义的。 例如,在处理来回传递JSON的GWT / Grails应用程序时,我只做了一次。 将缓存的数据存储在JSON表示中使其变得更快,因为我不必将结果转换为JSON。 但是,对于大多数目的而言,本文介绍的快速处理技术是有效的。
翻译自: https://www.ibm.com/developerworks/opensource/library/j-memcached2/index.html