“設計應對變化”--例項講解一個數據同步系統
系列文章索引:
[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派還有非主流資料庫派之間大打口水仗,用一種資料庫來一統天下.如果某個應用系統是需要高度安全的儘管它採用的資料庫產品和資料庫結構都一致,但它在一個單獨的地方,使用V**來連線它的資料庫,安全主管不會同意.
於是,在各個應用系統間同步資料的計劃這樣難產了,大家要資料,還是用最傳統的方式,帶個行動硬碟,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,抽象出同步介面 對與使用者資訊,我們前面討論的結果認為在當前的各系統中,使用者標識,使用者名稱稱和年齡是“使用者類”共有的屬性,現在我們為使用者類抽象出一個介面:
interface IUser
{
int UID,
string Name,
int Age
}
除了使用者資訊,可能還有其它資訊也需要我們同步,所以我們還需要抽象出一個共同的“資料同步介面”。這裡我們假設以“記錄時間”作為記錄是否要同步的依據,只有修改過的或者新增的記錄才需要同步。
interface IDataSync
{
datetime RecordDate
}
我們修改一下前面的使用者介面定義:
interface IUser : IDataSyncData
{
int UID,
string Name,
int Age
}
3,實現資料同步實體類 有了使用者類介面,我們可以實現使用者實體類了,一般情況下,兩個系統間的同一個表可以共享一個實體類的,但我們這裡的情況有點不同,兩個系統間的使用者表結構不一致,需要單獨定義。 系統A中的使用者實體類:
//程式碼檔案 SystemA.User.cs 開始
/*
本類由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中的使用者實體類:
//程式碼檔案 SystemB.User.cs 開始
/*
本類由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資料了
今天需要使用“資料同步程式”將外網資料庫的FundYield 資料重新同步到內網,上次成功的一次將50W資料查詢了出來,但這次不行了。記得上次外網伺服器剩餘記憶體較多,SQLSERVER只佔用了150M,這次佔了500多M,程式無論如何也不能一次查詢出50W資料來,老是查詢超時,但這個資料著急要,只有想辦法了。
系統使用每個表的最後修改日期(ZHXGRQ)欄位作為更新的標記,檢查了下資料,發現有51W多條資料都是 1999-1-1 ,除非程式將這51W條資料全部一次查詢出來,否則只有另外想辦法。看了下表結構,還有一個ID欄位(bigint型別),雖然不是主鍵,但不重複,這樣我們可以使用這個欄位作為“分頁”的依據了,每次查詢個10-20W資料是沒有問題,於是將原來的實體類修改為下面的樣子:
資料更新實體類必須繼承一個數據更新介面:
WcfMail.Interface.IDataSyncEntity
namespace WFT_DataSyncModel
{
[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;//對映為表
好了,實體類的修改僅此一處,實體類對映指定為SqlMap型別,必須建立一個SqlMap配置檔案,檔名固定是 “EntitySqlMap.config” ,下面是檔案內容:
<?xml version="1.0" encoding="utf-8"?>
<!--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,所需的工作完成,我們只改了一下實體類的對映型別和編寫了一個實體類查詢檔案,編譯專案,重新發布,開始執行,剩下的只是每次修改一下配置檔案的查詢條件了,比如我現在正在使用的條件:
where ID>=600000 and ID<800000
這樣做比較像使用了資料庫的檢視,但對於通常的檢視,資料庫是不允許更新的。我們減輕了維護資料庫檢視的工作,又獲得了檢視的便利性,而且避免了檢視的缺點,這實在是將資料對映為實體的好處。
所以,對於一個數據實體而言,它的資料來源可以是:
- 一個表
- 一個查詢
- 一個檢視
- 一個儲存過程
最後的工作就是等待它執行完成,這個任務就OK了。
==================
總結:
使用面向物件的方法(OO)也可以很方便的處理“純資料問題”,資料只是物件的一部分,我們將資料放到物件中去處理,使得我們對新問題的處理變得很容易,這就是OO的美妙之處!
下節,我們將探討一下資料的變身術。