最近有一个需求,需要逻辑重建一堆Db2数据库,如果一张张表导出导入,那工作量就完全不可想像,而且都是重复性工作。重复性工作当然应该找些工具来做,首选的是db2move,但是这玩意最大的问题是必须把数据库导出来,再到新数据库上重新导入,还是慢。幸亏IBM在开发Db2的时候让Load命令支持跨数据库捣腾数据,这样写一个脚本就搞定了。

Part 1(单表Load)

先弄一个单表跨数据库Load的脚本出来,简单粗暴的处理一堆传入参数,将remote database上面的一张表Load到local database里面。

脚本支持特性如下:

  • 支持Identity by default、identity always
  • 支持隐藏列
  • 支持LOB大字段
  • 不支持XML(Remote Load不支持,没办法)
  • 支持生成列
dbname=$1;
rdbname=$2;
ruser=$3;
rpwd=$4;
tabname=$5;
cursor=$6;

curdir=$(cd $(dirname $0) && pwd);

db2 connect to ${dbname};
typeset hasgenerated=$(db2 -x "Select count(*) from syscat.columns where tabschema=SUBSTR('${tabname}',1,INSTR('${tabname}', '.' )-1) and tabname=SUBSTR('${tabname}',INSTR('${tabname}', '.' )+1) and generated='A'");
typeset colatt=generatedoverride;
if [ ${hasgenerated} -gt 0 ];then
   colatt=${colatt}" identityoverride";
fi

typeset cols=$(db2 -x "select trim(listagg(colname,',')) from (select colname from syscat.columns where tabschema=SUBSTR('${tabname}',1,INSTR('${tabname}', '.' )-1) and tabname=SUBSTR('${tabname}',INSTR('${tabname}', '.' )+1) order by COLNO)");

db2 -v "declare ${cursor} cursor database ${rdbname} user ${ruser} using ${rpwd} for select ${cols} from ${tabname}";
db2 -v "load from ${cursor} of cursor modified by implicitlyhiddeninclude ${colatt} messages ${curdir}/logs/${tabname}.log replace into ${tabname} nonrecoverable";
if [ $? -ne 0 ];then
   echo "load ${tabname} failed"
fi
db2 -v "SET INTEGRITY FOR ${tabname} ALL IMMEDIATE UNCHECKED";

Part 2(整库Load)

有了单表的Load,接下来就好办了,只需要一个while循环遍历syscat.tables就完事了,主要脚本如下:

typeset tablist=$(db2 -x "select trim(tabschema)||'.'||trim(tabname) from syscat.tables where tabschema not like 'SYS%' and type='T'")
echo "${tablist}"|while read tabname
do
  sh ${curdir}/load_table.sh ${dbname} ${rdbname} ${ruser} ${rpwd} ${tabname} cursor
done

简单测试了下,单线程除了慢没啥不好的,但是慢就是忍受不了的,那就弄个多线程的版本吧。

Part 3(搓搓的多线程版本)

一说到脚本的多线程并发控制,很多人会想到用nohup挂后台配合一个循环来做,基本上原理也就这样,以下是一个基本版本:

typeset tablist=$(db2 -x "select trim(tabschema)||'.'||trim(tabname) from syscat.tables where tabschema not like 'SYS%' and type='T'")

i=0
echo "${tablist}"|while read tabname
do
  let i=$i+1;
  nohup sh ${curdir}/load_table.sh ${dbname} ${rdbname} ${ruser} ${rpwd} ${tabname} i && let i=$i-1 &
  while [ $i -le 10 ]
  do
     sleep 5;
  done
done
wait

上面的版本基本上实现了脚本10个并发的控制,但是不是理想的并发控制。

Part 4(基于FIFO)

AIX内置的FIFO管道其实就是一个控制脚本并发的工具,建立以下FIFO命令如下:

mkfifo ${pipe};
exec 6<>${pipe};  将新建的FIFO绑定至描述符6中

由于每次Read管道都会读取一条消息,而如果管道里面是空的,则会一直pending在read命令,这样就比较好的实现了并发控制。比如初始化的时候往管理塞n个消息,然后用read来读取,则一次最多有n个并发执行,第n+1的脚本必须等待前面n个脚本中的1个执行完成并且把消息再重新放回管道,才会继续往下执行。

完整的代码如下:

#!/bin/ksh
rdbname=
ruser=
rpwd=
dbname=
curday=
curdir=$(cd $(dirname $0) && pwd);
curhour=$(date +"%H");
file=
concurrent=9
pipe="/tmp/load_pipe"

while getopts ":vr:d:l:u:p:f:h" optname  
do  
    case "$optname" in  
        "v")  
            set -x;
            ;;  
        "r")  
            rdbname=$OPTARG; 
            ;;  
        "u")
            ruser=$OPTARG;  
            ;;  
        "p")
            rpwd=$OPTARG;  
            ;; 
        "d")  
            dbname=$OPTARG;
            ;;
        "f")
            file=$OPTARG;
            ;;  
        "c")
            concurrent=$OPTARG;
            ;;
        "l")
            typeset logfilepath=${curdir}/$OPTARG;
            touch ${logfilepath};
            exec 1>${logfilepath};
            exec 2>${logfilepath};
            ;;
        "h"|"?")
            print "Usage:$0 [-x] -r rdbname -d dbname [-l logfile]"
            exit 0  
            ;;  
        ":")  
            print "Unknown error while processing options"  
            exit -1  
            ;;  
    esac  
done 

if [ -z "${dbname}" ]; then
   printf "Option -d dbname is require\n"
   exit -1;
fi

if [ -z "${rdbname}" ]; then
   printf "Option -r rdbname is require\n"
   exit -1;
fi

if [ -z "${ruser}" ]; then
   printf "Option -u ruser is require\n"
   exit -1;
fi

if [ -z "${rpwd}" ]; then
   printf "Option -p rpwd is require\n"
   exit -1;
fi

rm ${pipe};
mkfifo ${pipe};
exec 6<>${pipe};
i=0;
while [ $i -lt ${concurrent} ]
do
   echo "cur_${i}" >> ${pipe}
   let i=$i+1;
done

db2 connect to ${dbname};
typeset tablist=$(db2 -x "select trim(tabschema)||'.'||trim(tabname) from syscat.tables where tabschema not like 'SYS%' and type='T'")
if [ ! -z "${file}" ];then
   tablist=$(cat $file) 
   i=1;
   while [ $i -lt ${concurrent} ]
   do
     read -u6 cursor;
     let i=$i+1;
   done
fi

echo "${tablist}"|while read tabname
do
  read -u6 cursor;
  sh ${curdir}/load_table.sh ${dbname} ${rdbname} ${ruser} ${rpwd} ${tabname} ${cursor} &
done
wait

至此,实现了一个比较完整的脚本并发控制。顺带添加了并发控制的参数,可以通过-c指定同时并发执行脚本数量。

Simple is better,shell make life simple。

发表评论