【51CTO精选译文】建立网站时,最常见的需求是将网页载入时间最小化,例如,一般对主要页面的载入时间要求控制在0.5秒以内,当然网页载入时间是与许多因素关联的,如网络,缓存,Web服务器,脚本语言/代码,数据库访问等。当然我想要讨论在创建Web页面时对数据库的访问,我指的是动态Web页面,如使用PHP,Java/J2EE,Ruby和Asp(.NET)等创建的动态Web页面。
一个非常常见的编程风格是使用脚本段,如:
- < html>
- < body>
- Time now is < %= new java.util.Date() %>
- < /body>
- < /html>
使用当前时间的文本代替上面的“< %= new java.util.Date() %>”。
如果我制作了一个动态内容站点,比如使用Wordpress建立一个博客站点,需要生成多个动态内容,如最新的帖子,热门标签,帖子评论等,同样也会生成对数据库的访问,主要是查询数据库,我想这里不用再多说什么,大家都能理解了。
问题
在生成一些“重量级”的网页时,如在线报纸和书店,可能会涉及到多个查询,如你是否已经登录?我们是否有建议推给你?最新的主题是什么?你以前的兴趣是什么?你有朋友在线吗?你在网站上会产生什么内容?
我最近审查一个网站,每个页面产生的查询次数都大于500,我个人认为这是一个非常高的数目了,但这些查询确实都是需要的,问题是网页载入时间花了2秒。
经过调整,重写和使用索引后,页面载入时间下降到0.6秒,但这还不够快,问题是所有数据库访问都是串行的,它们需要并行起来。Web页面并行化的需求由此诞生。
MySQL为一个查询计算只能使用一个线程,但I/O却是支持多线程的,这就导致在标准的Linux发行版上,对于一个给定的Web页面,只有一个CPU可以使用。模板引擎是一个接一个地解析脚本段的,按顺序执行的,在Java中,一个JSP页面被重写为一个普通的Java Servlet类,脚本段成为主要的代码,HTML代码成为打印标准的输出,因此获得是线性执行代码。
即便是再先进的框架,其标准的方法还是线性的,例如,使用Spring框架,你有Java对象和控制器负责Web页面,可以避免在动态Web页面中使用脚本,只需要那些控制器提供的数据,因此如果使用Spring + Velocity,一个Web页面看起来就会是:
- < html>
- < body>
- Login time as recorded in DB is: ${user.loginTime}
- < /body>
- < /html>
在预建user对象时调用getLoginTime()方法进行转换,但这个方法是如何工作的呢?
它会缓慢地初始化以便访问数据库得到答案吗?
在某些init()方法期间控制器会设置值吗?
控制器会设置值以响应Web页面的请求参数,并逐一解析它们吗?
上面的一切都是线性或是串行执行的。
压缩网页载入时间:如何并行?
Web页面并行化不是一件容易的事情,需要理解多线程编程,程序员需要了解竞争环境、死锁和资源缺乏问题等,不过这些在动态网页中一般不会成问题,有些编程语言对多线程编程提供了良好的支持,Java就是这样一门语言。
假设我需要产生一个10个查询来响应Web页面的请求,如使用Java,我们可以像下面这样编写代码:
- CountDownLatch doneSignal = new CountDownLatch(10);
- Runnable task1 = new Runnable() {
- public void run()
- {
- user.setLoginTime(this.jdbcTemplate.queryForInt("SELECT ... FROM ..."));
- doneSignal.countDown();
- }
- } ;
- Runnable task2 = new Runnable() {
- public void run()
- {
- headlines = getSimpleJdbcTemplate().query("SELECT * FROM headline WHERE...",
- new ParameterizedRowMapper< Headline>() {
- public Headline mapRow(ResultSet rs, int rowNum)
- {
- Headline headline = new Headline();
- headline.setTitle(rs.getString("title");
- headline.setUrl(rs.getString("url");
- ...
- }
- }
- doneSignal.countDown();
- }
- } ;
- ...
- Runnable task10 = new Runnable() {
- ...
- doneSignal.countDown();
- }
- Executor executor = Executors.newFixedThreadPool(numberOfAvailableProcessors);
- executor.execute(task1);
- ...
- executor.execute(task10);
- doneSignal.await();
- // Now fill in the Model
上面的代码其实相对较简单,可读性也比较好,它的意思是:
◆让我们创建10个任务,但不执行它们,仅仅是制定命令;
◆每项任务完成后,让CountDownLatch知道它已经完成(但记住我们还没有执行它);
◆我们创建或使用一个线程池,使用n个线程,n可以与我们的处理器数量进行关联;
◆我们要求线程池处理所有的线程,在处理过程中,要么是同时运行它们,要么有些是顺序执行,依赖于有多少线程可使用;
◆我们请求CountDownLatch进行阻塞,直到这10个任务都通知已经完成了;
接着执行下一批任务。
Spring有一个内置的TaskExecutor机制提供与上面说的线程池类似的解决方案。
我大多数时候都是使用的C/C++/Java,我不知道在PHP,Ruby,ASP.NET或其它语言该如何实现,上面的代码肯定不能拿去直接使用,我希望看到框架能够提供解决这个问题的方案,以便对于一般的Web开发都可以用上Web页面并行技术。
原文:The DB problem inherent to dynamic web pages
作者:Shlomi Noach
【推荐阅读】