关闭

MyBatis 缓存机制深度解剖 / 自定义二级缓存

706人阅读 评论(1) 收藏 举报
分类:

感谢有奉献精神的人

转自:http://denger.iteye.com/blog/1126423/


缓存概述 

  • 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持;
  • 一级缓存基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
  • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache、Hazelcast等。
  • 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
  • MyBatis 的缓存采用了delegate机制 及 装饰器模式设计,当put、get、remove时,其中会经过多层 delegate cache 处理,其Cache类别有:BaseCache(基础缓存)、EvictionCache(排除算法缓存) 、DecoratorCache(装饰器缓存):          BaseCache         :为缓存数据最终存储的处理类,默认为 PerpetualCache,基于Map存储;可自定义存储处理,如基于EhCache、Memcached等; 
              EvictionCache    :当缓存数量达到一定大小后,将通过算法对缓存数据进行清除。默认采用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等; 
              DecoratorCache:缓存put/get处理前后的装饰器,如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache的数据 put或get 进行序列化及反序列化处理、当设置flushInterval(默认1/h)后,则使用 ScheduledCache 对缓存数据进行定时刷新等。
  • 一般缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 Key 的生成采取规则为:[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。
  • 对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock,使用ReentrantReadWriteLock 的实现,从而通过 Lock 机制防止在并发 Write Cache 过程中线程安全问题。

源码剖解 
接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。 
执行过程: 

① 通常情况下,我们需要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操作,上述根据产品 ID 获取 Product 对象。 
② 当调用 ProductMapper 时中的方法时,其实这里所调用的是 MapperProxy 中的方法,并且 MapperProxy已经将将所有方法拦截,其具体原理及分析,参考 MyBatis+Spring基于接口编程的原理分析,其 invoke 方法代码为:
Java代码  收藏代码
  1. //当调用 Mapper 所有的方法时,将都交由Proxy 中的 invoke 处理:  
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  3.     try {  
  4.       if (!OBJECT_METHODS.contains(method.getName())) {  
  5.         final Class declaringInterface = findDeclaringInterface(proxy, method);  
  6.         // 最终交由 MapperMethod 类处理数据库操作,初始化 MapperMethod 对象  
  7.         final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession);  
  8.         // 执行 mapper method,返回执行结果   
  9.         final Object result = mapperMethod.execute(args);  
  10.         ....  
  11.         return result;  
  12.       }  
  13.     } catch (SQLException e) {  
  14.       e.printStackTrace();  
  15.     }  
  16.     return null;  
  17.   }  

③其中的 mapperMethod 中的 execute  方法代码如下: 
Java代码  收藏代码
  1. public Object execute(Object[] args) throws SQLException {  
  2.     Object result;  
  3.     // 根据不同的操作类别,调用 DefaultSqlSession 中的执行处理  
  4.     if (SqlCommandType.INSERT == type) {  
  5.       Object param = getParam(args);  
  6.       result = sqlSession.insert(commandName, param);  
  7.     } else if (SqlCommandType.UPDATE == type) {  
  8.       Object param = getParam(args);  
  9.       result = sqlSession.update(commandName, param);  
  10.     } else if (SqlCommandType.DELETE == type) {  
  11.       Object param = getParam(args);  
  12.       result = sqlSession.delete(commandName, param);  
  13.     } else if (SqlCommandType.SELECT == type) {  
  14.       if (returnsList) {  
  15.         result = executeForList(args);  
  16.       } else {  
  17.         Object param = getParam(args);  
  18.         result = sqlSession.selectOne(commandName, param);  
  19.       }  
  20.     } else {  
  21.       throw new BindingException("Unkown execution method for: " + commandName);  
  22.     }  
  23.     return result;  
  24.   }  
由于这里是根据 ID 进行查询,所以最终调用为 sqlSession.selectOne函数。也就是接下来的的 DefaultSqlSession.selectOne 执行; 
④ ⑤ 可以在 DefaultSqlSession 看到,其 selectOne 调用了 selectList 方法:
Java代码  收藏代码
  1. public Object selectOne(String statement, Object parameter) {  
  2.     List list = selectList(statement, parameter);  
  3.     if (list.size() == 1) {  
  4.       return list.get(0);  
  5.     }   
  6.     ...  
  7. }  
  8.   
  9. public List selectList(String statement, Object parameter, RowBounds rowBounds) {  
  10.     try {  
  11.       MappedStatement ms = configuration.getMappedStatement(statement);  
  12.       // 如果启动用了Cache 才调用 CachingExecutor.query,反之则使用 BaseExcutor.query 进行数据库查询   
  13.       return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
  14.     } catch (Exception e) {  
  15.       throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
  16.     } finally {  
  17.       ErrorContext.instance().reset();  
  18.     }  
  19. }  
⑥到这里,已经执行到具体数据查询的流程,在分析 CachingExcutor.query 前,先看看 MyBatis 中 Executor 的结构及构建过程。 


执行器(Executor): 
Executor:  执行器接口。也是最终执行数据获取及更新的实例。其类结构如下: 
 
BaseExecutor: 基础执行器抽象类。实现一些通用方法,如createCacheKey 之类。并且采用 模板模式 将具体的数据库操作逻辑(doUpdate、doQuery)交由子类实现。另外,可以看到变量 localCache: PerpetualCache,在该类采用 PerpetualCache 实现基于 Map 存储的一级缓存,其 query 方法如下:
Java代码  收藏代码
  1. public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {  
  2.     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());  
  3.     // 执行器已关闭  
  4.     if (closed) throw new ExecutorException("Executor was closed.");  
  5.     List list;  
  6.     try {  
  7.       queryStack++;   
  8.       // 创建缓存Key  
  9.       CacheKey key = createCacheKey(ms, parameter, rowBounds);   
  10.       // 从本地缓存在中获取该 key 所对应 的结果集  
  11.       final List cachedList = (List) localCache.getObject(key);   
  12.       // 在缓存中找到数据  
  13.       if (cachedList != null) {   
  14.         list = cachedList;  
  15.       } else { // 未从本地缓存中找到数据,开始调用数据库查询  
  16.         //为该 key 添加一个占位标记  
  17.         localCache.putObject(key, EXECUTION_PLACEHOLDER);   
  18.         try {  
  19.           // 执行子类所实现的数据库查询 操作  
  20.           list = doQuery(ms, parameter, rowBounds, resultHandler);   
  21.         } finally {  
  22.           // 删除该 key 的占位标记  
  23.           localCache.removeObject(key);  
  24.         }  
  25.         // 将db中的数据添加至本地缓存中  
  26.         localCache.putObject(key, list);  
  27.       }  
  28.     } finally {  
  29.       queryStack--;  
  30.     }  
  31.     // 刷新当前队列中的所有 DeferredLoad实例,更新 MateObject  
  32.     if (queryStack == 0) {   
  33.       for (DeferredLoad deferredLoad : deferredLoads) {  
  34.         deferredLoad.load();  
  35.       }  
  36.     }  
  37.     return list;  
  38.   }  
BatchExcutorReuseExcutor SimpleExcutor: 这几个就没什么好说的了,继承了 BaseExcutor 的实现其 doQuery、doUpdate 等方法,同样都是采用 JDBC 对数据库进行操作;三者区别在于,批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在Mybatis 的文档上都有详细说明。 

CachingExecutor: 二级缓存执行器。个人觉得这里设计的不错,灵活地使用 delegate机制。其委托执行的类是 BaseExcutor。 当无法从二级缓存获取数据时,同样需要从 DB 中进行查询,于是在这里可以直接委托给 BaseExcutor 进行查询。其大概流程为: 

流程为: 从二级缓存中进行查询 -> [如果缓存中没有,委托给 BaseExecutor] -> 进入一级缓存中查询 -> [如果也没有] -> 则执行 JDBC 查询,其 query 代码如下:
Java代码  收藏代码
  1. public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {  
  2.     if (ms != null) {  
  3.       // 获取二级缓存实例  
  4.       Cache cache = ms.getCache();  
  5.       if (cache != null) {  
  6.         flushCacheIfRequired(ms);  
  7.         // 获取 读锁( Read锁可由多个Read线程同时保持)  
  8.         cache.getReadWriteLock().readLock().lock();  
  9.         try {  
  10.           // 当前 Statement 是否启用了二级缓存  
  11.           if (ms.isUseCache()) {  
  12.             // 将创建 cache key 委托给 BaseExecutor 创建  
  13.             CacheKey key = createCacheKey(ms, parameterObject, rowBounds);  
  14.             final List cachedList = (List) cache.getObject(key);  
  15.             // 从二级缓存中找到缓存数据  
  16.             if (cachedList != null) {  
  17.               return cachedList;  
  18.             } else {  
  19.               // 未找到缓存,很委托给 BaseExecutor 执行查询  
  20.               List list = delegate.query(ms, parameterObject, rowBounds, resultHandler);  
  21.               tcm.putObject(cache, key, list);  
  22.               return list;  
  23.             }  
  24.           } else { // 没有启动用二级缓存,直接委托给 BaseExecutor 执行查询   
  25.             return delegate.query(ms, parameterObject, rowBounds, resultHandler);  
  26.           }  
  27.         } finally {  
  28.           // 当前线程释放 Read 锁  
  29.           cache.getReadWriteLock().readLock().unlock();  
  30.         }  
  31.       }  
  32.     }  
  33.     return delegate.query(ms, parameterObject, rowBounds, resultHandler);  
  34. }  
至此,已经完完了整个缓存执行器的整个流程分析,接下来是对缓存的 缓存数据管理实例进行分析,也就是其 Cache 接口,用于对缓存数据 put 、get及remove的实例对象。 


Cache 委托链构建: 
正如最开始的缓存概述所描述道,其缓存类的设计采用 装饰模式,基于委托的调用机制。 
缓存实例构建: 
缓存实例的构建 ,Mybatis 在解析其 Mapper 配置文件时就已经将该实现初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中可以看到: 
Java代码  收藏代码
  1. private void cacheElement(XNode context) throws Exception {  
  2.     if (context != null) {  
  3.       // 基础缓存类型  
  4.       String type = context.getStringAttribute("type""PERPETUAL");  
  5.       Class typeClass = typeAliasRegistry.resolveAlias(type);  
  6.       // 排除算法缓存类型  
  7.       String eviction = context.getStringAttribute("eviction""LRU");  
  8.       Class evictionClass = typeAliasRegistry.resolveAlias(eviction);  
  9.       // 缓存自动刷新时间  
  10.       Long flushInterval = context.getLongAttribute("flushInterval");  
  11.       // 缓存存储实例引用的大小  
  12.       Integer size = context.getIntAttribute("size");  
  13.       // 是否是只读缓存  
  14.       boolean readWrite = !context.getBooleanAttribute("readOnly"false);  
  15.       Properties props = context.getChildrenAsProperties();  
  16.       // 初始化缓存实现  
  17.       builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);  
  18.     }  
  19.   }  
以下是  useNewCache 方法实现: 
Java代码  收藏代码
  1. public Cache useNewCache(Class typeClass,  
  2.                            Class evictionClass,  
  3.                            Long flushInterval,  
  4.                            Integer size,  
  5.                            boolean readWrite,  
  6.                            Properties props) {  
  7.     typeClass = valueOrDefault(typeClass, PerpetualCache.class);  
  8.     evictionClass = valueOrDefault(evictionClass, LruCache.class);  
  9.     // 这里构建 Cache 实例采用 Builder 模式,每一个 Namespace 生成一个  Cache 实例  
  10.     Cache cache = new CacheBuilder(currentNamespace)  
  11.         // Builder 前设置一些从XML中解析过来的参数  
  12.         .implementation(typeClass)  
  13.         .addDecorator(evictionClass)  
  14.         .clearInterval(flushInterval)  
  15.         .size(size)  
  16.         .readWrite(readWrite)  
  17.         .properties(props)  
  18.         // 再看下面的 build 方法实现  
  19.         .build();  
  20.     configuration.addCache(cache);  
  21.     currentCache = cache;  
  22.     return cache;  
  23. }  
  24.   
  25. public Cache build() {  
  26.     setDefaultImplementations();  
  27.     // 创建基础缓存实例  
  28.     Cache cache = newBaseCacheInstance(implementation, id);  
  29.     setCacheProperties(cache);  
  30.     // 缓存排除算法初始化,并将其委托至基础缓存中  
  31.     for (Class<? extends Cache> decorator : decorators) {  
  32.       cache = newCacheDecoratorInstance(decorator, cache);  
  33.       setCacheProperties(cache);  
  34.     }  
  35.     // 标准装饰器缓存设置,如LoggingCache之类,同样将其委托至基础缓存中  
  36.     cache = setStandardDecorators(cache);  
  37.     // 返回最终缓存的责任链对象  
  38.     return cache;  
  39. }  
最终生成后的缓存实例对象结构: 
 
可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。 


Cache 实例解剖: 
实例类:SynchronizedCache 
说   明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。 
解   剖: 
对于 Lock 机制来说,其分为 Read 和 Write 锁,其 Read 锁允许多个线程同时持有,而 Write 锁,一次能被一个线程持有,如果当 Write 锁没有释放,其它需要 Write 的线程只能等待其释放才能去持有。 
其代码实现:
Java代码  收藏代码
  1. public void putObject(Object key, Object object) {  
  2.     acquireWriteLock();  // 获取 Write 锁  
  3.     try {  
  4.       delegate.putObject(key, object); // 委托给下一个 Cache 执行 put 操作  
  5.     } finally {  
  6.       releaseWriteLock(); // 释放 Write 锁  
  7.     }  
  8.   }  
对于 Read 数据来说,也是如此,不同的是 Read 锁允许多线程同时持有 : 
Java代码  收藏代码
  1. public Object getObject(Object key) {  
  2.     acquireReadLock();  
  3.     try {  
  4.       return delegate.getObject(key);  
  5.     } finally {  
  6.       releaseReadLock();  
  7.     }  
  8.   }  
其具体原理可以看看 jdk concurrent 中的 ReadWriteLock 实现。 


实例类:LoggingCache 
说   明:用于日志记录处理,主要输出缓存命中率信息。 
解   剖: 
说到缓存命中信息的统计,只有在 get 的时候才需要统计命中率: 
Java代码  收藏代码
  1. public Object getObject(Object key) {  
  2.     requests++; // 每调用一次该方法,则获取次数+1  
  3.     final Object value = delegate.getObject(key);  
  4.     if (value != null) {  // 命中! 命中+1  
  5.       hits++;  
  6.     }  
  7.     if (log.isDebugEnabled()) {  
  8.       // 输出命中率。计算方法为: hits / requets 则为命中率  
  9.       log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  
  10.     }  
  11.     return value;  
  12. }  



实例类:SerializedCache 
说   明:向缓存中 put 或 get 数据时的序列化及反序列化处理。 
解   剖: 
序列化在Java里面已经是最基础的东西了,这里也没有什么特殊之处: 
Java代码  收藏代码
  1. public void putObject(Object key, Object object) {  
  2.      // PO 类需要实现 Serializable 接口  
  3.     if (object == null || object instanceof Serializable) {  
  4.       delegate.putObject(key, serialize((Serializable) object));   
  5.     } else {  
  6.       throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);  
  7.     }  
  8.   }  
  9.   
  10.   public Object getObject(Object key) {  
  11.     Object object = delegate.getObject(key);  
  12.     // 获取数据时对 二进制数据进行反序列化  
  13.     return object == null ? null : deserialize((byte[]) object);  
  14.   }  
其 serialize 及 deserialize 代码就不必要贴了。 


实例类:LruCache 
说   明:最近最少使用的:移除最长时间不被使用的对象,基于LRU算法。 
解   剖: 
这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好象之前看过 XMemcached 的 LRU 算法也是这样实现的。 
初始化 LinkedHashMap,默认为大小为 1024 个元素: 
Java代码  收藏代码
  1. public LruCache(Cache delegate) {  
  2.     this.delegate = delegate;  
  3.     setSize(1024); // 设置 map 默认大小  
  4. }  
  5. public void setSize(final int size) {  
  6.     // 设置其 capacity 为size, 其 factor 为.75F  
  7.     keyMap = new LinkedHashMap(size, .75F, true) {  
  8.       // 覆盖该方法,当每次往该map 中put 时数据时,如该方法返回 True,便移除该map中使用最少的Entry  
  9.       // 其参数  eldest 为当前最老的  Entry  
  10.       protected boolean removeEldestEntry(Map.Entry eldest) {  
  11.         boolean tooBig = size() > size;  
  12.         if (tooBig) {  
  13.           eldestKey = eldest.getKey(); //记录当前最老的缓存数据的 Key 值,因为要委托给下一个 Cache 实现删除  
  14.         }  
  15.         return tooBig;  
  16.       }  
  17.     };  
  18.   }  
  19.   
  20. public void putObject(Object key, Object value) {  
  21.     delegate.putObject(key, value);  
  22.     cycleKeyList(key);  // 每次 put 后,调用移除最老的 key  
  23. }  
  24. // 看看当前实现是否有 eldestKey, 有的话就调用 removeObject ,将该key从cache中移除  
  25. private void cycleKeyList(Object key) {  
  26.     keyMap.put(key, key); // 存储当前 put 到cache中的 key 值  
  27.     if (eldestKey != null) {  
  28.       delegate.removeObject(eldestKey);  
  29.       eldestKey = null;  
  30.     }  
  31.   }  
  32.   
  33. public Object getObject(Object key) {  
  34.     keyMap.get(key); // 便于 该 Map 统计 get该key的次数  
  35.     return delegate.getObject(key);  
  36.   }  


实例类:PerpetualCache 
说   明:这个比较简单,直接通过一个 HashMap 来存储缓存数据。所以没什么说的,直接看下面的 MemcachedCache 吧。 


自定义二级缓存/Memcached 
其自定义二级缓存也较为简单,它本身默认提供了对 Ehcache 及 Hazelcast 的缓存支持:Mybatis-Cache,我这里参考它们的实现,自定义了针对 Memcached 的缓存支持,其代码如下: 
Java代码  收藏代码
  1. package com.xx.core.plugin.mybatis;  
  2.   
  3. import java.util.LinkedList;  
  4. import java.util.concurrent.locks.ReadWriteLock;  
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;  
  6.   
  7. import org.apache.ibatis.cache.Cache;  
  8. import org.slf4j.Logger;  
  9. import org.slf4j.LoggerFactory;  
  10.   
  11. import com.xx.core.memcached.JMemcachedClientAdapter;  
  12. import com.xx.core.memcached.service.CacheService;  
  13. import com.xx.core.memcached.service.MemcachedService;  
  14.   
  15. /** 
  16.  * Cache adapter for Memcached. 
  17.  *  
  18.  * @author denger 
  19.  */  
  20. public class MemcachedCache implements Cache {  
  21.   
  22.     // Sf4j logger reference  
  23.     private static Logger logger = LoggerFactory.getLogger(MemcachedCache.class);  
  24.   
  25.     /** The cache service reference. */  
  26.     protected static final CacheService CACHE_SERVICE = createMemcachedService();  
  27.   
  28.     /** The ReadWriteLock. */  
  29.     private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  
  30.   
  31.     private String id;  
  32.     private LinkedList<String> cacheKeys = new LinkedList<String>();  
  33.   
  34.     public MemcachedCache(String id) {  
  35.         this.id = id;  
  36.     }  
  37.     // 创建缓存服务类,基于java-memcached-client  
  38.     protected static CacheService createMemcachedService() {  
  39.         JMemcachedClientAdapter memcachedAdapter;  
  40.   
  41.         try {  
  42.             memcachedAdapter = new JMemcachedClientAdapter();  
  43.         } catch (Exception e) {  
  44.             String msg = "Initial the JMmemcachedClientAdapter Error.";  
  45.             logger.error(msg, e);  
  46.             throw new RuntimeException(msg);  
  47.         }  
  48.         return new MemcachedService(memcachedAdapter);  
  49.     }  
  50.   
  51.     @Override  
  52.     public String getId() {  
  53.         return this.id;  
  54.     }  
  55.   
  56.     // 根据 key 从缓存中获取数据  
  57.     @Override  
  58.     public Object getObject(Object key) {  
  59.         String cacheKey = String.valueOf(key.hashCode());  
  60.         Object value = CACHE_SERVICE.get(cacheKey);  
  61.         if (!cacheKeys.contains(cacheKey)){  
  62.             cacheKeys.add(cacheKey);  
  63.         }  
  64.         return value;  
  65.     }  
  66.   
  67.     @Override  
  68.     public ReadWriteLock getReadWriteLock() {  
  69.         return this.readWriteLock;  
  70.     }  
  71.   
  72.     // 设置数据至缓存中  
  73.     @Override  
  74.     public void putObject(Object key, Object value) {  
  75.         String cacheKey = String.valueOf(key.hashCode());  
  76.   
  77.         if (!cacheKeys.contains(cacheKey)){  
  78.             cacheKeys.add(cacheKey);  
  79.         }  
  80.         CACHE_SERVICE.put(cacheKey, value);  
  81.     }  
  82.     // 从缓存中删除指定 key 数据  
  83.     @Override  
  84.     public Object removeObject(Object key) {  
  85.         String cacheKey = String.valueOf(key.hashCode());  
  86.   
  87.         cacheKeys.remove(cacheKey);  
  88.         return CACHE_SERVICE.delete(cacheKey);  
  89.     }  
  90.     //清空当前 Cache 实例中的所有缓存数据  
  91.     @Override  
  92.     public void clear() {  
  93.         for (int i = 0; i < cacheKeys.size(); i++){  
  94.             String cacheKey = cacheKeys.get(i);  
  95.             CACHE_SERVICE.delete(cacheKey);  
  96.         }  
  97.         cacheKeys.clear();  
  98.     }  
  99.   
  100.     @Override  
  101.     public int getSize() {  
  102.         return cacheKeys.size();  
  103.     }  
  104. }  

在  ProductMapper 中增加配置: 
Xml代码  收藏代码
  1. <cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />  

启动Memcached: 
Shell代码  收藏代码
  1. memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v  

执行Mapper 中的查询、修改等操作,Test: 
Java代码  收藏代码
  1. @Test  
  2.     public void testSelectById() {  
  3.         Long pid = 100L;  
  4.   
  5.         Product dbProduct = productMapper.selectByID(pid);  
  6.         Assert.assertNotNull(dbProduct);  
  7.   
  8.         Product cacheProduct = productMapper.selectByID(pid);  
  9.         Assert.assertNotNull(cacheProduct);  
  10.   
  11.         productMapper.updateName("IPad", pid);  
  12.   
  13.         Product product = productMapper.selectByID(pid);  
  14.         Assert.assertEquals(product.getName(), "IPad");  
  15.     }  

Memcached Loging: 
 
看上去没什么问题~ OK了。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:401728次
    • 积分:4765
    • 等级:
    • 排名:第6284名
    • 原创:103篇
    • 转载:175篇
    • 译文:0篇
    • 评论:24条
    源码网站