GPars应用:并发导入数据

从以前的数据库中导入数据是许多项目必不可少的步骤,如果数据库大的话,还是挺费时间的。如果能利用GPars将批量数据导入过程并行的话,性能还是能提高不少的。

Tomas Lin给我们分享了他使用Grails和GSQL开发批量数据导入脚本,并用GPars并行化此过程的方法。

1. 用Grails和GSQL开发的脚本

使用GPars之前的代码样例如下,从中我们也可复习一下GSQL及Grails API的相关用法:

import groovy.sql.Sql
import org.codehaus.groovy.grails.commons.ConfigurationHolder

/* Imports photos into our Grails application */
def photoImport = {

  def sql = Sql.newInstance(ConfigurationHolder.config.db as String,
            ConfigurationHolder.config.dbUsername as String,
            ConfigurationHolder.config.dbPassword as String,
            "com.mysql.jdbc.Driver")

  /* go through each row of photos */
  sql.eachRow("""SELECT * from photos""") { photo ->

    // get the user
    def user = User.findByUsername( photo.username )

    if (!user) {

      println 'could not locate user: ' + photo.userid
      return;

    } else {

      // creates a new album if the user does not have one
      def album = Album.findByUser( user )
      if( !album ){
        album = new Album( title : "${user.username}'s album", user: user )
	  }

      // import data from database into the photo list
	  album.addToPhotos(caption: photo.title, description: photo.description, album: album)

      // save modified album
	  if (!album.save(flush: true)){
        println "could not save album : " + album.errors
      }
    }
  }
}.call()

几点说明:

  • 该脚本是从photo表中将数据导入到新数据库中,这里只是说明主体思路。
  • 用{...}.call()是为了能在grails shell或grails console中都能够调用该脚本。(如果用Gant脚本,还得做许多设置工作,而Grails shell或Grails console可以自动处理这些设置)
  • 由于产品数据源与开发数据源放在不同地方,这些不同环境信息保存在Config.groovy中

2. 加入GPars

为了利用GPars,配置及代码需要做少量改动:

  • 包含GPars的相关.jar文件,或者在grails-app/conf/BuildConfig.groovy文件中加入:
    	dependencies {
    	  build 'org.codehaus.gpars:gpars:0.10'
    	  build 'org.coconut.forkjoin.jsr166y:jsr166y:070108'
    	}
    	
  • 在代码中加入GPars,但由于JDBC的ResultSet不支持并发遍历机制,因此要改用sql.rows(),这里有说明。
    	sql.eachRow("""SELECT * from photos""") { photo -> ... }
    	

    改为

    	GParsPool.withPool {
    	   sql.rows( """select * from photos""" ).eachParallel { photo -> ... }
    	}
    	
  • 由于并发产生的新线程没有正确的hibernate session,因此,如果直接运行上面所改代码,会报"HibernateException: No Hibernate Session bound to thread..."错误。可以用withTransaction闭包包一下GORM代码,为运行GORM代码,它将获取已有session或创建一个新的。
    	user = User.findByUsername( photo.username )
    	

    改为

    	User.withTransaction{
    		user = User.findByUsername( photo.username )
    	}
    	
  • 为避免StaleObjectExceptions和OptimisticLockExceptions,可以使用悲观数据库锁
    	def album = Album.findByUser( user )
    	if( !album ){
    	  album = new Album( title : "${user.username}'s album", user: user )
    	}
    	

    改为

    	def album = Album.findByUser( user )
    	if( !album ){
    	  album = new Album( title : "${user.username}'s album", user: user )
    	}  else {
    	  // add a pessimistic lock to the album
    	  album = Album.lock( album.id )
    	}
    	

修改后完整的代码样例请参考原文: Writing batch import scripts with Grails, GSQL and GPars

据Tomas Lin“交待”,改造之后,速度大大提高,所花费时间为原来的1/3(原文有对比表格为证)。就改这么几行代码,成果还是比较卓著的。他还提到Spring Batch开发起来比较麻烦而且性能无法保证。

相关资源:

By songwei - Posted on 10 八月 2010