前几天和朋友讨论数据库Cache的时候,谈到了iBatis框架不支持一级缓存,后来尝试作了一些扩展来支持一级缓存,放出来大家探讨一下。
首先让我们简单回顾一下缓存的概念。直白的说,缓存就是把从数据库里查出来的数据放到内存中,供后续操作使用。例如,某个应用需要让业务人员查询前日的数据报表,可能同时有很多人在查询该数据,并且数据没有实时的要求,则可以在第一次查询的时候把结果放到缓存中,以提高后续操作的效率。缓存分为一级缓存和二级缓存,所谓一级缓存通常是指在一次数据库事物范围内的缓存,当数据库事物提交,连接被释放后,该缓存就被销毁了;所谓二级缓存通常指存在时间比较长,例如1天等。一级缓存通常在事物开启的时候建立,在事物提交或回滚的时候销毁。Hibernate对一级缓存和二级缓存都支持的很好,而ibatis则只对应用二级缓存提供了内置的支持。因此本文重点讨论如何为ibatis提供一级缓存功能。
先介绍一下我的环境:Spring1.2.6 + iBatis2.1.3 + MySql5.0。Spring已经成为事实上的标准,对iBatis提供了非常好的支持。因此我们在此讨论如何在Spring环境下提供对iBatis的一级缓存支持。Spring对iBatis的封装主要是通过SqlMapClientTemplate来实现的,而事物方面这是通过TransactionTemplate来提供支持,因此要提供一级缓存的功能,就要在这两个类上做一些文章。 当前事物的缓存可以放在当前的线程(ThreadLocal)中,看下面的代码:
|
public class CacheModel {
private static final Log logger = LogFactory .getLog(CacheModel.class);
private static final ThreadLocal<Map<Object, Object>> cacheModel = new ThreadLocal<Map<Object, Object>>();
synchronized static boolean exist() { return cacheModel.get() != null; }
public synchronized static void add(Object key, Object value) { if (exist()) { cacheModel.get().put(key, value); } }
public synchronized static Object get(Object key) { if (exist()) { return cacheModel.get().get(key); } return null; }
public synchronized static void initial() { if (!exist()) { cacheModel.set(new HashMap<Object, Object>()); logger.info("Initial current transaction's Cache."); } else { throw new RuntimeException("Current transaction's CacheModel allready initial!"); } }
public synchronized static void destroy() { cacheModel.set(null); logger.info("Destroy current transaction's Cache."); }
} |
接下来扩展TransactionTemplate,实现在开启和结束事物的时候创建和销毁CacheModel:
|
public class CacheTransactionTemplate extends TransactionTemplate { private static final long serialVersionUID = 489621858441822688L;
private boolean cacheable = true;
private boolean isInitialized = false;
/* (non-Javadoc) * @see org.springframework.transaction.support.TransactionTemplate#afterPropertiesSet() */ @Override public void afterPropertiesSet() { super.afterPropertiesSet();
isInitialized = true; }
/* (non-Javadoc) * @see org.springframework.transaction.support.TransactionTemplate#execute(org.springframework.transaction.support.TransactionCallback) */ @Override public Object execute(TransactionCallback action) throws TransactionException { TransactionStatus status = getTransactionManager().getTransaction(this);
initialCacheModel(status);
Object result = null; try { result = action.doInTransaction(status); logger.debug(action); } catch (RuntimeException ex) { // transactional code threw application exception -> rollback rollbackOnException(status, ex); throw ex; } catch (Error err) { // transactional code threw error -> rollback rollbackOnException(status, err); throw err; }
try { getTransactionManager().commit(status); } finally { destoryCacheModel(status); } return result; }
/** * Perform a rollback, handling rollback exceptions properly. * * @param status object representing the transaction * * @param ex the thrown application exception or error * * @throws TransactionException in case of a rollback error */ private void rollbackOnException(TransactionStatus status, Throwable ex) throws TransactionException { logger.debug("Initiating transaction rollback on application exception", ex); try { getTransactionManager().rollback(status); } catch (RuntimeException ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } catch (Error err) { logger.error("Application exception overridden by rollback error", ex); throw err; } finally { destoryCacheModel(status); } }
/** * Initail current transaction's CacheModel. * * <p> * Must be a new Transaction. * * Current transaction's CacheModel must have not been initialized. * </p> * * @param status */ private void initialCacheModel(TransactionStatus status) { if (cacheable && status != null && status.isNewTransaction()) { if (!CacheModel.exist()) { CacheModel.initial(); } } }
/** * Destroy current transaction's CacheModel. * * <p> * Must be a new Transaction. * * Current transaction's CacheModel must have been initialized. * </p> * * @param status */ private void destoryCacheModel(TransactionStatus status) { if (cacheable && status != null && status.isNewTransaction()) { if (CacheModel.exist()) { CacheModel.destroy(); } } }
/** * * @param cacheable */ public void setCacheable(boolean cacheable) { if (!isInitialized) { this.cacheable = cacheable; } }
}
|
上面的代码重写了TransactionTemplate的 execute方法,加入了对CacheModel的支持。接下来再扩展SqlMapClientTemplate,并重写query方法:
|
public class CacheSqlMapClientTemplate extends SqlMapClientTemplate {
private static final Log logger = LogFactory .getLog(CacheSqlMapClientTemplate.class);
private static final String DEFAULT_SPARATOR = "_";
/* (non-Javadoc) * @see org.springframework.orm.ibatis.SqlMapClientTemplate#queryForList(java.lang.String, java.lang.Object, int, int) */ @Override public List queryForList(String statementName, Object parameterObject, int skipResults, int maxResults) throws DataAccessException {
String key = statementName + DEFAULT_SPARATOR + generateKeyPart(parameterObject) + skipResults + maxResults;
List result = queryDataFromCacheModel(key, List.class);
if (result != null) {
return result; }
result = super.queryForList(statementName, parameterObject, skipResults, maxResults);
addDataToCacheModel(key, result);
return result; }
/* (non-Javadoc) * @see org.springframework.orm.ibatis.SqlMapClientTemplate#queryForList(java.lang.String, java.lang.Object) */ @Override public List queryForList(String statementName, Object parameterObject) throws DataAccessException {
String key = statementName + DEFAULT_SPARATOR + generateKeyPart(parameterObject);
List result = queryDataFromCacheModel(key, List.class);
if (result != null) {
return result; }
result = super.queryForList(statementName, parameterObject);
addDataToCacheModel(key, result);
return result; }
/* (non-Javadoc) * @see org.springframework.orm.ibatis.SqlMapClientTemplate#queryForObject(java.lang.String, java.lang.Object, java.lang.Object) */ @Override public Object queryForObject(String statementName, Object parameterObject, Object resultObject) throws DataAccessException {
String key = statementName + DEFAULT_SPARATOR + generateKeyPart(parameterObject) + DEFAULT_SPARATOR + generateKeyPart(resultObject);
Object result = queryDataFromCacheModel(key, Object.class);
if (result != null) {
return result; }
result = super.queryForObject(statementName, parameterObject, resultObject);
addDataToCacheModel(key, result);
return result; }
/* (non-Javadoc) * @see org.springframework.orm.ibatis.SqlMapClientTemplate#queryForObject(java.lang.String, java.lang.Object) */ @Override public Object queryForObject(String statementName, Object parameterObject) throws DataAccessException {
String key = statementName + DEFAULT_SPARATOR + generateKeyPart(parameterObject); Object result = queryDataFromCacheModel(key, Object.class); if (result != null) { return result; }
result = super.queryForObject(statementName, parameterObject);
addDataToCacheModel(key, result);
return result; }
private <T> T queryDataFromCacheModel(Object key, Class<T> type) { if (CacheModel.exist()) {
logger.info("Look up data from current transaction's CacheModel , KEY={" + key + "}");
Object value = CacheModel.get(key);
if (value != null) { return type.cast(value); } else { logger .info("Current transaction's CacheModel dosen't contain any data relate with {" + key + "}"); }
} return null; }
private <T> void addDataToCacheModel(Object key, T value) { if (null == value) { return; } if (CacheModel.exist()) { CacheModel.add(key, value); logger.info("Add data to current transaction's CacheModel , KEY={" + key + "}"); } }
private String generateKeyPart(Object source) { if (source == null) { return ""; } else if (source instanceof String) { return (String) source; } else if (source instanceof Map) { return generate((Map) source); } else { try { return generate(BeanUtils.describe(source)); } catch (Exception e) { return source.toString(); } } }
private String generate(Map props) { try { Iterator iProps = props.keySet().iterator(); StringBuffer retBuffer = new StringBuffer(); while (iProps.hasNext()) { String key = (String) iProps.next(); if ("class".equals(key)) { continue; } retBuffer.append(key).append("=[").append(props.get(key)).append("]"); if (iProps.hasNext()) { retBuffer.append(", "); } } return retBuffer.toString(); } catch (Exception e) { return ""; } } } |
类扩展写好了,下面我们看一下如何配置:
|
<beans default-autowire="byName"> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/batchtest"/> <property name="username" value="root"/> <property name="password" value="foxdesign"/> </bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/>
<bean id="transactionTemplate" class="com.company.CacheTransactionTemplate"> <property name="cacheable"> <value>true</value> </property> </bean>
<bean id="customSqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation" value="SqlMapConfig.xml"/> </bean>
<bean id="sqlMapClientTemplate" class="com.company.CacheSqlMapClientTemplate"> <property name="sqlMapClient" ref="customSqlMapClient"/> </bean>
<bean id="userCacheDAO" class="com.company.imp.UserCacheDAOImpl"> </bean> </beans> |
经过上面的扩展,执行queryForObject和queryForList方法的时候,查询出来的数据将在当前事物内缓存起来,可以看一下下面的测试代码:
|
public void testWithTrans() {
transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus arg0) {
userCacheDAO.getUserByName("Tony");
userCacheDAO.getUserByName("Tony");
userCacheDAO.getUserByName("Aroma");
return null; } }); } |
该方案只是抛砖引玉的提供了一些想法,大家有什么更好的方法可以讨论一下。
本文介绍了一种为iBatis框架增加一级缓存支持的方法,通过扩展Spring中的TransactionTemplate和SqlMapClientTemplate,实现了在数据库事务范围内缓存查询结果。
795





