史荣久

史荣久的博客

他的个人主页  他的博客

码工考古之DataSync

史荣久  2010年06月19日 星期六 20:56 | 1670次浏览 | 4条评论

http://www.trydofor.com/a9w3-auhome/trydofor/article/2010/0619205031/body.htm
DataSync 是一个小布丁点的工程,用来在两个数据库间倒扯数据。

码工考古之DataSync
作者:臭豆腐[trydofor.com]
日期:2010-06-15
授权:署名-非商业-保持一致 1.0 协议
声明:拷贝、分发、呈现和表演本作品,请保留以上全部信息。

文档目录
1. DataSync是啥玩意
DataSync 是一个小布丁点的工程,用来在两个数据库间倒扯数据。

  • 纯JDBC,以PreparedStatement插数据,以SQL记log。
  • 纯SQL同步,支持变量。
  • 自动解决表间依赖关系。
  • 可自定义字段生成规则。
  • 不支持目标数据库的Update,只能Insert。
  • 不能一次倒扯海量数据。
  • java文件27个。
  • 代码1827行,注释196行,空行358,共计2362行。
  • 累积人力约1*8人时。

原始需求是这样的:
我们的高利贷系统,有近20*5个测试DB,经常要在两个DB间倒扯数据玩。
但不幸的是DB的主键有自增的,有业务逻辑产生的;表关系复杂;表多。

2. 蛇打七寸擒贼擒王
初步的需求分析和程序设计,集中在配置文件的定义上。
工具是文本编辑器,两张打印纸加签字笔。电脑方便撤销与修改,笔纸则便于涂鸦。
写了改,改了写的过程贯穿项目始终,美其名曰:持续重构 O(∩_∩)O。

测试数据的ER图:

一个自然人(PERSON,自增主键)对应一个顾客(CUSTOMER,业务主键),
一个顾客对应多个债权(LOAN,业务主键),
一个债权对应多个契约(CONTRACT,双主键,MAX+1)。
各表有特殊用途的字段,如:LOGNO,CREATEDBY,CREATEDDT。

程序配置文件:
< xml > 配置文件 
            001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
           
            
             <?xml
            
            
             version="1.0" encoding="UTF-8"?>
            
            
             <
            
            root
            
             >
            
            
             <!--
            
            
             数据库配置 -->
            
            
             <
            
            database
            
             >
            
            
             <!--
            
            
             源数据库 -->
            
            
             <
            
            source
            
             >
            
            
             <
            
            driver
            
             >
            
            com.ibm.db2.jcc.DB2Driver
            
             <
            
            /driver
            
             >
            
            
             <
            
            url
            
             >
            
            jdbc:db2://10.4.9.52:50000/SEC0601
            
             <
            
            /url
            
             >
            
            
             <
            
            user
            
             >
            
            db2inst1
            
             <
            
            /user
            
             >
            
            
             <
            
            pass
            
             >
            
            LNjT4DRw
            
             <
            
            /pass
            
             >
            
            
             <
            
            /source
            
             >
            
            
             <!--
            
            
             目标数据库 -->
            
            
             <
            
            target
            
             >
            
            
             <
            
            driver
            
             >
            
            com.ibm.db2.jcc.DB2Driver
            
             <
            
            /driver
            
             >
            
            
             <
            
            url
            
             >
            
            jdbc:db2://10.4.5.242:50000/PLX
            
             <
            
            /url
            
             >
            
            
             <
            
            user
            
             >
            
            db2inst1
            
             <
            
            /user
            
             >
            
            
             <
            
            pass
            
             >
            
            s1Iup3Dz
            
             <
            
            /pass
            
             >
            
            
             <
            
            /target
            
             >
            
            
             <
            
            /database
            
             >
            
            
             <!--
            
            
             全局变量 以 ${name}格式调用 -->
            
            
             <
            
            parameters
            
             >
            
            
             <!--
            
            
             | type=text时,以字符串保持value值或value指向文件的全部内容。
            
            
             | type=list时,以字符串列表保持value值或value指向文件的每一行。
            
            
             -->
            
            
             <
            
            para name=
            
             "
            
            
             system.commit.debug"
            
            type=
            
             "
            
            
             text"
            
            value=
            
             "
            
            
             true"
            
            /
            
             >
            
            
             <
            
            para name=
            
             "
            
            
             system.target.delete"
            
            type=
            
             "
            
            
             text"
            
            value=
            
             "
            
            
             false"
            
            /
            
             >
            
            
             <
            
            para name=
            
             "
            
            
             system.target.existed"
            
            type=
            
             "
            
            
             text"
            
            value=
            
             "
            
            
             delete"
            
            /
            
             >
            
            
             <
            
            para name=
            
             "
            
            
             system.source.delete"
            
            type=
            
             "
            
            
             text"
            
            value=
            
             "
            
            
             false"
            
            /
            
             >
            
            
             <!--
            
            
             ./data/loanid.txt 文件一行一个债权,存到 loanids的List中 -->
            
            
             <
            
            para name=
            
             "
            
            
             loanids"
            
            type=
            
             "
            
            
             list"
            
            value=
            
             "
            
            
             ./data/loanid.txt"
            
            /
            
             >
            
            
             <
            
            para name=
            
             "
            
            
             logno"
            
            type=
            
             "
            
            
             text"
            
            value=
            
             "
            
            
             9999"
            
            /
            
             >
            
            
             <
            
            /parameters
            
             >
            
            
             <!--
            
            
             操作对象(表) -->
            
            
             <
            
            candidate
            
             >
            
            
             <!--
            
            
             表的共通字段 -->
            
            
             <
            
            common
            
             >
            
            
             <!--
            
            
             使用变量 logno -->
            
            
             <
            
            column name=
            
             "
            
            
             LOGNO"
            
            value=
            
             "
            
            
             ${logno}"
            
            /
            
             >
            
            
             <!--
            
            
             使用固定值 9999 -->
            
            
             <
            
            column name=
            
             "
            
            
             CREATEDBY"
            
            value=
            
             "
            
            
             9999"
            
            /
            
             >
            
            
             <!--
            
            
            
            
             | 可以是固定值 2010-05-05 05:05:05 也可是DB变量 CURRENT TIMESTAMP
            
            
             | mutable 为true,该字段可变,不能作为where条件。默认为false。
            
            
             -->
            
            
             <
            
            column name=
            
             "
            
            
             CREATEDDT"
            
            mutable=
            
             "
            
            
             true"
            
            value=
            
             "
            
            
             CURRENT TIMESTAMP"
            
            /
            
             >
            
            
             <
            
            /common
            
             >
            
            
             <
            
            tables
            
             >
            
            
             <!--
            
            
             表的集合 -->
            
            
             <
            
            table name=
            
             "
            
            
             LOAN"
            
            
             >
            
            
             <!--
            
            
             表名 LOAN -->
            
            
             <!--
            
            
             该表在源数据库上抽取数据的SQL,使用全局变量 loanids -->
            
            
             <
            
            source
            
             >
            
            SELECT * FROM LOAN WHERE
               LOANID IN (${loanids})
            
             <
            
            /source
            
             >
            
            
             <
            
            /table
            
             >
            
            
             <
            
            table name=
            
             "
            
            
             CUSTOMER"
            
            
             >
            
            
             <!--
            
            
             以源数据中LOAN表的CUSTID集合作为参数 -->
            
            
             <
            
            source
            
             >
            
            SELECT * FROM CUSTOMER WHERE 
               CUSTID IN (${LOAN:CUSTID})
            
             <
            
            /source
            
             >
            
            
             <
            
            target
            
             >
            
            
             <!--
            
            
             | 在目标数据库中,该表的指定字段如何设值
            
            
             | 这里按源数据对应关系,设置成目标数据库中PERSON表的PERSONID值。
            
            
             -->
            
            
             <
            
            column name=
            
             "
            
            
             PERSONID"
            
            value=
            
             "
            
            
             ${PERSON:PERSONID}"
            
            /
            
             >
            
            
             <
            
            /target
            
             >
            
            
             <
            
            /table
            
             >
            
            
             <
            
            table name=
            
             "
            
            
             PERSON"
            
            
             >
            
            
             <
            
            source
            
             >
            
            SELECT * FROM PERSON WHERE 
               PERSONID IN (${CUSTOMER:PERSONID})
            
             <
            
            /source
            
             >
            
            
             <
            
            /table
            
             >
            
            
             <
            
            table name=
            
             "
            
            
             CONTRACT"
            
            
             >
            
            
             <
            
            source
            
             >
            
            SELECT * FROM CONTRACT WHERE 
               LOANID IN (${LOAN:LOANID})
            
             <
            
            /source
            
             >
            
            
             <
            
            target
            
             >
            
            
             <!--
            
            
             在目标数据库中执行SQL,以返回值设置字段值 -->
            
            
             <
            
            column name=
            
             "
            
            
             CONTRACTSEQ"
            
            clazz=
            
             "
            
            
             SqlQuery"
            
            value=
            
             "
            
            
             SELECT CASE WHEN  MAX(CONTRACTSEQ) IS NULL THEN 1
            
            
             ELSE MAX(CONTRACTSEQ)+1 END FROM CONTRACT
            
            
             WHERE LOANID=${CONTRACT:LOANID}"
            
            /
            
             >
            
            
             <
            
            /target
            
             >
            
            
             <
            
            /table
            
             >
            
            
             <
            
            /tables
            
             >
            
            
             <
            
            /candidate
            
             >
            
            
             <
            
            /root
            
             >
            
           

3. 行而知之知而又行
编码中有几个兴趣点,稍稍驻足了一些时间,调研和拓展了不少知识。

3.1. XML到类和类关系
类和类关系,在编码,编译和测试时十分方便。
xstream.alias("root", DataSyncConf.class);
xstream.aliasField("parameters",DataSyncConf.class,"parameters");
... ...
xstream.alias("para", ParaConf.class);
xstream.useAttributeFor(ParaConf.class,"name");
... ...
xstream.alias("candidate", CandidateConf.class);
xstream.aliasField("common",CandidateConf.class, "common");

3.2. 数据的依赖关系
数据依赖分两中情况:关系依赖和赋值依赖。

关系依赖和DB表的逻辑关系一致,如,
SELECT时是这样的:
一条线是,${loanids}到LOAN到CUSTOMER到PERSON;
另条线是,LOAN到CONTRACT。
而INSERT时,一般是SELECT的反过程。

赋值依赖,即一个字段的赋值方法,可能需要业务逻辑或变量生成。如,
column name="LOGNO" value="${logno} 是变量。
column name="LOANID" clazz="LoanIdMaker" 是业务逻辑。
通常两者是结合使用的,如,
column name="CONTRACTSEQ" clazz="SqlQuery" value="SELECT...${CONTRACT:LOANID}"

3.3. 变量作用及性质
变量主要是用来表示和解决依赖的。运行时涉及四类变量。
  1. 系统设置,定义程序行为。如:system.commit.debug
  2. 程序参数,作为生产原料。如:loanids
  3. 数据库结构(Schema)。如,表结构,表关系,字段类型。
  4. 实时数据,是Schema的具体表现。包括源数据和目标数据。

程序设计中,
(1)和(2)放到全局变量中,以 ${name}表示,运行时为 ParaContext。
(3)源DB结构,目标DB须与其一致。不可直接被变量引用。运行时为 DbContext。
(4)实时数据,以${TABLE:COLUMN}表示,运行时为 DataContext。

下面是变量的一个具体应用。
"CONTRACT WHERE LOANID IN (${LOAN:LOANID})"
这个SQL中有一个变量。解析时,重要信息有,
  • 所属DB,是source还是target,视SQL所在位置而定。
  • DB结构,是CONTRACT表LOANID字段。
  • 取值范围,是LOAN表
  • 取值标识,是LOANID,即LOAN表的LOANID字段。

3.4. 字符串分析处理
简单的解析SQL。如,
SELECT * FROM LOAN WHERE LOANID IN (${loanids})
必须知道${loanids} 对应的类型是 LOAN表的LOANID字段。
如果loanids是一组值,输出SQL时,是 '***','***'格式。

识别变量格式。如,
  1. name="CREATEDDT" value="2010-05-05 05:05:05"
  2. name="CREATEDDT" mutable="true" value="CURRENT TIMESTAMP"
在SQL表示中,1)输出为 '2010-05-05 05:05:05',2)则是 CURRENT TIMESTAMP

数据构造SQL。如,
单引号转义,数字型直接输出,字符日期加单引号等。

4. 走两步拉出来溜溜
程序设计为单线程,并且源数据全部载入后,才操作目标数据库。
优点是,程序结构简单易懂,数据依赖处理方便灵活。缺点是吃内存。

VMware上 java -server -Xmx512M -jar 执行测试数据,
LOAN, columns:212, records:3000
CUSTOMER, columns:109, records:2794
RPERSON, columns:312, records:2794
CONTRACT, columns:182, records:7089

初始化DbContext耗时1.5秒,
Select 15677 条源数据8秒,
插入目标数据(DB2)用了30分钟,平均每秒8.7条。
插入目标数据(MySql)用了8分钟,平均每秒32.7条。
速度真慢,难道是没批量提交?

程序是这样工作地,描述如下,(此图半天功,O(∩_∩)O)


下面是应用中较为重要的几个类。
TargetColumnSetter,用来实现如何对目标数据库字段赋值。
SimplePara,默认setter。
SqlQuery,执行SQL,使用结果赋值。

SqlFormat,根据sqlType类型和Object类型构造SQL语句。
VarParser,用来把字符串(主要是SQL)中的变量解析出来。
SqlBuilder,是根据参数,构造 JDBC Statement用的。

DataContext,按顺序存放源数据和目标数据。
ParaContext,存放全局变量。
DbContext,存放DB的表结构和关系;源DB和目标DB连接。


评论

我的评论:

发表评论

请 登录 后发表评论。还没有在Zeuux哲思注册吗?现在 注册 !

暂时没有评论

Zeuux © 2024

京ICP备05028076号