绝大部分情况,使用缓存对效率提升来说是一个非常大的性能提升,但在这种性能提升的背景,缓存数据是否有效,能否支持通知更新,缓存是否支持集群分布式这些问题是作为系统设计环节中必须要考虑的。
大家知道,ibatis对oscache提供了很好的支持,在更新刷新缓存,支持集群方面做的还算是令人满意。平时开发过程中也会遇到一些非SQL查询缓存需求,如登录用户部分登录处理过后信息需要能够被及时缓存起来,这是一个面向于一个用户级缓存,缓存范围非session会话级别的。如果数据库相关数据变动后,又需要对这些缓存数据进行通知更新。为了解决这个问题,我们可以使用两种办法:
1、 对缓存的数据通过SQL语句查询搞定,在OSCACHE缓存策略上进行刷新策略配置;
2、 直接使用OSCACHE中缓存刷新策略,对内存对象数据直接保存;
对与方法1,是基于ibatis本身对sql缓存的机制实现,但这样的实现是基于SQL形式,如果缓存数据不是能很好通过SQL查询结果时,还是存在一定的限制性。为了脱离SQL缓存,找出一种直接缓存,又可以得到相关数据变动更新的办法,我们就需要采用方法2。
针对于方法2中可以直接使用ibatis中oscache缓存,又可以采取数据变更通知。我的原理很简单,在一个jvm中,直接找到ibatis的oscache引用来实现自定义数据缓存。根据这种思路,我们查看一下ibatis中oscache引用代码com.ibatis.sqlmap.engine.cache.oscache. OSCacheController。看到下面OSCacheController代码红色部分,oscache被应用成静态private变量了,还是final,这样更好,反正我们不会去改他的引用,并且这个对象在jvm中就只有一份。大家注意在看flush代码,通过debug实践,可以得出他就是我们缓存更新策略生效时会调用的清理缓存方法,这点很重要,基于这个机制,是实现后面自定义缓存的关键部分。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- package com.ibatis.sqlmap.engine.cache.oscache;
-
- import com.ibatis.sqlmap.engine.cache.CacheController;
- import com.ibatis.sqlmap.engine.cache.CacheModel;
- import com.opensymphony.oscache.base.NeedsRefreshException;
- import com.opensymphony.oscache.general.GeneralCacheAdministrator;
-
- import java.util.Properties;
-
-
-
-
- public class OSCacheController implements CacheController {
-
- private static final GeneralCacheAdministrator CACHE = new GeneralCacheAdministrator();
-
- public void flush(CacheModel cacheModel) {
- CACHE.flushGroup(cacheModel.getId());
- }
-
- public Object getObject(CacheModel cacheModel, Object key) {
- String keyString = key.toString();
- try {
- int refreshPeriod = (int) (cacheModel.getFlushIntervalSeconds());
- return CACHE.getFromCache(keyString, refreshPeriod);
- } catch (NeedsRefreshException e) {
- CACHE.cancelUpdate(keyString);
- return null;
- }
- }
-
- public Object removeObject(CacheModel cacheModel, Object key) {
- Object result;
- String keyString = key.toString();
- try {
- int refreshPeriod = (int) (cacheModel.getFlushIntervalSeconds());
- Object value = CACHE.getFromCache(keyString, refreshPeriod);
- if (value != null) {
- CACHE.flushEntry(keyString);
- }
- result = value;
- } catch (NeedsRefreshException e) {
- try {
- CACHE.flushEntry(keyString);
- } finally {
- CACHE.cancelUpdate(keyString);
- result = null;
- }
- }
- return result;
- }
-
- public void putObject(CacheModel cacheModel, Object key, Object object) {
- String keyString = key.toString();
- CACHE.putInCache(keyString, object, new String[]{cacheModel.getId()});
- }
-
- public void setProperties(Properties props) {
- }
-
- }
接下来的事情很简单,看下面staitc代码部分,利用反射的机制,在jvm中取到OSCacheController.CACHE成员变量的引用,即使它是private,但是在sun公司的合法规范下,还是都能获取的到。:)获取到引用后,我们根据官方的接口也实现一下putObject、getObject、removeObject方法,只是这些方法中我们不在使用ibatis官方的CacheModel缓存对象了,使得方法变得更加简单。注意,我们提供的putObject方法带有cacheId参数,这个参数很重要,根据上面我们提到的官方缓存更新策略生效时,会调用flush方法的原理,我们在缓存自定义数据的同时,一定要指明我们需要使用缓存刷新策略的id是多少。因为通过反射机制,我们拿到了OSCacheController的CACHE引用,CACHE又是静态变量,我们缓存的数据和ibatis本身的缓存数据是用的同一个oacache,并且它更新缓存策略时,我拿过来的oscache对象也会同时更新。
-
-
-
-
-
-
- package com.zjhcsoft.biap;
-
- import java.lang.reflect.Field;
-
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- import com.ibatis.sqlmap.engine.cache.oscache.OSCacheController;
- import com.opensymphony.oscache.base.NeedsRefreshException;
- import com.opensymphony.oscache.general.GeneralCacheAdministrator;
-
-
-
-
-
-
-
- public class IbatisCacheHelper
- {
- private static Logger logger = LoggerFactory.getLogger(IbatisCacheHelper.class);
-
- private static GeneralCacheAdministrator CACHE = null;
-
-
-
-
- private static int FlushIntervalSeconds = 1000 * 60 * 60;
-
-
- static {
- try {
- Field field = OSCacheController.class.getDeclaredField("CACHE");
- field.setAccessible(true);
- CACHE = (GeneralCacheAdministrator) field.get(null);
- field.setAccessible(false);
- }
- catch (Exception e) {
- logger.warn("can't acquire ibatis cache! ", e);
- }
- }
-
- public static void putObject(String key, Object content, String cacheId)
- {
- if (CACHE != null) {
- CACHE.putInCache(key, content, new String[]{cacheId});
- }
- else {
- logger.warn("can't acquire ibatis cache,Data can not put into CACHE ");
- }
- }
-
- public static Object getObject(String key)
- {
- if (CACHE != null) {
- try {
- return CACHE.getFromCache(key, FlushIntervalSeconds);
- }
- catch (NeedsRefreshException e) {
- CACHE.cancelUpdate(key);
- return null;
- }
- }
- else {
- logger.warn("can't acquire ibatis cache,Data can not put into CACHE ");
- return null;
- }
- }
-
- public static void removeObject(String key)
- {
- if (CACHE != null) {
- String keyString = key.toString();
- try {
- Object value = CACHE.getFromCache(keyString, FlushIntervalSeconds);
- if (value != null) {
- CACHE.flushEntry(keyString);
- }
- }
- catch (NeedsRefreshException e) {
- try {
- CACHE.flushEntry(keyString);
- }
- finally {
- CACHE.cancelUpdate(keyString);
- }
- }
- }
- else {
- logger.warn("can't acquire ibatis cache,Data can not put into CACHE ");
- }
- }
- }
接下来我们做个例子,我们BIAP集成系统管理平台提高了统一的集成应用展现框架,并且在用户登录的时候动态去生成用户有权限访问的菜单,菜单是通过登录用户权限信息生成的html字符,生成过程较为耗时。在登录过程中,我们已经对登录相关的查询都进行了ibatis缓存,更新策略如下:
- <sqlMap namespace="sysLogin">
- <cacheModel id="operator-cache" type="OSCACHE">
- <flushInterval hours="24"/>
- <flushOnExecute statement="insertSysResource"/>
- <flushOnExecute statement="updateResource"/>
- <flushOnExecute statement="deleteResource"/>
- <flushOnExecute statement="deleteUserDataAccess"/>
- <flushOnExecute statement="insertUserDataAccess"/>
- <flushOnExecute statement="insertRelRoleRes"/>
- <flushOnExecute statement="deleteRelRoleRes"/>
- <flushOnExecute statement="insertRelation"/>
- <flushOnExecute statement="deleteRelationByFromId"/>
- <flushOnExecute statement="deleteRelationByTargetId"/>
- <property name="size" value="1000"/>
- </cacheModel>
登录代码里我们改动一下组装菜单部分的代码,缓存的key我们使用固定常量CACHE_MENU+用户登录id。在绿色部分先去判断缓存中有没有,处理好的菜单数据,没有的话我们通过调用私有menuProcess(operator)方法处理菜单,并且在下列红色代码部分中,将处理好的菜单数据menu放置在缓存中,并且使用常量CACHE_ID,CACHE_ID值为sysLogin.operator-cache,对没错,就是我们更新策略中SqlMap的namespace+cacheModel的id组合。
- protected String loginProcess(LoginSessionObject operator) {
- <span style="color:#009900;">menu = (String)IbatisCacheHelper.getObject(CACHE_MENU + operator.getUserRowId().toString());</span>
-
- if (StringUtil.isInvalid(menu)){
-
- menuProcess(operator);
-
-
- loginserv.systemPicProcess(lstSytem);
-
- operator.setEnableResource(resRowId.append("[[split]]")
- .append(resource).toString());
-
- <span style="color:#FF0000;">IbatisCacheHelper.putObject(CACHE_MENU + operator.getUserRowId().toString(), menu, CACHE_ID);</span>
- IbatisCacheHelper.putObject(CACHE_SYSTEM_MENU + operator.getUserRowId().toString(), operator.getLstSytem(), CACHE_ID);
- IbatisCacheHelper.putObject(CACHE_ENABLE_RES_IDS + operator.getUserRowId().toString(), operator.getEnableResourceIds(), CACHE_ID);
- IbatisCacheHelper.putObject(CACHE_RESOURCES + operator.getUserRowId().toString(), operator.getEnableResource(), CACHE_ID);
- }else{
-
- operator.setLstSytem((List<BaseMenu>)IbatisCacheHelper.getObject(CACHE_SYSTEM_MENU + operator.getUserRowId().toString()));
- lstSytem = operator.getLstSytem();
-
- operator.setEnableResourceIds((Set<Long>)IbatisCacheHelper.getObject(CACHE_ENABLE_RES_IDS + operator.getUserRowId().toString()));
-
- operator.setEnableResource((String)IbatisCacheHelper.getObject(CACHE_RESOURCES + operator.getUserRowId().toString()));
- }
-
- if (session.get(Constants.JXPortalSyn.PORTALSSOLOGIN) != null
- && !"".equals((String) session
- .get(Constants.JXPortalSyn.PORTALSSOLOGIN))) {
- if (operator.getLstSytem() == null
- || operator.getLstSytem().size() == 0) {
- return "portalSsoError";
- }
- }
-
-
- lstUserDateAccess = operator.getUserDataAccesses();
-
-
- return "main";
- }
接下来我们debug看看效果。第一次登录,从缓存中没有取到登录者的菜单处理数据。

我们将菜单处理后的数据,保存在oscache缓存中。

第二次登录,找到了我们之前的为该用户缓存的数据

我们来修改菜单来检查是否能更具缓存更新策略更新我们的缓存

用debug模式可以观测到GeneralCacheAdministrator中我们的sysLogin.operator-cache更新策略已经被调用。

第三次登录,恭喜,我们的缓存已经接到更新策略执行了清除。我们就不用担心缓存数据中的数据过时了。

总结:我们通过观察官方源码得知ibatis中缓存刷新机制和缓存使用机制,再利用合理规范的反射技术来获取jvm中ibatis使用的oscache对象,按照ibatis的刷新规范在缓存时保存选择使用缓存刷新策略,从而实现了我们自定义缓存和缓存动态更新的功能。重要的不在于大家对这功能的掌握,更关键的是对缓存设计思路的理解。