BroToolkit/BroFramework的缓存解决方案有多个插件与API组成,涉及到浏览器缓存、页面缓存、数据库缓存、缓存集群同步等多个方面。
浏览器缓存是通过设置 response 的 http 头来设置的。
- 所有控制器操作中,可以直接调用 nocache() 方法来禁用浏览器缓存
- 附件下载时,会自动设置长期有效的浏览器缓存
- 插件 cache-headers 提供了多个设置 http 头的 API 和配置,详见 插件首页
- 插件 assets-pipeline 提供了图片、js、css等资源缓存的解决方案(自动在 url 后面拼上一个文件唯一标示)
- 在浏览器端,关于 AJAX 缓存的问题,可以参考 AJAX 缓存概述
Hibernate 的默认缓存解决方案是 EhCache,因此,BroToolkit 中内置了 cache-ehcache 插件作为缓存实现方案。
- BroTooklie 的 DataSource.groovy 模板中,默认配置了下面几个缓存参数,启用了查询缓存、二级缓存等
hibernate {
cache.use_second_level_cache = true
cache.use_query_cache = true
cache.region.factory_class = 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory'
} - 此外,Grails 的动态查阅API、Domain类中都有相关的配置,详见 Caching Strategy
当一个用户访问某个页面(GSP、控制器操作)时,插件 cache 提供了将渲染结果缓存下来的功能,以便下次访问时,直接从缓存中读出渲染结果返回给浏览器,同时,我们采用了 cache-ehcache 插件作为缓存的实现。
cache 的文档详见:Cache Plugin,常用的功能简介如下。
缓存、清空控制器操作的渲染结果,如下例所示:
import grails.plugin.cache.CacheEvict
import grails.plugin.cache.Cacheable
class TestController {
// 缓存到名为 message 的缓存中
@Cacheable('message')
def lookup() {
println "called 'lookup'"
}
// 清空名为 message 的缓存
@CacheEvict(value='message', allEntries=true)
def evict() {
println "called 'evict'"
}
}
需注意的是,这里没有指定缓存的 key,默认会使用 params 来作为key,比如:
- 用户访问 /test 时,会生成一个 key 为空的缓存条目
- 用户再次访问 /test 时,会直接访问缓存中的数据
- 如果用户访问 /test?foo=bar,由于参数不同,则会创建一个新的缓存条目
但是,当 params 一致,但是由不同用户访问时,由于权限等各种原因,看到的内容应该是不一样的,这里就必须为不同用户设置不同的key,因此,我们扩展了一下注解 @Cachable 的 key 参数,如下例所示:
@Cacheable(value="foobar", key="#bropen.toolkit.web.cache.UserKeyGenerator")
def test() {
......
}
即,当key为 #bropen.toolkit.web.cache.UserKeyGenerator 时,缓存的 key 中会自动拼上当前用户信息,从而实现为不同用户缓存不同的内容。另外,还可以设置 key 为 #bropen.toolkit.web.cache.SessionKeyGenerator,在 key 中拼上当前的 Session 信息,用户注销后缓存就不再可用了。
除了控制器缓存外,cache插件还提供了gsp标签来缓存视图中的一部分或者一个视图模板,下面贴两个官方示例:
<cache:block>
<!-- Any valid markup may be included here, including dynamic expressions, invoking other tags, etc.... -->
</cache:block>
<cache:block key="${currentUser.id}">
<!-- Any valid markup may be included here, including dynamic expressions, invoking other tags, etc.... -->
</cache:block>
<cache:render template="myTemplate" model="[name: 'Some Value']"/>
<cache:render template="myTemplate" model="[name: 'Some Value']" key="${currentUser.id}"/>
此外,cache插件的功能并非仅限于此,详见官方文档。
cache插件提供的API,开发不是方便,且缺乏API文档,因此,BroToolkit 提供了一个基于 EhCache 的 bropen.toolkit.utils.grails.CacheUtils 的工具类,主要如下:
- Ehcache createCache
创建缓存,一般用不到,get、put、doWithBlockingCache时都会自动创建缓存 - Object get(String cacheName, Serializable key)
从缓存中取一个值,如果key所对应的缓存条目存在且未过期,则返回缓存值,否则返回null
println CacheUtils.get( "foobar", "a key") - boolean put(String cacheName, Serializable key, Object value, boolean blocking=false)
将键值对缓存,如果老缓存存在,会自动更新
CacheUtils.put( "foobar", "a key", "the value" ) - boolean evict(String cacheName, Serializable key)
从缓存中删除 key 对应的缓存条目 - flushCache(String cacheName)
清空名为 cacheName 的缓存中的所有条目 - Object doWithBlockingCache(String cacheName, Serializable key, Closure closure)
执行闭包参数 closure 中的代码,并将其结果缓存下来。
下次调用此 API 时,如果 key 在缓存中存在,则直接返回缓存中的值,而不执行闭包。
BTW:人组组织树都是采用这种方式缓存的
示例如下:
// 将闭包执行的结果缓存下来
CacheUtils.doWithBlockingCache( "foobar", "a key", {
return "the value"
})
当然,cache 的 key 或 value 除了字符串外,还可以为任何可以序列化(implements Serializable)的 Java Bean。
此外,cache等插件还在系统中注册了一些 spring bean,常见的如 ehcacheCacheManager,即ehcache的管理器,通过它可以获得各个缓存,并进行操作,如:
def cache = ctx.ehcacheCacheManager.getEhcache("osm")
println cache.getSize()
println cache.getKeys()
println cache.getCacheConfiguration()
cache.remove("a key")
ehcache 的配置详情可以参考官方文档 Cache EhCache。
BroToolkit 中,通过 BroToolkitCacheConfig 配置了:
- 一些缓存默认值
- 最长发呆期(timeToIdleSeconds):run-app模式下设为 30 秒,war包模式下设为 30 分
- 最长存活期(timeToLiveSeconds):run-app模式下设为 30 秒,war包模式下设为 4 小时
- 内存中的最大条目数(maxElementsInMemory):10000
- 磁盘缓存位置(diskStore)
- 根据当前主机和应用信息,在系统临时文件夹下生成唯一的缓存文件夹
- 系统启动时会在日志中打印出上述文件夹的位置
- 同时设置了两个缓存监听器工程,用于集群环境下的缓存同步配置,默认为异步方式:
- cacheEventListenerFactorySync:同步
- cacheEventListenerFactoryAsync:异步
- 通过外部配置文件 clusters.properties,设置集群的同步地址信息
通过阅读 BroToolkitCacheConfig、BroFrameworkCacheConfig、BroBPMCacheConfig 中的示例,以及官方文档中的示例,缓存配置本身非常简单,如下例所示:
// 在下面的文件夹下新建一个以 CacheConfig 结尾的配置文件
// grails-app/conf/MyCacheConfig.groovy
// 最后加载(大于 BroBPMCacheConfig 中的值)
order = 2000
config = {
// osm相关的缓存:30'
cache {
name "osm"
timeToLiveSeconds (application.warDeployed ? 30*60 : 60)
timeToIdleSeconds 0 // 没有idle时间,缓存创建时间+live时间后就过期。详细可以参考Element方法getExpirationTime的源码
maxElementsInMemory 100
}
// SSO缓存:10',同步
cache {
name "sso"
timeToLiveSeconds 10*60
timeToIdleSeconds 0
cacheEventListenerFactoryName 'cacheEventListenerFactorySync'
maxElementsInMemory 50
}
}
集群的情况下,如果是水平集群,并且主机名映射在对外IP地址上(不是127.xxx),则无须任何配置即可支持缓存的集群同步,否则,需要在每个集群节点(JVM)下部署 clusters.properties 文件。该文件的部署位置默认为 {user.dir}/bropen/clusters.properties,可以通过 Config 配置 bropen.toolkit.cluster.config.file 改变其位置。示例如下:
# 节点1的 clusters.properties 文件
# 主机地址、应用服务器端口
server.host = 192.168.1.11
server.port = 8080
# 节点2的 clusters.properties 文件
# 主机地址、应用服务器端口
server.host = 192.168.1.12
server.port = 8080
需要注意的是,如果是垂直集群,需要将不同的JVM节点绑定到不同的IP地址上。