Quantcast
Viewing all articles
Browse latest Browse all 11810

视频学习网站学习时长实时记录-性能优化实践

一、     应用场景描述

系统主要为教师在线学习提供服务,其中视频学习网站支持教师在线视频学习,教师在视频学习过程中其学习过程会被记录下来。每个专题下对应多个教学视频,每个教学视频时长不尽一致。现在的记录规则是:教师在看视频的时候,视频所在的页面每分钟提交一次请求,记录该视频已学习时长,并将该记录更新到数据库。

目前数据库中有8266个教师用户,在***政策下,极有可能在某一段时间内大部分教师用户同时在线看视频。这意味着在极端情况下,每分钟可能会提交6000+个请求,对应用服务器带来了很大的压力。另外,我们在更新学习时长记录前,会将其与已学时长(需要实时的查询)相比较,如果当前提交的时长比已学时长大则更新,否则不更新,频繁的查询与更新数据库严重降低了系统的响应速度。对学习时长的记录过程进行优化迫在眉睫。

二、     硬软件情况

硬件:一台服务器,核心4核,内存

软件:Windows Server 2008 64位操作系统, Tomcat 7, jdk1.6,MySql5.5

三、     优化过程

一》第一阶段

分析:首先想到的是,这个过程主要的压力在于大量的服务器请求和频繁的数据库连接,那么,合并请求,利用缓存机制应该可以解决问题。

解决方案:将用户的请求置入缓存,定时集中处理,合并更新操作。

具体做法:利用线程安全的包装后的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 {

//每个"用户-视频"在一个缓存时间内只查一次

      int learnedTime = dao.getLearnedTime(userName, videoId, topicId);已经学习的时间

      if(learnTime > learnedTime) {

         map.put(learn, learnTime);

      }else{

         map.put(learn, learnedTime);

      }     }

}

每隔一个小时,对缓存数据做一次处理,将学习记录更新到数据库:遍历HashMap数据项,生成sql语句,拼接到一起,然后在一个连接之内处理完。

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语句");

         }

        }

}

这样处理之后,系统性能得到了一定的改善,但是数据库连接的压力还是挺大的,从程序代码中可以看到,在往缓存中添加学习记录和更新之前,都有连接数据库进行查询的操作,对数据库连接也有较大的消耗。另外,用户端对服务器的大量请求并没有得到较好的解决。因此还需要继续优化。

二》第二阶段

分析:前面已经分析了仍然存在的两个问题,其一是应用服务器的大量并发请求,第二是数据库的频繁访问。还有一个没有提到过的,当缓存正在被读取时,往缓存里面写数据是要被阻塞的,如果缓存遍历和更新处理过慢,则会导致长时间的请求阻塞。

解决方案:对于应用服务器的请求,由于每个请求做的事情仅仅是做一个查询,然后向缓存里面更新数据,这个过程是非常短的,我们可以利用Tomcat配置一个较大的线程池,以响应如此多的请求;对于数据库的频繁访问,不难发现,其实更新过程已经合并了,只是查询已学时长还是单个做的,查询仅仅为了校验是否更新,若把是否更新交给数据库去决定,那么所有的查询请求都会合并到更新中去,这样这个问题就解决了;缓存的遍历快慢由缓存的大小决定,需要选择合适的缓存周期;更新的处理可以剥离出来,在遍历缓存的同时,将数据取出,另外开一个线程来处理更新操作,让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 {

        //直接放入缓存,判断是否更新在数据库处理

        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++;         //每处理一次更新,计数加一

                   //达到缓存周期时长,将缓存清理掉,计数清零

if(count % clearCycle == 0) {

      map.clear();

      count = 0;

}

4.校验过程都在数据库做,即更新语句作限制,略。

5.应用服务器Tomcat连接池配置。

1)下载tcnative-1.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-8" port="80" protocol="org.apache.coyote.http11.Http11AprProtocol" 

      connectionTimeout="20000" 

      redirectPort="8453" 

     maxThreads="1000"         

       minSpareThreads="200"

     acceptCount="1000"

/>

四、     优化总结

该实际场景的优化主要在四个方面:1、合并数据库连接请求;2、增加应用服务器响应线程数;3、实际更新处理与缓存周期剥离以减少阻塞;4、权衡缓存大小和用户使用习惯,合理设置缓存清理周期。

另外,该场景最初的瓶颈在于频繁的数据库连接,而正好可以通过合并连接来优化。在极端情况下,没法合并连接呢?这就必须要在数据库访问层利用连接池进行优化,在现有架构下,还不知道如何配置数据库连接池,这是一个需要摸索的重要优化点。

五、     测试数据

实际场景下,假设8000+用户同时在线看视频,一分钟有8000+次请求,平均每秒140次。每个请求由一个线程来执行,在测试时,模拟这个过程。

测试用例:每100毫秒提交20个请求,也就是一秒200个请求,每个请求开启一个新的线程执行,共提交2000000请求,测试时长10000秒。以3000用户(由于不好实际模拟,采用将数据库中数据提取出来的方式,用随机提交请求的方式在程序中测试)在线看视频,每三分钟处理一次更新,每8个处理周期清理一次缓存。测试结果毫无压力。可能在实际场景中,需要遍历的缓存项会多一些,但是根据经验,服务器执行更新语句可以达到每秒3000条以上,况且并不会对缓存阻塞,因此完全满足性能需求。

把请求频率改为每100毫秒提交50个请求,也就是每秒500个请求,其他条件不变,测试时长4000秒。测试结果也很理想,没有多大改变,也就是说支持3万用户的时长记录请求木有问题。服务器资源有浪费啊!

测试代码如下:

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 PutTask implements Runnable{     

      private LearnTimelearn;

      privateintlearnTime;     

      public PutTask(LearnTime learn, int learnTime){

         this.learn = learn;

         this.learnTime = learnTime;

      }

      publicvoid run() {

         LearnTimeHandleServlet. put(learn,learnTime);

      }    

}

 

作者:liyong199012 发表于2014-10-30 16:02:45 原文链接
阅读:99 评论:0 查看评论

Viewing all articles
Browse latest Browse all 11810

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>