“设计应对变化”--实例讲解一个数据同步系统
系列文章索引:
[WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 一]
[WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 二]
"开门待客"还是“送货上门”?
[WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 三]
[WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 四]
数据同步系统的功能:
- 支持同种数据库间的数据同步;
- 支持不同种数据库系统间的同步;
- 数据源可以是一个数据服务;
- 支持全库同步;
- 支持单表同步;
- 支持任意一段时间范围内的数据同步;
- 支持不同的“新数据”策略;
- 例如以时间戳,ID序列等;
- 其它自定义策略;
- 采用“数据实体”中间体,使得
- 原始库表的字段可以少于目标库相应的表;
- 原始库表的字段可以多于目标库相应的表;
- 原始库和目标库对应的表字段名称可以不一样;
- 二次开发快,仅需要以原始库或者目标库生成数据实体;
- 采用邮件作为数据通道,不受网络情况限制
- 例如,一方暂时不能联网;
- 双方网络不稳定;
- 在两个局域网之间实现“数据穿透”;
- 邮件内容使用7位ASCII编码,确保平台通用;
- 数据在传输过程中加密;
- 数据不经过任何文件存取,全部在内存中处理,带来的好处是:
- 数据很难被窃取;
- 不占用磁盘空间;
- 不会感染病毒木马;
- 处理速度快;
- 主动发送数据,更有控制权;
- 时时反馈数据发送状态,确保发送成功;
- 可以采用其它数据传输通道,例如
- HTTP,
- FTP,
- 甚至是网络文件共享
- 全部采用.NET 原生代码开发,零成本部署
- 例如不需要安装Jmail等邮件客户端;
- 不需要安装解压缩工具WinRAR/WinZIP;
- 整个程序无需安装,拷贝即可;
- 为跨平台部署提供可能(例如移植到Linux平台)
- 程序通过配置,自身互为发送端和接收端
- 通过配置,使用OpenMail,JMail,NotesMail组件;
- 通过配置,支持访问Oracle,SQLSERVER等不同的数据源;
- 通过配置,使得系统的数据传输方式可以采用邮件,文件或者FTP
----------------------
有这么多功能,是怎么做到的呢?
应该说,有这么多需求;
也可以说,有这么多变化;
这一切,都是靠良好的系统架构设计作为支撑的。
当然,如果你喜欢怎么快怎么搞,不考虑系统的稳定性,扩展性,宜用性,就当我什么都没有说,你不必再往下看了。
如果您有兴趣,请看我后面写的该主题系列文章。
1,商用数据库产品的"数据同步"
许多商用数据库系统都提供了数据同步功能,例如SQLSERVER,在建立数据同步环境的时候,将源数据库作为"发布服务器",将目标数据库作为"订阅服务器",同时还得启动SQL代理,数据同步环境才可以正常运行.在第一次同步之前,SQLSERVER会对目标服务器作初始化,保证两个数据库结构一模一样.我想它应该还作了其它工作,来标记数据的变化.
Oracle数据库的同步似乎要复杂一些,不知道最新的官方版本有没有提供一个直接的数据同步功能,现有的很多Oracle数据同步方案都采用导出数据文件,再在目标库上导入的方式,不是很方便.
这些数据库同步功能都要求源数据库和目标数据库结构必须完全一致,而且处在同一个网络内,甚至,还要求两个库的版本必须一致,例如,例如,SQLSERVER 2005不能作为SQLSERVER 2008的订阅服务器. 另外,你也别指望将SQLSERVER的数据直接同步到Oracle去.
总结:商用数据库产品的数据同步有以下限制
- 数据库平台必须一致
- 数据库结构必须完全一致;
- 数据库版本兼容或一致;
- 数据库服务器在同一个网络内部;
2,企业应用系统间的数据同步
问题场景
如果整个企业应用系统都采用了同一数据库厂商相同版本的产品,而且系统环境不是分布式的,数据同步不是大问题.但是很多大型企业应用系统内部由各种不同的数据库在提供数据访问和存储,例如CRM系统使用的Oracle 10g,OA使用的SQLSERVER 2008,销售系统使用的是SQLSERVER 2000,外部Web站点使用的是MySql,个人用户使用的是Access.如果有一天,要在各个应用系统中同步产品和客户信息,也许有人会骂为什么会有这么多不同的数据库,也许Oracle派和SQLSERVER派还有非主流数据库派之间大打口水仗,用一种数据库来一统天下.如果某个应用系统是需要高度安全的尽管它采用的数据库产品和数据库结构都一致,但它在一个单独的地方,使用VPN来连接它的数据库,安全主管不会同意.
于是,在各个应用系统间同步数据的计划这样难产了,大家要数据,还是用最传统的方式,带个移动硬盘,U盘或者移动PC来拷贝数据了,因为要同步数据就得要同一种数据库,要一种数据库就得改造所有相关的程序,这个代价实在是太大了,老板不会轻易同意的.
如何避免
出现前面的问题场景,接受该项目的架构师一定会骂原应用系统的架构师或者设计师,为什么不统一设计?为什么设计系统的时候程把大段的业务逻辑写到了存储过程中,程序直接访问数据的表和视图,使得程序与数据库紧密耦合?为什么不采用SOA架构,将数据以"服务"提供?至少,为什么不统一相关表的结构(听起来有无奈)?或者,为什么要搞分布式?
"存在既是合理的"的,企业的应有系统经过了若干年的发展才有现在的规模,才用不同的数据库,都是基于成本考虑的,例如,Web站点采用了开源的SNS框架,它原生支持MySQL,想让它支持Oracle得付出额外的成本.要想写出低耦合的程序没有那么多的设计时间,都是怎么快怎么上,先上功能给老板给客户看(怎么听起来有点像我们自己).
看来,问题很难避免.
问题分析
避免不了问题的产生,我们还是回到上面的场景,来分析一下做数据同步它有哪些难以克服的问题:
- 数据库平台不一致--有Oracle,SQLSERVER,MySql,Access;
- 数据库版本不一致--SQLSERVER 有2000,2008版本;
- 数据库结构不一致;
- 数据库不在同一个网络--有一个系统处于绝对安全的地方;
- 数据库与程序紧密耦合。
- 不能改变现有的系统
- 预算有限(时间、人力、物力)
“不能改变别人,就改变我们自己”,“房子有几道门,就需要几把钥匙”,想用一把钥匙去开所有的门显然是不可以的,我们需要为不同的门锁配制不同的钥匙,做到钥匙跟门锁一一适配。
看到这里,聪明的你可能已经猜出来了,这不就是“适配器模式”吗?是的,但还不完全正确。假设有10道门,10把钥匙分别由10个房客拿着,会不会发生开不开门的情况?所以我们还需要一个管家,房客要开门,找管家拿钥匙,管家根据房客的房号决定给他几号的钥匙,钥匙编号与房间的编号一一对应(用行话:这叫做钥匙与房间的映射,说得更专业点,这叫“关系映射”)。
聪明的你也许又看出来了,管家发钥匙,就是“中介者模式”,而管家发钥匙依据的是钥匙编号与房间的编号一一对应,就是“关系影射”,套在数据库与面向对象软件编程中,就是“ORM”.
是的,上面那个企业应用系统数据同步的解决之道就是:
- 使用适配器模式,统一访问各种数据库系统;
- 使用“ORM”组件,映射不同的表结构;
- 使用中介者模式,屏蔽数据库的各种差异,任何数据的处理都通过中介者完成。
解决方案大致就是这些,下节我们进入更技术化的讨论。
将同类型数据表映射成一个实体对象
1,复杂的同步需求
这里的数据表是关系数据库中的表,将数据表一对一的映射成实体对象是很成熟的技术了,例如大名鼎鼎的ORM持久化框架Hibernate,以及新近.NET平台的Entity Framework。但我们的数据同步环境可能有点特殊,不同的应用系统间却有类似功能的数据表,最典型的例子就是用户表。我们下面举例说明。
在A系统中有一个用户表 TB_User:
----------------------------------------------------------
字段名 类型 长度 主键 说明
==========================================================
UID int 是 用户标识
Name varchar 50 用户名称
BirthDay date 出生日期
RecDate datetime 记录更新或者插入时间
----------------------------------------------------------
在B系统中有一个用户表 Users:
----------------------------------------------------------
字段名 类型 长度 主键 说明
==========================================================
ID int 是 用户标识
UserName varchar 50 用户名称
Age int 年龄
----------------------------------------------------------
现在需要把A.TB_User的数据同步到B.Users,这两个表结构基本上不一样,但它们属于同一类,都是用户表,我们看到,下面3个字段是共同的:
---------------------------------------------------------
字段说明 A系统字段 B系统字段
=========================================================
用户标识 TB_User.UID Users.ID
用户名称 TB_User.Name Users.UserName
年龄 function(TB_User.BirthDay) Users.Age
----------------------------------------------------------
我们注意到两个差别,
A系统的TB_User.RecDate 在B系统中不存在,A系统的TB_User.BirthDay要使用一个函数function来转换成B系统需要的年龄字段。
弄清楚了两个系统间同类表的差异,要把数据从A系统同步到B系统不是很困难的事情。但具体怎么做呢?
- 写一个专门的程序来处理这两个表的同步?显得有点多余,而且表一旦很多,工作量将剧增。
- 使用SQL的函数或者存储过程?但是现在我们前一篇文章已经假设A系统和B系统用的不是同一种数据库,没法在运行时计算。
- 使用视图?这等于跟A系统的数据库增加负担,A系统的DBA不会乐意。
“面向抽象,不要面向细节”,
“如果不能改变别人,就改变我们自己”。
2,抽象出同步接口
对与用户信息,我们前面讨论的结果认为在当前的各系统中,用户标识,用户名称和年龄是“用户类”共有的属性,现在我们为用户类抽象出一个接口:
{
int UID,
string Name,
int Age
}
除了用户信息,可能还有其它信息也需要我们同步,所以我们还需要抽象出一个共同的“数据同步接口”。这里我们假设以“记录时间”作为记录是否要同步的依据,只有修改过的或者新增的记录才需要同步。
{
datetime RecordDate
}
我们修改一下前面的用户接口定义:
{
int UID,
string Name,
int Age
}
3,实现数据同步实体类
有了用户类接口,我们可以实现用户实体类了,一般情况下,两个系统间的同一个表可以共享一个实体类的,但我们这里的情况有点不同,两个系统间的用户表结构不一致,需要单独定义。
系统A中的用户实体类:
/*
本类由PWMIS 实体类生成工具(Ver 4.1)自动生成
http://www.pwmis.com/sqlmap
使用前请先在项目工程中引用 PWMIS.Core.dll
2010-9-22 0:22:24
*/
using System;
using PWMIS.Common;
using PWMIS.DataMap.Entity;
namespace SystemA
{
[Serializable()]
public partial class TB_User : EntityBase,IUser
{
public TB_User()
{
TableName = "TB_User";
EntityMap=EntityMapType.Table;
//IdentityName = "标识字段名";
IdentityName="UID";
//PrimaryKeys.Add("主键字段名");
PrimaryKeys.Add("UID");
PropertyNames = new string[] { "UID","Name","BirthDay","RecDate"};
PropertyValues = new object[PropertyNames.Length];
}
/// <summary>
/// 用户标识
/// </summary>
public System.Int32 UID
{
get{return getProperty<System.Int32>("UID");}
set{setProperty("UID",value );}
}
/// <summary>
/// 用户名
/// </summary>
public System.String Name
{
get{return getProperty<System.String>("Name");}
set{setProperty("Name",value ,50);}
}
/// <summary>
/// 生日
/// </summary>
public System.DateTime BirthDay
{
get{return getProperty<System.String>("BirthDay");}
set{setProperty("BirthDay",value );}
}
/// <summary>
/// 记录日期
/// </summary>
public System.DateTime RecordDate
{
get{return getProperty<System.String>("RecDate");}
set{setProperty("RecDate",value );}
}
//下面的代码由手工添加
/// <summary>
/// 根据生日获取年龄
/// </summary>
public System.Int32 Age
{
get{
datetime diff=datetime.Now-this.BirthDay;
int age=this.BirthDay.Year>1900?diff.Year:0;
return age;
}
}
}
}
//代码结束
系统A中的用户实体类比较复杂,因为年龄数据需要根据日期计算。
注意:我们这里并没有使用SQL查询来映射实体类,因为各种不同的数据库的日期函数都不尽相同,这样做的实体类就没有通用性,所以我们还是手工增加一个计算年龄的属性。
系统B中的用户实体类:
/*
本类由PWMIS 实体类生成工具(Ver 4.1)自动生成
http://www.pwmis.com/sqlmap
使用前请先在项目工程中引用 PWMIS.Core.dll
2010-9-22 0:22:24
*/
using System;
using PWMIS.Common;
using PWMIS.DataMap.Entity;
namespace SystemB
{
[Serializable()]
public partial class Users : EntityBase,IUser
{
public Users()
{
TableName = "Users";
EntityMap=EntityMapType.Table;
//IdentityName = "标识字段名";
IdentityName="ID";
//PrimaryKeys.Add("主键字段名");
PrimaryKeys.Add("ID");
PropertyNames = new string[] { "ID","UserName","Age"};
PropertyValues = new object[PropertyNames.Length];
}
/// <summary>
/// 用户标识
/// </summary>
public System.Int32 UID
{
get{return getProperty<System.Int32>("ID");}
set{setProperty("ID",value );}
}
/// <summary>
/// 用户名
/// </summary>
public System.String Name
{
get{return getProperty<System.String>("UserName");}
set{setProperty("UserName",value ,50);}
}
/// <summary>
/// 年龄
/// </summary>
public System.Int32 Age
{
get{return getProperty<System.Int32 >("Age");}
set{setProperty("Age",value );}
}
/// <summary>
/// 记录日期
/// </summary>
public System.DateTime RecordDate
{
get;
set;
}
}
}
//代码结束
系统B中的用户实体类比较简单,基本上跟数据库用户表结构一一对应。
4,如何使用数据同步实体类
好了,两个系统中的用户实体类都定义完成了,由于它们都继承自IUser接口,所以它们之间完全可以交换数据,最后剩下的工作就是将这两个实体类放到两个程序集中分别编译,例如
系统A中的类编译成SystemA.Model.dll,
系统B中的类编译成SystemB.Model.dll,
只要为数据同步程序的发送端和接收端程序分别指名要使用的“数据同步程序集”即可,无需显式引用,IOC框架能够将它们解除耦合。
数据同步程序发送端将使用SystemA.Model.dll,根据要同步的实体对象映射的数据表,到数据源查询数据,然后填充到实体类中;
数据同步程序接收端将使用SystemB.Model.dll,根据要同步的实体对象映射的数据表,将实体类中的数据,插入或者更新到目标数据库中;
数据的查询和更新操作都由PDF.NET数据开发框架内置支持,不需要写一行SQL语句。
这次,我们用一个更加实际的例子来说明,良好的设计是怎样应对变化的.
SQLSERVER 占了500多M内存,原来的程序无法一次查询出50多W数据了
{
[Serializable()]
public partial class FundYield : EntityBase, WcfMail.Interface.IDataSyncEntity
{
public FundYield()
{
TableName = "FundYield";
EntityMap=EntityMapType.SqlMap;
//IdentityName = "标识字段名";
//PrimaryKeys.Add("主键字段名");
PrimaryKeys.Add("jjdm");
PrimaryKeys.Add("FSRQ");
PropertyNames = new string[] { "ID","jjdm","jjmc","jjjc","dwjz","ljjz","FSRQ","QuarterYield","DayYield","WeekYield","WeekYieldPM","Month1Yield","Month1YieldPM","Month3Yield","Month3YieldPM","Month6Yield","YearYield","YearYieldPM","Year1Yield","Year1YieldPM","Year2Yield","Year3Yield","totalyield","bzc3","bzc6","bzc12","bzc24","BuyState","addtime","ZHXGRQ","DayYieldPM","Month6YieldPM","Year2YieldPM","Year3YieldPM","totalyieldPM","DayYieldCount","WeekYieldCount","Month1YieldCount","Month3YieldCount","Month6YieldCount","YearYieldCount","Year1YieldCount","Year2YieldCount","Year3YieldCount","totalYieldCount" };
PropertyValues = new object[PropertyNames.Length];
}
//...实体属性在此省略
}
在实体类 FundYield 中,有一个实体映射类型属性:
EntityMap=EntityMapType.SqlMap;//映射为自定义SQL查询
默认情况下,应该是
EntityMap=EntityMapType.Table;//映射为表
<!--SQL-MAP 实体类自定义查询配置文件
SQL 语句不能使用 Select * from table 格式,必须指定跟实体类一致的字段定义,否则可能发生难以预测的错误。
要生成实体类,请使用PDF.NET 实体类工具。
有关PDF.NET,请了解 http://www.pwmis.com/sqlmap
power by dth,2010.12.8
-->
<configuration>
<Namespace name="WFT_DataSyncModel">
<Map name="FundYield">
<Sql>
<![CDATA[
SELECT
ID , jjdm , jjmc , jjjc , dwjz , ljjz , FSRQ , QuarterYield , DayYield , WeekYield , WeekYieldPM , Month1Yield , Month1YieldPM , Month3Yield , Month3YieldPM , Month6Yield , YearYield , YearYieldPM , Year1Yield , Year1YieldPM , Year2Yield , Year3Yield , totalyield , bzc3 , bzc6 , bzc12 , bzc24 , BuyState , addtime , ZHXGRQ , DayYieldPM , Month6YieldPM , Year2YieldPM , Year3YieldPM , totalyieldPM , DayYieldCount , WeekYieldCount , Month1YieldCount , Month3YieldCount , Month6YieldCount , YearYieldCount , Year1YieldCount , Year2YieldCount , Year3YieldCount , totalYieldCount
FROM FundYield where id < 400000
]]></Sql>
</Map>
</Namespace>
</configuration>
这样做比较像使用了数据库的视图,但对于通常的视图,数据库是不允许更新的。我们减轻了维护数据库视图的工作,又获得了视图的便利性,而且避免了视图的缺点,这实在是将数据映射为实体的好处。
所以,对于一个数据实体而言,它的数据源可以是:
- 一个表
- 一个查询
- 一个视图
- 一个存储过程
最后的工作就是等待它执行完成,这个任务就OK了。
下节,我们将探讨一下数据的变身术。