1、 利用场景描写
系统主要为教师在线学习提供服务,其中视频学习网站支持教师在线视频学习,教师在视频学习进程中其学习进程会被记录下来。每一个专题下对应多个教学视频,每一个教学视频时长不尽1致。现在的记录规则是:教师在看视频的时候,视频所在的页面每分钟提交1次要求,记录该视频已学习时长,并将该记录更新到http://www.wfuyu.com/db/。
目前http://www.wfuyu.com/db/中有8266个教师用户,在***政策下,极有可能在某1段时间内大部份教师用户同时在线看视频。这意味着在极端情况下,每分钟可能会提交6000+个要求,对利用http://www.wfuyu.com/server/带来了很大的压力。另外,我们在更新学习时长记录前,会将其与已学时长(需要实时的查询)相比较,如果当条件交的时长比已学时长大则更新,否则不更新,频繁的查询与更新http://www.wfuyu.com/db/严重下降了系统的响应速度。对学习时长的记录进程进行优化燃眉之急。
2、 硬软件情况
硬件:1台http://www.wfuyu.com/server/,核心4核,内存
软件:Windows Server 2008 64位操作系统, Tomcat 7, jdk1.6,http://www.wfuyu.com/access/5.5
3、 优化进程
1》第1阶段
分析:首先想到的是,这个进程主要的压力在于大量的http://www.wfuyu.com/server/要求和频繁的http://www.wfuyu.com/db/连接,那末,合并要求,利用缓存机制应当可以解决问题。
解决方案:将用户的要求置入缓存,定时集中处理,合并更新操作。
具体做法:利用线程安全的包装后的HashMap作为用户要求缓存。
public static Map<LearnTime, Integer> map = Collections.synchronizedMap(new LinkedHashMap<LearnTime,Integer>()); //用户要求来了以后将要求置入缓存 protected static void put(String userName, String videoId, int topicId, int totalTime, int learnTime) { LearnTime learn = new LearnTime(userName, videoId, topicId, totalTime); //可以保证缓存中的时间比看过的时长要大 if(map.containsKey(learn)) { if(learnTime > map.get(learn)) { map.put(learn, learnTime); } }else { //每一个"用户-视频"在1个缓存时间内只查1次 int learnedTime = dao.getLearnedTime(userName, videoId, topicId);已学习的时间 if(learnTime > learnedTime) { map.put(learn, learnTime); }else{ map.put(learn, learnedTime); } } } |
每隔1个小时,对缓存数据做1次处理,将学习记录更新到http://www.wfuyu.com/db/:遍历HashMap数据项,生成sql语句,拼接到1起,然后在1个连接以内处理完。
Iterator<LearnTime> i = s.iterator(); StringBuilder str = new StringBuilder(); while (i.hasNext()) { learn = i.next(); userName = learn.getUserName(); videoId = learn.getVideoId(); topicId = learn.getTopicId(); learnTime = map.get(learn); totalTime = learn.getTotalTime(); if(learnTime < totalTime){ //分数保存两位小数 String sScore = new DecimalFormat("#.00").format(0.5*learnTime/totalTime); Double score = Double.valueOf(sScore); str.append(sql语句); }else if(learnTime >= totalTime) { learnedTime = dao.getLearnedTime(userName, videoId, topicId);已学习的时间 if(learnedTime != totalTime) { str.append("sql语句"); } } } |
这样处理以后,系统性能得到了1定的改良,但是http://www.wfuyu.com/db/连接的压力还是挺大的,从程序代码中可以看到,在往缓存中添加学习记录和更新之前,都有连接http://www.wfuyu.com/db/进行查询的操作,对http://www.wfuyu.com/db/连接也有较大的消耗。另外,用户端对http://www.wfuyu.com/server/的大量要求并没有得到较好的解决。因此还需要继续优化。
2》第2阶段
分析:前面已分析了依然存在的两个问题,其1是利用http://www.wfuyu.com/server/的大量并发要求,第2是http://www.wfuyu.com/db/的频繁访问。还有1个没有提到过的,当缓存正在被读取时,往缓存里面写数据是要被阻塞的,如果缓存遍历和更新处理过慢,则会致使长时间的要求阻塞。
解决方案:对利用http://www.wfuyu.com/server/的要求,由于每一个要求做的事情仅仅是做1个查询,然后向缓存里面更新数据,这个进程是非常短的,我们可以利用Tomcat配置1个较大的线程池,以响应如此多的要求;对http://www.wfuyu.com/db/的频繁访问,不难发现,其实更新进程已合并了,只是查询已学时长还是单个做的,查询仅仅为了校验是不是更新,若把是不是更新交给http://www.wfuyu.com/db/去决定,那末所有的查询要求都会合并到更新中去,这样这个问题就解决了;缓存的遍历快慢由缓存的大小决定,需要选择适合的缓存周期;更新的处理可以剥离出来,在遍历缓存的同时,将数据取出,另外开1个线程来处理更新操作,让HashMap的锁释放,减少阻塞时长,遍历并生成sql语句进程应当可以控制在几秒内,影响不大。
具体做法:
1.将要求置入缓存
public static Map<LearnTime, Integer> map = Collections.synchronizedMap(new LinkedHashMap<LearnTime,Integer>()); //用户要求来了以后将要求置入缓存 protected static void put(String userName, String videoId, int topicId, int totalTime, int learnTime) { LearnTime learn = new LearnTime(userName, videoId, topicId, totalTime); //可以保证缓存中的时间比看过的时长要大 if(map.containsKey(learn)) { if(learnTime > map.get(learn)) { map.put(learn, learnTime); } }else { //直接放入缓存,判断是不是更新在http://www.wfuyu.com/db/处理 map.put(learn, learnTime); } } |
2.更新进程剥离
static class UpdateTask implements Runnable{ private String sql; public UpdateTask(String sql) { this.sql = sql; } @Override public void run() { dao.updateLearnTime(sql); } } |
3.缓存周期清算
count++; //每处理1次更新,计数加1 //到达缓存周期时长,将缓存清算掉,计数清零 if(count % clearCycle == 0) { map.clear(); count = 0; } |
4.校验进程都在http://www.wfuyu.com/db/做,即更新语句作限制,略。
5.利用http://www.wfuyu.com/server/Tomcat连接池配置。
1)下载tcnative⑴.dll,以支持APR要求 2)将dll文件复制到windows/system32下面,或将其加入path 3)配置Tomcat下的server.xml <Executor name="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="100" prestartminSpareThreads="true" /> <Connector executor="tomcatThreadPool" URIEncoding="utf⑻" port="80" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="20000" redirectPort="8453" maxThreads="1000" minSpareThreads="200" acceptCount="1000" /> |
4、 优化总结
该实际场景的优化主要在4个方面:1、合并http://www.wfuyu.com/db/连接要求;2、增加利用http://www.wfuyu.com/server/响应线程数;3、实际更新处理与缓存周期剥离以减少阻塞;4、权衡缓存大小和用户使用习惯,公道设置缓存清算周期。
另外,该场景最初的瓶颈在于频繁的http://www.wfuyu.com/db/连接,而正好可以通过合并连接来优化。在极端情况下,没法合并连接呢?这就必须要在http://www.wfuyu.com/db/访问层利用连接池进行优化,在现有架构下,还不知道如何配置http://www.wfuyu.com/db/连接池,这是1个需要摸索的重要优化点。
5、 测试数据
实际场景下,假定8000+用户同时在线看视频,1分钟有8000+次要求,平均每秒140次。每一个要求由1个线程来履行,在测试时,摹拟这个进程。
测试用例:每100毫秒提交20个要求,也就是1秒200个要求,每一个要求开启1个新的线程履行,共提交2000000要求,测试时长10000秒。以3000用户(由于不好实际摹拟,采取将http://www.wfuyu.com/db/中数据提取出来的方式,用随机提交要求的方式在程序中测试)在线看视频,每3分钟处理1次更新,每8个处理周期清算1次缓存。测试结果毫无压力。可能在实际场景中,需要遍历的缓存项会多1些,但是根据经验,http://www.wfuyu.com/server/履行更新语句可以到达每秒3000条以上,况且其实不会对缓存阻塞,因此完全满足性能需求。
把要求频率改成每100毫秒提交50个要求,也就是每秒500个要求,其他条件不变,测试时长4000秒。测试结果也很理想,没有多大改变,也就是说支持3万用户的时长记录要求木有问题。http://www.wfuyu.com/server/资源有浪费啊!
测试代码以下:
Random random = new Random(); LearnTime learn; int index = 0; int learnTime = 0; for(int i = 0;i < 2000000;i++){ index = random.nextInt(3000); learn = learns[index]; learnTime = random.nextInt(learn.getTotalTime()+1); Thread thread = new Thread(new PutTask(learn, learnTime)); thread.start(); if(i%50 == 0){ Thread.sleep(100); } } //要求线程摹拟以下: static class PutTaskimplements Runnable{ private LearnTimelearn; privateintlearnTime; public PutTask(LearnTime learn,int learnTime){ this.learn = learn; this.learnTime = learnTime; } publicvoid run() { LearnTimeHandleServlet.put(learn,learnTime); } } |