最近在研究memcached,有兴趣的朋友,欢迎围观我的memcached演练系列。

memcache-client-forjava 是一款memcached高可用解决方案,由一位阿里的一个牛人开源的一个框架。项目源码发布在google.code。需要×××下载。如果不能下载,留下邮箱。

周所周知,memcache是一个分布式数据缓存组件,节点的一致性和高可用均需要在客户端实现。

这个项目已经很长时间没有更新了,最新版是2.5.1。但项目不庞大,是研究源码,提高设计能力的一个好的选择。

主要内容

1.为什么要分析缓存源码

2.将memcache-client-forjava demo maven化

3.分析DefaultCacheImpl作为起点,进行分析

1.为什么要分析缓存源码

就不说虚的啦,互联网大环境等等。如果你在各大招聘网站,刷过招聘JD,应该对一些招聘要求要求有很深的印象。

**最少熟悉一款nosql数据库,如redis,memcached等

**要求具备数据库调优,web服务调优经验...

可见缓存是我们不得不掌握的一项技能了。而且我有过一次面试,直接让“如何设计一款缓存服务”。现在想想,回答不太理想,这也是我对缓存这一块耿耿于怀的一个诱因。

分享些关于缓存的若干资源,大家有兴趣补补。

按一个正常访问请求来说,缓存发生地点:浏览器缓存,代理缓存(CDN缓存),web缓存(本地缓存),web分布式缓存,数据库缓存,操作系统缓存,CPU缓存。

缓存的意义:适配CPU和磁盘随机访问的速度差异,重用计算结果,降低重复计算等。

评价缓存的一些指标:命中率,过期数据清除算法等。

2.将memcache-client-forjava demo maven化

个人由于比较讨厌维护jar之间的依赖,比较喜欢使用maven管理之。

2.1 先从https://code.google.com/archive/p/memcache-client-forjava/downloads下载

2.2 由于memcache-client-forjava 并没有提交到中央仓库,所以需要我们手动上传至本地仓库。

mvn install:install-file -DgroupId=com.alisoft -DartifactId=alisoft-xplatform-asf-cache -Dversion=2.5.1 -Dpackaging=jar -Dfile=C:\Users\zhaoguoyu\Downloads\alisoft-xplatform-asf-cache-2.5.1.jarmvn install:install-file -DgroupId=com.alisoft -DartifactId=alisoft-xplatform-asf-cache -Dversion=2.5.1 -Dpackaging=jar -Dfile=C:\Users\zhaoguoyu\Downloads\alisoft-xplatform-asf-cache-2.5.1-src.jar -DgeneratePom=true -Dclassifier=sources

2.3 pom.xml

    
4.0.0
    
com.alisoft.examples
    
asftest
    
1.0-SNAPSHOT
    
jar
    
asftest
    
http://maven.apache.org
    
        
UTF-8
    
    
        
            
com.alisoft
            
alisoft-xplatform-asf-cache
            
2.5.1
        
        
            
log4j
            
log4j
            
1.2.17
        
        
            
commons-logging
            
commons-logging
            
1.2
        
        
            
org.codehaus.woodstox
            
wstx-asl
            
3.2.1
        
        
            
stax
            
stax-api
            
1.0.1
        
        
            
com.caucho
            
hessian
            
4.0.7
        
        
            
junit
            
junit
            
4.11
            
test
        
    

2.4 复制cache-demo-2.5代码到test目录

2.5 最后的项目结构

3.分析DefaultCacheImpl

3.1 DefaultCacheImpl类图

ICache
: Cache统一接口,支持泛型。具有常用的数据的增删改查接口。 DefaultCacheImpl:实现了ICache接口,是默认的本地Cache的实现,线程安全。 CheckOutOfDateSchedule:过期数据检查任务,定期清除过期数据。 接下来,重点分析下DefaultCacheImpl具体实现

3.2 主要属性

public class DefaultCacheImpl implements ICache
{/** * 具体内容存放的地方 */ConcurrentHashMap
[] caches;/** * 超期信息存储 */ConcurrentHashMap
 expiryCache;/** * 清理超期内容的服务 */private  ScheduledExecutorService scheduleService;/** * 清理超期信息的时间间隔,默认10分钟 */private int expiryInterval = 10;/** * 内部cache的个数,根据key的hash对module取模来定位到具体的某一个内部的Map, * 减小阻塞情况发生。 */private int moduleSize = 10;...}
  1. 维护了2个ConcurrentHashMap,分别存储数据有效信息和实际数据,而ConcurrentHashMap本身是线程安全的。

  2. scheduleService驱动CheckOutOfDateSchedule任务执行

  3. 考虑热快情况,加入了取模逻辑,降低数据争用。

3.3 初始化

public DefaultCacheImpl(){   init();}public DefaultCacheImpl(int expiryInterval,int moduleSize){   this.expiryInterval = expiryInterval;   this.moduleSize = moduleSize;   init();}@SuppressWarnings("unchecked")private void init(){   caches = new ConcurrentHashMap[moduleSize];   //初始化数据存放桶   for(int i = 0 ; i < moduleSize ;i ++)      caches[i] = new ConcurrentHashMap
();      expiryCache = new ConcurrentHashMap
();      scheduleService = Executors.newScheduledThreadPool(1);   //启动定时任务   scheduleService.scheduleAtFixedRate(new CheckOutOfDateSchedule(caches,expiryCache),          0, expiryInterval * 60, TimeUnit.SECONDS);      if (Logger.isInfoEnabled())      Logger.info("DefaultCache CheckService is start!");}

3.4 数据的存取

...public boolean containsKey(String key){   checkValidate(key);   return getCache(key).containsKey(key);}public Object get(String key){   checkValidate(key);   return getCache(key).get(key);}public Object put(String key, Object value){   Object result = getCache(key).put(key, value);   expiryCache.put(key,(long)-1);      return result;}public Object put(String key, Object value, Date expiry){   Object result = getCache(key).put(key, value);   expiryCache.put(key,expiry.getTime());      return result;}private ConcurrentHashMap
getCache(String key){   long hashCode = (long)key.hashCode();      if (hashCode < 0)      hashCode = -hashCode;      int moudleNum = (int)hashCode % moduleSize;      return caches[moudleNum];}/**检查key是否过期,过期则删除*/private void checkValidate(String key){   if (expiryCache.get(key) != null && expiryCache.get(key) != -1          && new Date(expiryCache.get(key)).before(new Date()))   {      getCache(key).remove(key);      expiryCache.remove(key);   }}...

数据的存储:首先根据key 的hashCode选择合适的数据桶,还需要在expiryCache中记录过期信息。

数据的读取:首先根据key expiryCache记录,判断是否过期,过期则删除。

逻辑非常简单,但是这各默认实现,没有考虑数据回收情况。当缓存大批量数据,容易出现内存溢出。

3.5 缓存溢出再现

public class DefaultCacheImplTest{ICache
 cache;@Beforepublic void setUpBeforeClass() throws Exception{   cache = new DefaultCacheImpl();}@Afterpublic void tearDownAfterClass() throws Exception{   cache.clear();}@Testpublic void testGet(){   int i=0;       while(true){           i++;           cache.put("key1"+i, "value"+i);           System.out.println(""+i);;       }// Assert.assertEquals("value1", cache.get("key1"));}  ...}

为了容易再现问题,加入“-Xmx5m -Xms5m”

经过测试接近30000时,出现java.lang.OutOfMemoryError: Java heap space。

个人认为一个缓存必须要具备以下组件

  1. 命中率统计

  2. 老数据回收

  3. 数据管理

  4. 过期数据清除

而DefaultCacheImpl 简单实现了3 和4.

但不可否认,它为我们研究缓存提供了入门参考。