Java 高性能缓存设计思想(Memcache)

# package com.akala.dbcache.core;   
    
 import java.lang.reflect.Method;   
 import java.net.SocketException;   
 import java.util.ArrayList;   
 import java.util.Collections;   
 import java.util.HashMap;   
 import java.util.Iterator;   
 import java.util.List;   
 import java.util.Map;   
 import java.util.concurrent.ConcurrentHashMap;   
    
 import org.apache.log4j.Logger;   
 import org.hibernate.Criteria;   
 import org.hibernate.HibernateException;   
 import org.hibernate.Session;   
 import org.hibernate.Transaction;   
 import org.hibernate.criterion.Order;   
 import org.hibernate.criterion.Projection;   
 import org.hibernate.criterion.Projections;   
 import org.hibernate.criterion.Restrictions;   
 import org.hibernate.criterion.SimpleExpression;   
    
 /**  
  * 免责声明:本代码所有权归作者所有,在保持源代码不被破坏以及所有人署名的基础上,任何组织或个人可自由无限使用,使用者需自行承担所有风险。
*
* 舍得网是一个资源共享网站,只要一张舍得券就可以免费拿走网上的任何一件物品,注册成功后就可得到一张舍得券,成功给予别人一样物品也可以得到一张舍得券。
*

进舍得网看看


*

舍得网高效数据库缓存源代码及文档下载


* 警告:只有从上面这个地址下载才能保证代码是最新的。
*
* 该Class是数据库缓存/分布式经典解决方案,可以承受超大规模的应用,该方案简单明了,效率高,配置起来非常简单,可以直接用。^_^
* 相对于代码行数,该Class可以说是全亚洲最强大的数据库操作工具,本来想说全世界最强,想想还是谦虚点好,开个玩笑:)
* 缓存思路:
* 对于单个对象的缓存,用HashMap就可以了,稍微复杂一点用LRU算法包装一个HashMap,再复杂一点的分布式用memcached即可,没什么太难的。
* 但是对于列表缓存,似乎没有太好的通用办法,一般的思路是只取列表的id,然后根据id去对象缓存中查找,下面分析本人如何改进列表缓存
*
* 一:mysql和hibernate的底层在做通用的列表缓存时都是根据查询条件把列表结果缓存起来,但是只要该表的记录
* 有任何变化(增加/删除/修改),列表缓存要全部清除,这样只要一个表的记录经常变化(通常情况都会这样),列表缓存几乎失效,命中率太低了。
*
* 二:本人想了一个办法改善了一下列表缓存,在表的记录有改变时,遍历所有列表缓存,只有那些被影响到的列表缓存才会被删除,而不是直接清除所有
* 列表缓存。这样处理有个好处,可以缓存各种查询条件(如等于、大于、不等于、小于)的列表缓存,但也有个潜在的性能问题,由于需要遍历,系统开销
* 比较大,如果列表缓存最大长度设置成10000,两个4核的CPU每秒也只能遍历完300多次,这样如果每秒有超过300个insert/update/delete,系统就吃不消了。
*
* 三:在前面两种解决办法都不完美的情况下,本人和同事经过几个星期的思索,总算得出了根据表的某几个字段做散列的缓存办法,这种办法无需大规模遍历,
* 所以系统开销很小,由于这种列表缓存按照字段做了散列,所以命中率极高。思路如下:
*
* 每个表有3个缓存Map(key=value键值对),第一个Map是对象缓存A,在A中,key是数据库的id,Value是数据库对象(也就是一行数据);第二个Map是
* 通用列表缓存B,B的最大长度一般1000左右,在B中,key是查询条件拼出来的String(如start=0,length=15active=0state=0category=109),Value是该条件查询下
* 的所有id组成的List;第三个Map是散列缓存C,在C中,key是散列的字段(如根据userId散列的话,其中某个key就是userId=109这样的String)组成的
* String,value是一个和B类似的HashMap。其中只有B这个Map是需要遍历的,不知道说明白了没有,看完小面这个例子应该就明白了,就用论坛的回复表作说明:
* 假设回复表T中假设有字段id,topicId,postUserId等字段(topicId就是帖子的id,postUserId是发布者id)。
*
* 1:第一种情况,也是最常用的情况,就是获取一个帖子对应的回复,sql语句应该是象
* select id from T where topicId=2008 order by createTime desc limit 0,5
* select id from T where topicId=2008 order by createTime desc limit 5,5
* select id from T where topicId=2008 order by createTime desc limit 10,5
* 的样子,那么这种列表很显然用topicId做散列是最好的,把上面三个列表缓存(可以是N个)都散列到key是topicId=2008这一个Map中,当id是2008的帖子有新的
* 回复时,系统自动把key是topicId=2008的散列Map清除即可。由于这种散列不需要遍历,因此可以设置成很大,例如100000,这样10万个帖子对应的所有回复列表都可以缓存起来,
* 当有一个帖子有新的回复时,其余99999个帖子对应的回复列表都不会动,缓存的命中率极高。
*
* 2:第二种情况,就是后台需要显示最新的回复,sql语句应该是象
* select id from T order by createTime desc limit 0,50
* 的样子,这种情况不需要散列,因为后台不可能有太多人访问,常用列表也不会太多,所以直接放到通用列表缓存B中即可。
*
* 3:第三种情况,获取一个用户的回复,sql语句象
* select id from T where userId=2046 order by createTime desc limit 0,15
* select id from T where userId=2046 order by createTime desc limit 15,15
* 的样子,那么这种列表和第一种情况类似,用userId做散列即可。
*
* 4:第四种情况,获取一个用户对某个帖子的回复,sql语句象
* select id from T where topicId=2008 and userId=2046 order by createTime desc limit 0,15
* 的样子,这种情况比较少见,一般以topicId=2008为准,也放到key是topicId=2008这个散列Map里即可。
*
* * 总结:这种缓存思路可以存储大规模的列表,缓存命中率极高,因此可以承受超大规模的应用,但是需要技术人员根据自身业务逻辑来
* 配置需要做散列的字段,一般用一个表的索引键做散列(注意顺序,最散的字段放前面),假设以userId为例,可以存储N个用户的M种列表,如果某个
* 用户的相关数据发生变化,其余N-1个用户的列表缓存纹丝不动。以上说明的都是如何缓存列表,缓存长度和缓存列表思路完全一样,如缓存象
* select count(*) from T where topicId=2008
* 这样的长度,也是放到topicId=2008这个散列Map中。如果再配合好使用mysql的内存表和memcached,加上F5设备做分布式负载均衡,该系统 * 对付像1000万IP/天这种规模级的应用都足够了,除搜索引擎外一般的应用网站到不了这种规模。
* 任何问题请发送mail到[email protected] * * @author [email protected] * @see 舍得网 * */ public class BaseManager { /** * 记录日志的logger,log4j配置文件必须配置一个名为operlogger的logger */ private static Logger logger = LoggerUtil.getLogger("operlogger"); /** * 用于存放所有该manager管理om的对象缓存 key=value,key都是Long对象,value是BaseRecord对象 */ private LRUMap RECORDS_CACHE = new LRUMap(Config.getInstance().getMaxOmCacheLength()); /** * 公用列表的cache,key=value,key是字符串,value是id组成的List。需要遍历,不能设置太大! */ private LRUMap RECORDS_LIST_CACHE = new LRUMap(Config.getInstance().getMaxListCacheLength()); /** * 用于存放recordset长度的cache,key=value,value存放Integer对象 */ private LRUMap RECORDS_LENGTH_CACHE = new LRUMap(Config.getInstance().getMaxLengthCacheLength()) ; /** * 二级缓存,根据applicationContext.xml指定的字段来做二级缓存,只能支持等于查询,不需要遍历,可以设置大一点。
* 这里的key是包含hashFieldsList里字段的string,value是一个java.util.HashMap!!!
*/ private LRUMap HASH_LIST_CACHE = new LRUMap(Config.getInstance().getMaxSecondaryCacheLength()) ; /** * 二级长度缓存,根据applicationContext.xml指定的字段来做二级缓存,只能支持等于查询,不需要遍历,可以设置大一点。
* 这里的key是包含hashFieldsList里字段的string,value是一个java.util.HashMap!!!
*/ private LRUMap HASH_LENGTH_CACHE = new LRUMap(Config.getInstance().getMaxSecondaryCacheLength()) ; /** * 缓冲update对象,对于帖子的点击次数的update可以用这个实现。 */ private static Map UPDATE_DB_MAP = new ConcurrentHashMap(); /** * 该manager管理的是哪个class */ private Class recordClass = null ; /** * hibernate 配置文件,可以实现数据库按表拆分的分布式 */ private String hibernateConfigFile = null ; /** * applicationContext.xml里配置的二级散列缓存字段,如果有多个用;隔开。一般用userId做散列,2个字段散列足够了! */ private String hashFieldsList = null ; /** * 二级散列缓存的字段列表 */ private String[] hashFields = null ; /** * 得到需要散列的字段数组 * @return */ private String[] getHashFields(){ if(hashFieldsList==null){ return null; }else{ if(hashFields==null){ hashFields = hashFieldsList.split(";"); } return hashFields; } } /** * 删除所有缓存!!!!!!!!!!!
* 删除所有缓存,尽供后台调用,前台页面别瞎用!
*/ public void clearAllCache(){ RECORDS_CACHE.clear(); RECORDS_LIST_CACHE.clear(); RECORDS_LENGTH_CACHE.clear(); HASH_LIST_CACHE.clear(); HASH_LENGTH_CACHE.clear(); } /** * 增加一个ShutDown钩子,在应用服务器tomcat/resin停止的时候把该需要update的数据更新了,免得漏掉UPDATE_DB_MAP里需要update的对象 */ static { Runtime.getRuntime().addShutdownHook(new UpdateThread(UPDATE_DB_MAP)); } /** * 负责定时update数据库的线程 */ private static UpdateThread updateThread = null ; /** * 负责接收报文的线程,用于接收别的服务器删除缓存的通知 */ private static UdpReceiverThread udpReceiverThread = null ; static{ //启动定时更新数据库的线程 if(updateThread==null){ updateThread = new UpdateThread(UPDATE_DB_MAP); updateThread.start(); } if(Config.getInstance().useDistributedDbCache()){ //启动接受报文的线程 if(udpReceiverThread==null){ try { udpReceiverThread = new UdpReceiverThread(); udpReceiverThread.start(); } catch (SocketException e) { e.printStackTrace(); } } } } /** * 根据id获取记录。第一步从本机内存中取,如果没有则转向memcached server获取,
* 如果memcached server也没有才从数据库中获取,这样可以大大减轻数据库服务器的压力。
* @param id 记录的id * @return BaseRecord对象 */ public BaseRecord findById(long id){ //第一步:在本地缓存中查找 if(RECORDS_CACHE.containsKey(id)){ return (BaseRecord)RECORDS_CACHE.get(id); } BaseRecord br = null; //第二步:去memcached server中查找 if(Config.getInstance().useMemCached()){ br = this.getFromMemCachedServer(recordClass.getName()+""+id); if(br!=null){ RECORDS_CACHE.put(id, br); return br; } } //第三步:读取数据库 Session s = HibernateUtil.currentSession(hibernateConfigFile); try { Transaction tx= s.beginTransaction(); br = (BaseRecord) s.get(recordClass, id); tx.commit(); }catch(HibernateException he){ he.printStackTrace(); }finally{ HibernateUtil.closeSession(hibernateConfigFile); } //第四步:放入本地缓存 RECORDS_CACHE.put(id, br); //第五步:放入memcached缓存中 set2MemCachedServer(br); return br; } /** * 根据id获取记录。第一步从本机内存中取,如果没有则转向memcached server获取,
* 如果memcached server也没有才从数据库中获取,这样可以大大减轻数据库服务器的压力。
* @param id 记录的id * @return BaseRecord 对象 */ public BaseRecord findById(String id){ if(!StringUtils.isDigits(id))return null; return findById(Long.parseLong(id)); } /** * 根据某一个字段的值来获取对象 * @param fieldName 字段名 * @param value 字段值 * @return BaseRecord 对象 */ public BaseRecord findByProperty(String fieldName,Object value){ ArrayList list = new ArrayList(); list.add(Restrictions.eq(fieldName,value)); List list2 = this.getList(list, null, 0, 1); if(list2==null || list2.size()==0) return null; return (BaseRecord)list2.get(0); } /** * 根据ID从数据库中删除数据,如果有必要,可以重写该方法删除缓存中的纪录和列表中的list缓存! * @param id long * @return boolean */ public boolean deleteById(long id){ BaseRecord br = this.findById(id); return this.delete(br); } /** * 根据ID从数据库中删除数据,如果有必要,可以重写该方法删除缓存中的纪录和列表中的list缓存! * @param id String * @return boolean */ public boolean deleteById(String id){ if(!StringUtils.isDigits(id))return false; return this.deleteById(Long.parseLong(id)); } /** * 从数据库中删除数据,如果有必要,可以重写该方法删除缓存中的纪录和列表中的list缓存! * @param br BaseRecord * @return boolean */ public boolean delete(BaseRecord br){ long id = br.getId(); Session s = HibernateUtil.currentSession(this.hibernateConfigFile); try { Transaction tx = s.beginTransaction(); s.delete(br); tx.commit(); if(Config.getInstance().isDbLog()){ logger.info("删除数据库对象:"+br); } } catch (HibernateException e){ e.printStackTrace(); return false; } finally { HibernateUtil.closeSession(hibernateConfigFile); } //删除memcached缓存 if(Config.getInstance().useMemCached()){ if(MemcachedUtil.getMemCachedClient()!=null){ boolean ret = MemcachedUtil.getMemCachedClient().delete(this.recordClass+""+id); } } //删除列表缓存 removeListCache(br,true); //分布式删除对象缓存 removeFromCache(id,true,true); return true; } /** * 更新一个数据库对象。如修改一个用户昵称时,不会影响任何排序,那么就不需要清除列表缓存。 * @param record 要更新的对象 * @param clearListCache true表示需要清除列表缓存 false表示不需要 * @return boolean */ public boolean update(BaseRecord record,boolean clearListCache){ Session s = HibernateUtil.currentSession(this.hibernateConfigFile); try { Transaction ts = s.beginTransaction(); s.update(record); ts.commit(); //重新设置memcached server的缓存 set2MemCachedServer(record); //分布式删除对象缓存 removeFromCache(record.getId(),false,true); //删除列表缓存 if(clearListCache){ removeListCache(record,true); } if(Config.getInstance().isDbLog()){ logger.info("修改数据库对象:"+record); } } catch (HibernateException e) { e.printStackTrace(); return false; } finally { HibernateUtil.closeSession(this.hibernateConfigFile); } return true ; } /** * 创建一个数据库记录,并把对象放入本机缓存和memcached缓存。 * @param record BaseRecord * @return BaseRecord 返回数据库中的对象, */ public BaseRecord create(BaseRecord record){ Session s = HibernateUtil.currentSession(this.hibernateConfigFile); try{ Transaction tx= s.beginTransaction(); s.save(record); tx.commit(); if(Config.getInstance().isDbLog()){ logger.info("创建数据库对象:"+record); } //放入memcached缓存中 set2MemCachedServer(record); //刚创建的纪录放入缓存中 RECORDS_CACHE.put(record.getId(),record); //删除列表缓存 removeListCache(record,true); } catch (HibernateException e){ e.printStackTrace(); return null; } finally { HibernateUtil.closeSession(hibernateConfigFile); } return record; } /** * 每个表必须有id这个字段,每个类必须有id这个field。自定义条件查询列表,理论上这个方法
* 可以满足所有需求,特别注意缓存key的拼法!!!!!
* 在memcached缓存上存的则不是List而是由分开的id列表,如:131425256887987
* key是象s10l20,createTime desc$age<90aget>80pid=12343这样的字符串
* @param expList 查询条件 * @param orders 排序 * @param start 开始位置 * @param length 获取长度 * @return List 数据库记录 */ public List getList(List expList,List orders,int start,int length){ List fList = new ArrayList();//field set if(expList!=null){ for(int i=0;i0){ for(int j=0;j oList = new ArrayList(); for(int i=0;i oList = new ArrayList(); for(int i=0;i0){ for(int i=0;i0){ for(int i=0;i oList = new ArrayList(); for(int i=0;i expList){ List fList = new ArrayList(); if(expList!=null){ for(int i=0;i0){ for(int j=0;j0){ for(int i=0;i * 遍历该List方法
* Iterator iterator = list.iterator();
* while(iterator.hasNext()) {
* Object[] o = (Object[]) iterator.next();
* //...
* }
* @param expList * @param orders * @param project 包含sum count group等复杂组合查询条件的Projection(s) * @param start * @param length * @return List */ public List getProjectionList(List expList,List orders,Projection project,int start,int length){ List fList = new ArrayList();//field set if(expList!=null){ for(int i=0;i0){ for(int i=0;i0){ for(int i=0;i expList,Projection project){ List fList = new ArrayList();//field set if(expList!=null){ for(int i=0;i0){ for(int i=0;i * 利用缓存更新数据库,在压力特别大的时候用,比如在更新帖子的点击次数,这种情况没必要立即更新而且更新频繁所以采用缓存
* 这种情况一般不更新缓存,因为一般这种点击次数的修改不会影响排列次序,如果做影响排列顺序的修改(如优先级)则
* 必须用update()方法!
* @param record 需要update的对象 * @return boolean */ public boolean putToUpdateMap(BaseRecord record){ String remoteKey = "update2:"+this.recordClass.getName()+""+record.getId(); //记录需要更新的远程对象 UPDATE_DB_MAP.put(remoteKey,this); //分布式... if(Config.getInstance().useDistributedDbCache()){ //重新设置远程服务器的key if(Config.getInstance().useMemCached()){ if(MemcachedUtil.getMemCachedClient()!=null){ boolean ret = MemcachedUtil.getMemCachedClient().set(remoteKey, record,new java.util.Date(Config.getInstance().getMemCachedExpire())); removeFromCache(record.getId(),false,true); } } } return true; } /** * 分布式从缓存中去掉对象,下次读取就会从memcached读取或者从数据库读取。在jsp或其他地方调用isLocal一律用true。 * @param id the id * @param realRemove 是否真的删除,当删除数据库时调用 * @param isLocal 是否是本地调用 */ public void removeFromCache(long id,boolean realRemove,boolean isLocal){ if(isLocal){ //分布式清除缓存,发UDP报文,通知其他服务器删除缓存 if(Config.getInstance().useDistributedDbCache()){ String s = this.recordClass.getName()+"removeFromCache"+id; UdpSenderUtil.getInstance().sendAll(s); } } if(realRemove||!isLocal){ RECORDS_CACHE.remove(id); } } /** * 判断一个条件是否和一个Object条件匹配。支持等于、不等于、大于、小于、大于或等于、小于或等于的缓存。主要还是等于条件的查询比较多!!
* 暂时不支持like or and的查询缓存,如果有这个需要,想其他办法吧。!
* @param fieldMap 一个对象所有field的值。 * @param c 条件字符串。如userId=4 and state=0 * @return */ private boolean isSqlRestrictionInFieldMap(HashMap fieldMap,String c){ boolean isMatched = true ; int loc = 0 ; if((loc=c.indexOf("="))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+1); String fieldValue2 = fieldMap.get(fieldName); if(!fieldValue2.equals(fieldValue)){ isMatched = false ; } }else if((loc=c.indexOf("<>"))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+2); String fieldValue2 = fieldMap.get(fieldName); if(fieldValue2.equals(fieldValue)){ isMatched = false ; } }else if((loc=c.indexOf(">="))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+2); String fieldValue2 = fieldMap.get(fieldName); if(StringUtils.isDigits(fieldValue)&&StringUtils.isDigits(fieldValue2)){ long fv = Long.parseLong(fieldValue); long fv2 = Long.parseLong(fieldValue2); if(!(fv2>=fv)){ isMatched = false ; } } }else if((loc=c.indexOf("="))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+2); String fieldValue2 = fieldMap.get(fieldName); if(StringUtils.isDigits(fieldValue)&&StringUtils.isDigits(fieldValue2)){ long fv = Long.parseLong(fieldValue); long fv2 = Long.parseLong(fieldValue2); if(!(fv2=fv)){ isMatched = false ; } } }else if((loc=c.indexOf("<"))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+1); String fieldValue2 = fieldMap.get(fieldName); if(StringUtils.isDigits(fieldValue)&&StringUtils.isDigits(fieldValue2)){ long fv = Long.parseLong(fieldValue); long fv2 = Long.parseLong(fieldValue2); if(!(fv2"))>-1){ String fieldName = c.substring(0,loc); String fieldValue = c.substring(loc+1); String fieldValue2 = fieldMap.get(fieldName); if(StringUtils.isDigits(fieldValue)&&StringUtils.isDigits(fieldValue2)){ long fv = Long.parseLong(fieldValue); long fv2 = Long.parseLong(fieldValue2); if(!(fv2>fv)){ isMatched = false ; } } } return isMatched ; } /** * 本地(localhost)调用,在jsp中调用isLocal一律用true。
* 自动删除列表缓存,列表缓存的key必须是由字段名称=字段值组成,如boardId=1threadId=3state=1
* 所以删除时只要利用要删除的对象的字段值组成一个条件字符串,再看key中的条件是否满足这些条件就可以
* 决定是否要删除这些缓存List
* 这是一个比较好的自动删除缓存的办法
* @param bt BaseRecord对象,如User * @param isLocal 是否本地调用 */ public void removeListCache(BaseRecord bt,boolean isLocal){ if(isLocal){ //分布式清除缓存 if(Config.getInstance().useDistributedDbCache()){ String s123 = this.recordClass.getName()+"removeFromListCache"+bt.getId(); UdpSenderUtil.getInstance().sendAll(s123); } } HashMap fieldMap = new HashMap(); //把对象的域值转换到一个hashmap里 Object[] args = null ; Method[] ms = bt.getClass().getMethods(); String[] secondaryCacheFields = this.getHashFields(); try{ for(int i=0;i0){ for(int j=0;j * 如果修改了字段只影响一个排序,如修改了帖子的更新时间,那么修改之前不要调用该方法,直接用update(BaseRecord,true)即可。
* 如果修改的字段不影响排序,典型的像修改了用户的昵称,那么用update(BaseRecord,false)即可,即不需要清除列表缓存。
* @param bt */ public void removeListCache(BaseRecord bt){ removeListCache(bt,true); } /** * 分割字符串到数组,注意,缓存字符$之前的串不需要参与计算。每个缓存列表的key都包含$字符!!!
* 自己写的一个按照特定分界符分解字符串,比String的split方法快很多。
* @param s key * @param delimiter 分界符号 * @return */ private String[] splitString(String s,String delimiter){ s = s.substring(s.indexOf("{1}quot;)+1); ArrayList l = new ArrayList(); int index = s.indexOf(delimiter); while(index>-1){ String s0 = s.substring(0,index); if(s0.length()>0)l.add(s0); s = s.substring(index+1); index = s.indexOf(delimiter); } if(s.length()>0)l.add(s); return (String[])l.toArray(new String[0]); } /** * 自动删除列表缓存,列表缓存的key必须是由字段名称=字段值组成,如boardId=1threadId=3state=1
* 所以删除时只要利用要删除的对象的字段值组成一个条件字符串,再看key中的条件是否满足这些条件就可以
* 决定是否要删除这些缓存List
* 这是一个比较好的自动删除缓存的办法
* 暂时支持like语句,大于(>)语句,小于(<)语句,等于(=)语句的自动清除
* 已经实现了分布式清除缓存的功能!!!!!!!!!!!!! * @param bt BaseRecord对象,如User * @param rMap 缓存 */ private void removeListCache2(BaseRecord bt,LRUMap rMap,HashMap fieldMap){ try{ //遍历删除公用缓存 Iterator it = rMap.keySet().iterator(); if(it==null)return; String k = null ; String[] ks = null ; while(it.hasNext()){ k = (String)it.next(); boolean isMatched = true ; ks = splitString(k,""); if(ks!=null&&ks.length>0){ for(int i=0;i


那么最后的缓存结构应该是下面这个样子:

 

缓存A是:

Key键(long型)

Value值(类型T)

11

Id=11的T对象

22

Id=22的T对象

133

Id=133的T对象

……

 

列表缓存B是:

Key键(String型)

Value值(ArrayList型)

from T order by createTime desc limit 0,50

ArrayList,对应取出来的所有id

from T order by createTime desc limit 50,50

ArrayList,对应取出来的所有id

from T order by createTime desc limit 100,50

ArrayList,对应取出来的所有id

……

 

散列缓存C是:

Key键(String型)

Value值(HashMap)

userId=2046

Key键(String型)

Value值(ArrayList)

userId=2046#0,5

id组成的List

userId=2046#5,5

id组成的List

userId=2046#15,5

id组成的List

……

userId=2047

Key键(String型)

Value值(ArrayList)

userId=2047#0,5

id组成的List

userId=2047#5,5

id组成的List

userId=2047#15,5

id组成的List

……

userId=2048

Key键(String型)

Value值(ArrayList)

userId=2048#topicId=2008#0,5

id组成的List

userId=2048#5,5

id组成的List

userId=2048#15,5

id组成的List

……



JVM参数调优是一个很头痛的问题,可能和应用有关系,下面是本人一些调优的实践经验,希望对读者能有帮助,环境LinuxAS4,resin2.1.17,JDK6.0,2CPU,4G内存,dell2950服务器,网站是http://shedewang.com

一:串行垃圾回收,也就是默认配置,完成10万request用时153秒,JVM参数配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log-XX:+PrintGCDetails -XX:+PrintGCTimeStamps ";
这种配置一般在resin启动24小时内似乎没有大问题,网站可以正常访问,但查看日志发现,在接近24小时时,Full GC执行越来越频繁,大约每隔3分钟就有一次Full GC,每次Full GC系统会停顿6秒左右,作为一个网站来说,用户等待6秒恐怕太长了,所以这种方式有待改善。MaxTenuringThreshold=7表示一个对象如果在救助空间移动7次还没有被回收就放入年老代,GCTimeRatio=19表示java可以用5%的时间来做垃圾回收,1/(1+19)=1 /20=5%。

二:并行回收,完成10万request用时117秒,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xmx2048M -Xms2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -Xnoclassgc-Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC-XX:MaxGCPauseMillis=500 -XX:+UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=7-XX:GCTimeRatio=19 ";
并行回收我尝试过多种组合配置,似乎都没什么用,resin启动3小时左右就会停顿,时间超过10 秒。也有可能是参数设置不够好的原因,MaxGCPauseMillis表示GC最大停顿时间,在resin刚启动还没有执行Full GC时系统是正常的,但一旦执行Full GC,MaxGCPauseMillis根本没有用,停顿时间可能超过20秒,之后会发生什么我也不再关心了,赶紧重启resin,尝试其他回收策略。

三:并发回收,完成10万request用时60秒,比并行回收差不多快一倍,是默认回收策略性能的2.5倍,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19-Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 ";
这个配置虽然不会出现10秒连不上的情况,但系统重启3个小时左右,每隔几分钟就会有5秒连不上的情况,查看gc.log,发现在执行ParNewGC时有个promotion failed错误,从而转向执行Full GC,造成系统停顿,而且会很频繁,每隔几分钟就有一次,所以还得改善。UseCMSCompactAtFullCollection是表是执行Full GC后对内存进行整理压缩,免得产生内存碎片,CMSFullGCsBeforeCompaction=N表示执行N次Full GC后执行内存压缩。

四:增量回收,完成10万request用时171秒,太慢了,配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xincgc ";
似乎回收得也不太干净,而且也对性能有较大影响,不值得试。

五:并发回收的I-CMS模式,和增量回收差不多,完成10万request用时170秒。
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC-XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing-XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=10-XX:-TraceClassUnloading ";
采用了sun推荐的参数,回收效果不好,照样有停顿,数小时之内就会频繁出现停顿,什么sun推荐的参数,照样不好使。

六:递增式低暂停收集器,还叫什么火车式回收,不知道属于哪个系,完成10万request用时153秒
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseTrainGC ";
该配置效果也不好,影响性能,所以没试。

七:相比之下,还是并发回收比较好,性能比较高,只要能解决ParNewGC(并行回收年轻代)时的promotionfailed错误就一切好办了,查了很多文章,发现引起promotion failed错误的原因是CMS来不及回收(CMS默认在年老代占到90%左右才会执行),年老代又没有足够的空间供GC把一些活的对象从年轻代移到年老代,所以执行Full GC。CMSInitiatingOccupancyFraction=70表示年老代占到约70%时就开始执行CMS,这样就不会出现Full GC了。SoftRefLRUPolicyMSPerMB这个参数也是我认为比较有用的,官方解释是softlyreachable objects will remain alive for some amount of time after the last timethey were referenced. The default value is one second of lifetime per freemegabyte in the heap,我觉得没必要等1秒,所以设置成0。配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server-Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc-XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC-XX:+CMSPermGenSweepingEnabled -XX:+UseCMSCompactAtFullCollection-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled-XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=70-XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails-XX:+PrintGCTimeStamps -XX:+PrintGCApplicationConcurrentTime-XX:+PrintGCApplicationStoppedTime -Xloggc:log/gc.log ";
上面这个配置内存上升的很慢,24小时之内几乎没有停顿现象,最长的只停滞了0.8s,ParNew GC每30秒左右才执行一次,每次回收约0.2秒,看来问题应该暂时解决了。

参数不明白的可以上网查,本人认为比较重要的几个参数是:-Xms -Xmx -Xmn MaxTenuringThreshold GCTimeRatio UseConcMarkSweepGCCMSInitiatingOccupancyFraction SoftRefLRUPolicyMSPerMB

 

Memcached是什么?
Memcached是高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度。
Memcached由Danga Interactive开发,用于提升LiveJournal.com访问速度的。LJ每秒动态页面访问量几千次,用户700万。Memcached将数据库负载大幅度降低,更好的分配资源,更快速访问。

如何使用memcached-Server端?
在服务端运行:
# ./memcached -d -m 2048 -l 10.0.0.40 -p 11211
这将会启动一个占用2G内存的进程,并打开11211端口用于接收请求。由于32位系统只能处理4G内存的寻址,所以在大于4G内存使用PAE的32位服务器上可以运行2-3个进程,并在不同端口进行监听。

如何使用memcached-Client端?(Java版是这样的,参看http://www.whalin.com/memcached/#download)
String serverStr = "218.241.154.12:12321";
String[] serverlist = {serverStr};
//String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
//Integer[] weights = { new Integer(5), new Integer(2) };
int initialConnections = 100;
int minSpareConnections = 50;
int maxSpareConnections = 500;
long maxIdleTime = 1000 * 60 * 30; // 30 minutes
long maxBusyTime = 1000 * 60 * 5; // 5 minutes
long maintThreadSleep = 1000 * 5; // 5 seconds
int socketTimeOut = 1000 * 3; // 3 seconds to block on reads
//int socketConnectTO = 1000 * 3; // 3 seconds to block on initial connections. If 0, then will use blocking connect (default)
//boolean failover = false; // turn off auto-failover in event of server down
boolean nagleAlg = false; // turn off Nagle's algorithm on all sockets in pool
//boolean aliveCheck = false; // disable health check of socket on checkout

pool = SockIOPool.getInstance("mymemcache");
pool.setServers( serverlist );
//pool.setWeights( weights );
pool.setInitConn( initialConnections );
pool.setMinConn( minSpareConnections );
pool.setMaxConn( maxSpareConnections );
pool.setMaxIdle( maxIdleTime );
pool.setMaxBusyTime( maxBusyTime );
pool.setMaintSleep( maintThreadSleep );
pool.setSocketTO( socketTimeOut );
pool.setNagle( nagleAlg );
pool.setHashingAlg( SockIOPool.NEW_COMPAT_HASH );
pool.setAliveCheck( true );
pool.initialize();

mCachedClient = new MemCachedClient( "mymemcache" );
mCachedClient.setCompressEnable( false );
mCachedClient.setCompressThreshold(4096);
然后用mCachedClient的set/get/delete方法就可以了。memcached的吞吐量每秒大概能get两万次左右,这比mysql的select提高了好几倍,所以很多网站都用这个来做缓存,如豆瓣。

我在系统种也用了memcached,我的建议是在有分布式的时候才考虑用memcached,如果只有一台应用服务器就没有必要用memcached,毕竟Memcached的吞吐量还是有限,本地HashMap每秒可以get一百万次,用本地HashMap做缓存才是最快的。用了分布式后,缓存同步和分布式session都是比较难处理的问题,所以建议1000万pv/天以下的应用不要用分布式。

 



你可能感兴趣的:(Memcache,Cache)