缓存就是利剑

来源:互联网 发布:html嵌套php if语句 编辑:程序博客网 时间:2024/04/29 14:38

说是支持1亿pv/天,也许有点夸张,也是为了吸引您能点进来,如果您能认真看完相信也不会让您失望,当然,肯定有很多“高手”会对此会嗤之以鼻,没关系,有很多眼高手低的人总喜欢评论别人却从不会看清自己。

 

如果大家真想支持我、支持中国人开源项目,请把该文贴到自己的博客中或者收藏本文,记得包含文档的下载地址!!!!!!!谢谢。

 

我说的系统主要是构建在hibernate之上的高效数据库缓存系统,其中包含了分布式解决方案,该系统已经应用在舍得网上了,没有发现大问题,本人也相信该系统已经足够强大,应付数百万IP/天的应用都不是问题,我这么说肯定有人会对此表示怀疑,其实系统到底能撑多少IP/天不在于系统本身而是在于使用该系统的人。

 

代码看上去很简单,其实却是两年经验的总结,整过过程也遇到了很多难点,最后一一解决了,所以也请各位珍惜他人的劳动成果。本系统非常简洁易用,主程序BaseManager.java不到1000行代码,用“精悍”来形容绝对不为过,1000行代码却包含了数据库对象的缓存、列表和长度的缓存、按字段散列缓存、update延时更新、自动清除列表缓存等功能,用它来实现像论坛、博客、校友录、交友社区等绝大部分应用网站都足够了。

 

我在理想状态下做了压力测试,在没有数据库操作的jsp页面(舍得网新首页)里可以完成2000requests每秒(正常情况可能有1/1000request有数据库查询,其余999/1000都是直接从缓存里读取),物品详情页每秒可完成3000requests,纯静态html页面也只能完成7000requests/秒,我对首页进行了三个小时的压力测试,完成了24850800requestsjava一点事都没有,内存没有上涨。按照2000requests/秒算,一天按15小时计算,那么每天能完成3600*15*2000=1亿零8百万requests,当然这是理想状态,实际状态就算打一折,还能完成1000pv/天,要知道,这只是一个普通13千块钱买的服务器,内存4GCPU2个,LinuxAS4系统,apache2.0.63/resin2.1.17/jdk6.0的环境。

 

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。现在进入正题。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

 

为什么要用缓存?如果问这个问题说明你还是新手,数据库吞吐量毕竟有限,每秒读写5000次了不起了,如果不用缓存,假设一个页面有100个数据库操作,50个用户并发数据库就歇菜,这样最多能支撑的pv也就50*3600*15=270万,而且数据库服务器累得半死,搞不好什么时候就累死了。我的这套缓存系统比单独用memcached做缓存还要强大,相当于在memcached上再做了两级缓存,大家都知道memcached很强了,但是吞吐量还是有限,每秒20000getput当遇到超大规模的应用时还是会歇菜,本地HashMap每秒可执行上百万次putget,在这上面损耗的性能几乎可以忽略不记了。温馨提示:能不用分布式的时候就不要用分布式,非用分布式的时候再考虑用memcached,我的缓存系统在这方面都已经实现了,改个配置就可以了,有兴趣的可以仔细测试测试!

 

一般数据库缓存在我看来包含四种。第一种:单个对象的缓存(一个对象就是数据库一行记录),对于单个对象的缓存,用HashMap就可以了,稍微复杂一点用LRU算法包装一个HashMap,再复杂一点的分布式用memcached即可,没什么太难的;第二种:列表缓存,就像论坛里帖子的列表;第三种:长度的缓存,比如一个论坛板块里有多少个帖子,这样才方便实现分页。第四种:复杂一点的groupsumcount查询,比如一个论坛里按点击数排名的最HOT的帖子列表。第一种比较好实现,后面三种比较困难,似乎没有通用的解决办法,我暂时以列表缓存(第二种)为例分析。

 

mysqlhibernate的底层在做通用的列表缓存时都是根据查询条件把列表结果缓存起来,但是只要该表的记录有任何变化(增加/删除/修改),列表缓存要全部清除,这样只要一个表的记录经常变化(通常情况都会这样),列表缓存几乎失效,命中率太低了。

 

本人想了一个办法改善了列表缓存,当表的记录有改变时,遍历所有列表缓存,只有那些被影响到的列表缓存才会被删除,而不是直接清除所有列表缓存,比如在一个论坛版(id=1)里增加了一个帖子,那么只要清除id=1这个版对应的列表缓存就可以了,版id=2就不用清除了。这样处理有个好处,可以缓存各种查询条件(如等于、大于、不等于、小于)的列表缓存,但也有个潜在的性能问题,由于需要遍历,CPU符合比较大,如果列表缓存最大长度设置成10000,两个4核的CPU每秒也只能遍历完300多次,这样如果每秒有超过300insert/update/delete,系统就吃不消了。

 

在前面两种解决办法都不完美的情况下,本人和同事经过几个星期的思索,总算得出了根据表的某几个字段做散列的缓存办法,这种办法无需大规模遍历,所以CPU符合非常小,由于这种列表缓存按照字段做了散列,所以命中率极高。思路如下:每个表有3个缓存Mapkey=value键值对),第一个Map是对象缓存A,在A中,key是数据库的idValue是数据库对象(也就是一行数据);第二个Map是通用列表缓存BB的最大长度一般1000左右,在B中,key是查询条件拼出来的String(如start=0,length=15#active=0#state=0),Value是该条件查询下的所有id组成的List;第三个Map是散列缓存C,在C中,key是散列的字段(如根据userId散列的话,其中某个key就是userId=109这样的String)组成的Stringvalue是一个和B类似的HashMap。其中只有B这个Map是需要遍历的,不知道说明白了没有,看完小面这个例子应该就明白了,就用论坛的回复表作说明,假设回复表T中假设有字段idtopicIdpostUserId等字段(topicId就是帖子的idpostUserId是发布者id)。

 

第一种情况,也是最常用的情况,就是获取一个帖子对应的回复,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个)都散列到keytopicId=2008这一个Map中,当id2008的帖子有新的回复时,系统自动把keytopicId=2008的散列Map清除即可。由于这种散列不需要遍历,因此可以设置成很大,例如100000,这样10万个帖子对应的所有回复列表都可以缓存起来,当有一个帖子有新的回复时,其余99999个帖子对应的回复列表都不会动,缓存的命中率极高。

 

第二种情况,就是后台需要显示最新的回复,sql语句应该是象

select id from T order by createTime desc limit 0,50

的样子,这种情况不需要散列,因为后台不可能有太多人访问,常用列表也不会太多,所以直接放到通用列表缓存B中即可。

 

第三种情况,获取一个用户的回复,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

select id from T where userId=2046 order by createTime desc limit 30,15

的样子,那么这种列表和第一种情况类似,用userId做散列即可。

 

第四种情况,获取一个用户对某个帖子的回复,sql语句象

select id from T where topicId=2008 and userId=2046 order by createTime desc limit 0,15

select id from T where topicId=2008 and userId=2046 order by createTime desc limit 15,15

的样子,这种情况比较少见,一般以topicId=2008为准,也放到keytopicId=2008这个散列Map里即可。

 

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

 

缓存A是:

Key键(long型)

Value值(类型T

11

Id=11T对象

22

Id=22T对象

133

Id=133T对象

……

 

列表缓存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

……

 

……

 

总结:这种缓存思路可以存储大规模的列表,缓存命中率极高,因此可以承受超大规模的应用,但是需要技术人员根据自身业务逻辑来配置需要做散列的字段,一般用一个表的索引键做散列(注意顺序,最散的字段放前面),假设以userId为例,可以存储N个用户的M种列表,如果某个用户的相关数据发生变化,其余N-1个用户的列表缓存纹丝不动。以上说明的都是如何缓存列表,缓存长度和缓存列表思路完全一样,如缓存象select count(*) from T where topicId=2008这样的长度,也是放到topicId=2008这个散列Map中。如果再配合好使用mysql的内存表和memcached,加上F5设备做分布式负载均衡,该系统对付像1000IP/天这种规模级的应用都足够了,除搜索引擎外一般的应用网站到不了这种规模。

 

再次申明:系统到底是不是强大不在系统本身而在于使用该系统的人!!!

 

这个缓存系统是我和同事几年经验的总结,看似简单,其实也没那么简单,把它作为开源有下面几个目的:第一,真的希望有很多人能用它;第二:希望更多的人能够完善和改进它;第三:希望大家能聚到一起为通用高效数据库缓存构架作出贡献,毕竟,数据库操作是各种应用最常用的操作,也是最容易产生性能瓶颈的地方。

 

Zip包中包含了配置方法和测试用的jsp,只要把它配置成一个web应用就可以快速调试并看到缓存的力量了。

 

配置说明文件在docs/开始配置.txt里有说明。

 

最后啰嗦一句,如果大家真想支持我、支持中国人开源项目,请把该文贴到自己的博客中或者收藏本文,记得包含文档的下载地址!!!!!!!谢谢。thank you and Good luck

 

QQ群:24561583

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

一:串行垃圾回收,也就是默认配置,完成10request用时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%

二:并行回收,完成10request用时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 GCMaxGCPauseMillis根本没有用,停顿时间可能超过20秒,之后会发生什么我也不再关心了,赶紧重启resin,尝试其他回收策略。

三:并发回收,完成10request用时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表示执行NFull GC后执行内存压缩。

四:增量回收,完成10request用时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模式,和增量回收差不多,完成10request用时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推荐的参数,照样不好使。

六:递增式低暂停收集器,还叫什么火车式回收,不知道属于哪个系,完成10request用时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(并行回收年轻代)时的promotion failed错误就一切好办了,查了很多文章,发现引起promotion failed错误的原因是CMS来不及回收(CMS默认在年老代占到90%左右才会执行),年老代又没有足够的空间供GC把一些活的对象从年轻代移到年老代,所以执行Full GCCMSInitiatingOccupancyFraction=70表示年老代占到约70%时就开始执行CMS,这样就不会出现Full GC了。SoftRefLRUPolicyMSPerMB这个参数也是我认为比较有用的,官方解释是softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte 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.8sParNew GC30秒左右才执行一次,每次回收约0.2秒,看来问题应该暂时解决了。

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

 

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

如何使用memcached-Server?
在服务端运行:
# ./memcached -d -m 2048 -l 10.0.0.40 -p 11211
这将会启动一个占用2G内存的进程,并打开11211端口用于接收请求。由于32位系统只能处理4G内存的寻址,所以在大于4G内存使用PAE32位服务器上可以运行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);
然后用mCachedClientset/get/delete方法就可以了。memcached的吞吐量每秒大概能get两万次左右,这比mysqlselect提高了好几倍,所以很多网站都用这个来做缓存,如豆瓣。

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

用了分布式后,缓存同步和分布式session都是比较难处理的问题,所以建议1000pv/天以下的应用不要用分布式。



一般来说读多写少,即数据库更新不太频繁的数据,是很适合做memcached;另外,附带说一下memcachedkey & value的限制:
key250个字符;value1M的对象

最近一直在思考,如何才能解决数据库的瓶颈,读可以采取很多种手段来cache,如果写的并发很大的时候,如订单的集中提交,改如何优化呢? 

0 0