天天看點

基于微軟Synchronization Services雙向同步技術在企業項目中的架構應用研究

項目應用場景:

     某客戶是一個大型集團企業的資訊部門,掌管着企業幾百台伺服器,并且以後會不斷擴充;

   為了更好的維護資訊辦的伺服器;資訊部門需要開發一套維護系統,來記錄各個伺服器的相關狀态資訊(如,IP、所安裝在伺服器的應用系統和相關資訊等),便于維護和查詢;客戶維護人員可以攜帶筆記本脫機即時編寫維修伺服器相關記錄資訊(增、删、查、改),用戶端聯網即可自動同步到伺服器最新維護資料。 

     客戶要求實作伺服器端和用戶端雙向同步的方式,用戶端操作資料伺服器端自動更新,反之操作伺服器端,

    用戶端自動更新。

早先的解決方案:

用戶端和服務端都使用SQL2005資料庫,并采用資料庫訂閱的方式進行資料同步。

此種設計缺點是:

      1、用戶端必須安裝SQL2005(包括SQL Server Express),造成操作用戶端系統運作速度降低,占用用戶端計算機系統資源大等問題。

      2、同步方式配置複雜。需要訂閱方式釋出伺服器和訂閱伺服器互相注冊,伺服器端與用戶端配置複雜。

最新的解決方案:

      1、用戶端資料庫采用微軟Microsoft SQL Server Compact 3.5壓縮資料庫,實施技術Microsoft Synchronization Services v1.0。

      2、伺服器端資料庫采用微軟Microsoft SQL Server 2005(2008)。

      3、開發工具是Microsoft Visual Studio 2008正式版。

      伺服器端開發B/S維護系統;C/S端開發用戶端Winform(WPF)應用程式;實作用戶端winform系統業務操作會相應的同步到伺服器中;反之,伺服器端的業務系統操作也會自動同步到下屬不同的用戶端Compact資料庫中。

     架構設計:

     1、伺服器端系統架構設計采用MVPC架構:Web Client Software Factory

     2、用戶端業務系統架構設計也MVPC架構:Smart Client Software Factory

     優點:用戶端子產品代碼很容易移植到伺服器端asp.net開發的業務系統;解決用戶端與伺服器端應用子產品重複,減少代碼量;

如果采用Microsoft SQL Server Compact 3.5用戶端資料庫(Sdf擴充名的資料庫檔案),不必安裝Microsoft SQL Server 2005資料庫,客戶隻需要一個安裝包,即可實作用戶端的應用系統安裝,更加友善靈活的使用者體驗。

 Sync Services for ADO.NET provider 技術應用場景

    * C\S結構的離線應用,在本地緩存中心資料庫中的部分資料(極特别的情況下會緩存全部)。應用程式使用緩存的資料,并在特定的時間把一批更改上次到中心資料庫。

    * 協作應用,應用程式隻使用本地資料,并周期性的與其他參與者進行 Peer-to-Peer 的同步。資料庫雙向同步:Feature實作用戶端C/S系統資料庫資料與伺服器端B/S管理系統資料庫資料進行資料雙向同步;

Use case:使用者在用戶端針對本地資料庫系統進行增加、删除、修改等操作,如果線上聯網狀态,資料自動同步到遠端SQL Server資料庫;有管理權限使用者,登入B/S系統進行增加、删除、修改等操作,SQL Server資料自動同步到用戶端資料庫。

功能實作Server與Client端進行資料操作(增加、删除),都能很好的進行資料雙向同步;

Sync Demo源碼程式截圖如下:

Compact資料庫同步技術功能

Microsoft SQL Server Compact 3.5 (以前稱為 Microsoft SQL Server 2005 Mobile Edition)是一種壓縮資料庫,很适合嵌入到移動應用程式和桌面應用程式中。Microsoft SQL Server Compact 3.5 為開發本機和托管應用程式的開發人員提供了與其他 SQL Server 版本通用的程式設計模型。SQL Server Compact Edition 以很少的空間占用提供關系資料庫功能:健壯資料存儲、優化查詢處理器以及可靠、可縮放的連接配接。 

Microsoft Synchronization Services for ADO.NET 是一組 DLL,提供了一個可組構的 API。根據應用程式的體系結構和要求,可以使用提供的所有或部分元件。

Synchronization Services 實作了 SQL Server Compact 3.5 用戶端資料庫和伺服器資料庫或任何其他資料源(如以 XML 形式提供股票報價的服務)之間的同步。在同步兩個資料庫方面,Synchronization Services 支援使用為之提供了 ADO.NET 提供程式的任何伺服器資料庫的雙層和 N 層體系結構。

在對用戶端資料庫和其他類型的資料源進行同步方面,Synchronization Services 支援基于服務的體系結構。與雙層或 N 層體系結構相比,此體系結構需要編寫更多的應用程式代碼;但是,它不需要開發人員采取另一種不同的方式進行同步。

通過 Microsoft Visual Studio 2008 的Microsoft Synchronization Services for ADO.NET,可以通過雙層、N 層和基于服務的體系結構同步來自不同來源的資料。

Synchronization Services API 提供了一組用于在資料服務和本地存儲區之間同步資料的元件,而不是僅僅用于複制資料庫及其架構。應用程式正越來越多地用于移動用戶端,如便攜式計算機和裝置。由于這些移動用戶端與中央伺服器沒有連貫或可靠的網絡連接配接,是以對于這些應用程式而言,能夠在用戶端上使用資料的一份本地副本十分重要。同等重要的一點是:在網絡連接配接可用時,需要能夠将資料的本地副本與中央伺服器同步。Synchronization Services API 以 ADO.NET 資料通路 API 為藍本,提供了一種直覺的資料同步手段。Synchronization Services 對建構依靠連續網絡連接配接的應用程式這一工作進行了合乎邏輯的擴充,使我們得以針對斷續連接配接的網絡環境開發應用程式。

相關代碼:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Data.SqlClient;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.IO;

using Microsoft.Synchronization;

using Microsoft.Synchronization.Data;

using Microsoft.Synchronization.Data.SqlServerCe;

using Microsoft.Synchronization.Data.Server;

using System.Data.SqlServerCe;

namespace SynchrnoizationDemo

{

    public partial class frmMain : Form

    {

        #region 定義正常資料區

        private const string strServerListTableName = "ServerList";

        private const string strUserListTableName = "UserList";

        private const string strCreationTrackingColumn = @"create_timestamp";

        private const string strUpdateTrackingColumn = @"update_timestamp";

        private const string strDeletionTrackingColumn = @"update_timestamp";

        private const string strUpdateOriginatorIdColumn = @"update_originator_id";

        private const string strNoClientDBWarnString = "用戶端資料庫不存在,請首先執行 雙向同步 指令";

        #endregion

        #region 定義變量區

        private string strServerIP = @"(local)";

        private string strServerDataBaseName = "SyncDemo";

        private string strLoginUser = "sa";

        private string strLoginPassWord = "sa";

        private string strServerConnectString = "";

        private string strClientDataBasename = Application.StartupPath + @"\ClientDB.sdf";

        private string strClientConnectString = "";

        private bool blnOperationClient = true;

        private string strOperationTable = strServerListTableName;

        public frmMain()

        {

            InitializeComponent();

        }

        private void frmMain_Load(object sender, EventArgs e)

            //  初始化相關參數

            SetEnvironmentValue();

        #region 通用方法

        /// <summary>

        /// 擷取相關參數

        /// </summary>

        private void GetEnvironmentValue()

            strServerIP = txtServerIP.Text.Trim();

            strServerDataBaseName = txtServerDataBaseName.Text.Trim();

            strLoginUser = txtLoginUser.Text.Trim();

            strLoginPassWord = txtLoginPassWord.Text.Trim();

            strServerConnectString = "Data Source="+strServerIP+";Initial Catalog="+strServerDataBaseName+";User ID="+strLoginUser+";Password="+strLoginPassWord+";";

            strClientDataBasename = txtClientDataBaseFileName.Text.Trim();

            strClientConnectString = "Data Source=" + strClientDataBasename;

            blnOperationClient = rbClient.Checked;

            strOperationTable = tabMain.SelectedIndex == 0 ? strServerListTableName : strUserListTableName;

        /// 顯示相關參數

        private void SetEnvironmentValue()

            txtServerIP.Text = strServerIP;

            txtServerDataBaseName.Text = strServerDataBaseName;

            txtLoginUser.Text = strLoginUser;

            txtLoginPassWord.Text = strLoginPassWord;

            txtClientDataBaseFileName.Text = strClientDataBasename;

            if (blnOperationClient)

            {

                rbClient.Checked=true;

                rbServer.Checked=false;

            }

            else

                rbServer.Checked=true;

                rbClient.Checked=false;

        /// 檢查用戶端資料庫是否存在

        /// <returns></returns>

        private Boolean CheckClientDb()

            //這裡預設已經調用了GetEnvironmentValue函數

            return File.Exists(strClientDataBasename);

        #region 重新整理資料

        /// 重新整理調用

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void btRefersh_Click(object sender, EventArgs e)

            btRefersh.Enabled = false;

            //RefreshData(tabMain.SelectedTab.Text.Replace("表", ""));

            RefreshData(strServerListTableName);

            RefreshData(strUserListTableName);

            btRefersh.Enabled = true;

        /// 資料重新整理

        /// <param name="sTableName"></param>

        private void RefreshData(string sTableName)

            GetEnvironmentValue();

            string sSql = "Select * from " + sTableName;

            DataTable dtQuery;

            try

                #region 讀取伺服器端

                SqlDataAdapter serverLoadAdapter = new SqlDataAdapter(sSql, strServerConnectString);

                dtQuery = new DataTable();

                serverLoadAdapter.Fill(dtQuery);

                RemoveServerTrackingColumns(dtQuery);

                switch (sTableName)

                {

                    case strServerListTableName:

                        dgSLServer.DataSource = dtQuery;

                        break;

                    case strUserListTableName:

                        dgULServer.DataSource = dtQuery;

                }

                #endregion

                #region 讀取用戶端

                if (CheckClientDb())

                    SqlCeDataAdapter clientLoadAdapter = new SqlCeDataAdapter(sSql,strClientConnectString);

                    dtQuery = new DataTable();

                    clientLoadAdapter.Fill(dtQuery);

                    RemoveClientTrackingColumns(dtQuery);

                    switch (sTableName)

                    {

                        case strServerListTableName:

                            dgSLClient.DataSource = dtQuery;

                            break;

                        case strUserListTableName:

                            dgULClient.DataSource = dtQuery;

                    }

                else

                    MessageBox.Show(strNoClientDBWarnString);

            catch (System.Exception e)

                MessageBox.Show(e.Message);

        /// 去除用戶端表中不需要顯示的列

        /// <param name="dataTable"></param>

        private static void RemoveClientTrackingColumns(DataTable dataTable)

            if (dataTable.Columns.Contains("__sysInsertTxBsn"))

                dataTable.Columns.Remove("__sysInsertTxBsn");

            if (dataTable.Columns.Contains("__sysChangeTxBsn"))

                dataTable.Columns.Remove("__sysChangeTxBsn");

        /// 去除伺服器端表中不需要顯示的列

        private static void RemoveServerTrackingColumns(DataTable dataTable)

            if (dataTable.Columns.Contains("update_timestamp"))

                dataTable.Columns.Remove("update_timestamp");

            if (dataTable.Columns.Contains("create_timestamp"))

                dataTable.Columns.Remove("create_timestamp");

            if (dataTable.Columns.Contains("update_originator_id"))

                dataTable.Columns.Remove("update_originator_id");

        #region 同步資料

        /// 同步資料調用

        private void btSync_Click(object sender, EventArgs e)

            btSync.Enabled = false;

            panOperation.Enabled = false;

            SynchronizeData();

            btRefersh_Click(null, null);

            panOperation.Enabled = true;

            btSync.Enabled = true;

        /// 資料同步

        private void SynchronizeData()

            #region 初始化

            SyncAgent syncAgent = new SyncAgent();

            #endregion

                #region 伺服器端準備

                DbServerSyncProvider serverSyncProvider = new DbServerSyncProvider();

                SqlConnection serverConnection = new SqlConnection(strServerConnectString);

                serverSyncProvider.Connection = serverConnection;

                syncAgent.RemoteProvider = serverSyncProvider;

                #region 用戶端準備

                if (!CheckClientDb())

                    SqlCeEngine clientEngine = new SqlCeEngine(strClientConnectString);

                    clientEngine.CreateDatabase();

                    clientEngine.Dispose();

                SqlCeClientSyncProvider clientSyncProvider = new SqlCeClientSyncProvider(strClientConnectString);

                syncAgent.LocalProvider = clientSyncProvider;

                #region SyncTable和SyncGroup準備

                //  ServerList表

                SyncTable tableServerList = new SyncTable(strServerListTableName);

                tableServerList.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;

                tableServerList.SyncDirection = SyncDirection.Bidirectional;

                //  UserList表

                SyncTable tableUserList = new SyncTable(strUserListTableName);

                tableUserList.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;

                tableUserList.SyncDirection = SyncDirection.Bidirectional;

                //  SyncGroup

                SyncGroup syncGroup = new SyncGroup("SyncDemo");

                tableServerList.SyncGroup = syncGroup;

                tableUserList.SyncGroup = syncGroup;

                syncAgent.Configuration.SyncTables.Add(tableServerList);

                syncAgent.Configuration.SyncTables.Add(tableUserList);

                #region SyncAdapter準備

                #region ServerList準備

                SqlSyncAdapterBuilder ServerListBuilder = new SqlSyncAdapterBuilder();

                ServerListBuilder.Connection = serverConnection;

                ServerListBuilder.SyncDirection = SyncDirection.Bidirectional;

                // 主表及其相關列

                ServerListBuilder.TableName = strServerListTableName;

                ServerListBuilder.DataColumns.Add("ServerID");

                ServerListBuilder.DataColumns.Add("ServerIP");

                ServerListBuilder.DataColumns.Add("ServerBuyTime");

                // tombstone表及其相關列

                ServerListBuilder.TombstoneTableName = strServerListTableName + "_tombstone";

                ServerListBuilder.TombstoneDataColumns.Add("ServerID");

                ServerListBuilder.TombstoneDataColumns.Add("ServerIP");

                ServerListBuilder.TombstoneDataColumns.Add("ServerBuyTime");

                // 相關的跟蹤列

                ServerListBuilder.CreationTrackingColumn = strCreationTrackingColumn;

                ServerListBuilder.UpdateTrackingColumn = strUpdateTrackingColumn;

                ServerListBuilder.DeletionTrackingColumn = strDeletionTrackingColumn;

                ServerListBuilder.UpdateOriginatorIdColumn = strUpdateOriginatorIdColumn;

                SyncAdapter ServerListSyncAdapter = ServerListBuilder.ToSyncAdapter();

                ((SqlParameter)ServerListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_last_received_anchor"]).DbType = DbType.Binary;

                ((SqlParameter)ServerListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_new_received_anchor"]).DbType = DbType.Binary;

                serverSyncProvider.SyncAdapters.Add(ServerListSyncAdapter);

                #region UserList準備

                SqlSyncAdapterBuilder UserListBuilder = new SqlSyncAdapterBuilder();

                UserListBuilder.SyncDirection = SyncDirection.Bidirectional;

                UserListBuilder.Connection = serverConnection;

                UserListBuilder.TableName = strUserListTableName;

                UserListBuilder.DataColumns.Add("UserID");

                UserListBuilder.DataColumns.Add("UserName");

                UserListBuilder.DataColumns.Add("UserPW");

                UserListBuilder.TombstoneTableName = strUserListTableName + "_tombstone";

                UserListBuilder.TombstoneDataColumns.Add("UserID");

                UserListBuilder.TombstoneDataColumns.Add("UserName");

                UserListBuilder.TombstoneDataColumns.Add("UserPW");

                UserListBuilder.CreationTrackingColumn = strCreationTrackingColumn;

                UserListBuilder.UpdateTrackingColumn = strUpdateTrackingColumn;

                UserListBuilder.DeletionTrackingColumn = strDeletionTrackingColumn;

                UserListBuilder.UpdateOriginatorIdColumn = strUpdateOriginatorIdColumn;

                SyncAdapter UserListSyncAdapter = UserListBuilder.ToSyncAdapter();

                ((SqlParameter)UserListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_last_received_anchor"]).DbType = DbType.Binary;

                ((SqlParameter)UserListSyncAdapter.SelectIncrementalInsertsCommand.Parameters["@sync_new_received_anchor"]).DbType = DbType.Binary;

                serverSyncProvider.SyncAdapters.Add(UserListSyncAdapter);

                #region 資料同步

                SqlCommand anchorCmd = new SqlCommand();

                anchorCmd.CommandType = CommandType.Text;

                anchorCmd.CommandText = "Select @" + SyncSession.SyncNewReceivedAnchor + "= @@DBTS";// " = @@DBTS";

                anchorCmd.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp).Direction = ParameterDirection.Output;

                serverSyncProvider.SelectNewAnchorCommand = anchorCmd;

                SqlCommand clientIdCmd = new SqlCommand();

                clientIdCmd.CommandType = CommandType.Text;

                clientIdCmd.CommandText = "SELECT @" + SyncSession.SyncOriginatorId + " = 1";

                clientIdCmd.Parameters.Add("@" + SyncSession.SyncOriginatorId, SqlDbType.Int).Direction = ParameterDirection.Output;

                serverSyncProvider.SelectClientIdCommand = clientIdCmd;

                syncAgent.Synchronize();

                #endregion   

            catch (Exception e)

        #region 增量操作

        #region 産生各種随機資料

        /// 産生随機IP

        private string ReturnRandomIP(Random rand)

            string sRe = "192.168." + rand.Next(0, 256).ToString() + "." + rand.Next(0, 256).ToString();

            return sRe;

        /// 産生随機ID

        /// <param name="rand"></param>

        private string ReturnRandomID(Random rand)

            return rand.Next((int)(DateTime.Now.ToFileTime() % 10000)).ToString();

        /// 産生随機字元

        private char ReturnRandomChar(Random rand)

            int ret = rand.Next(122);

            while (ret < 48 || (ret > 57 && ret < 65) || (ret > 90 && ret < 97))

                ret = rand.Next(122);

            return (char)ret;

        /// 産生随機字元串

        private string ReturnRandomString(Random rand)

            int length = 10;

            StringBuilder sb = new StringBuilder(length);

            for (int i = 0; i < length; i++)

                sb.Append(ReturnRandomChar(rand));

            return sb.ToString();

        /// 傳回一個随機操作的ID

        private string ReturnRandomUpdateOrDeleteID(Random rand)

            DataTable dtRef = new DataTable();

            if (strOperationTable == strServerListTableName)

                if (blnOperationClient)

                    dtRef = (DataTable)dgSLClient.DataSource;

                    dtRef = (DataTable)dgSLServer.DataSource;

                    dtRef = (DataTable)dgULClient.DataSource;

                    dtRef = (DataTable)dgULServer.DataSource;

            int iTotalRows = dtRef.Rows.Count;

            return dtRef.Rows[rand.Next(0, iTotalRows)][0].ToString();

        /// 具體執行增删改的操作

        /// <param name="CommandString"></param>

        private void ExecuteOperation(string CommandString)

                #region 用戶端操作

                    SqlCeConnection conn = new SqlCeConnection(strClientConnectString);

                    SqlCeCommand cmd = new SqlCeCommand();

                    cmd.Connection = conn;

                    cmd.CommandText = CommandString.ToString();

                    try

                        conn.Open();

                        cmd.ExecuteNonQuery();

                    catch (System.Exception exp)

                        MessageBox.Show(exp.Message);

                    finally

                        conn.Close();

                #region 伺服器端操作

                SqlConnection conn = new SqlConnection(strServerConnectString);

                SqlCommand cmd = new SqlCommand();

                cmd.Connection = conn;

                cmd.CommandText = CommandString;

                try

                    conn.Open();

                    cmd.ExecuteNonQuery();

                catch (System.Exception exp)

                    MessageBox.Show(exp.Message);

                finally

                    conn.Close();

            RefreshData(strOperationTable);

        /// 更新

        private void btUpdate_Click(object sender, EventArgs e)

            btUpdate.Enabled = false;

            Random rand = new Random();

            StringBuilder sSql = new StringBuilder();

                //update ServerList set ServerIP='123.12',ServerBuyTime=GetDate() where ServerID=1

                sSql.Append(@" update ServerList ");

                sSql.Append(@" set ServerIP='"+ReturnRandomIP(rand)+"', ");       //ServerIP

                sSql.Append(@" ServerBuyTime=GetDate() ");      //ServerBuyTime

                sSql.Append(@" where ServerID="+ReturnRandomUpdateOrDeleteID(rand)+" ");    //UpdateID

                sSql.Append(@" update UserList ");

                sSql.Append(@" set UserName = '"+ReturnRandomString(rand)+"', ");         //UserName

                sSql.Append(@" UserPW = '"+ReturnRandomString(rand)+"' ");                //UserPW

                sSql.Append(@" where UserID="+ReturnRandomUpdateOrDeleteID(rand)+" ");    //UpdateID

            ExecuteOperation(sSql.ToString()); 

            btUpdate.Enabled = true;

        /// 添加

        private void btAdd_Click(object sender, EventArgs e)

            btAdd.Enabled = false;

                sSql.Append(@" insert into ServerList (ServerID, ServerIP, ServerBuyTime) ");

                sSql.Append(@" values( ");

                sSql.Append(@" " + ReturnRandomID(rand) + ", ");   //ServerID

                sSql.Append(@" '" + ReturnRandomIP(rand) + "', "); //ServerIP

                sSql.Append(@" GetDate() ");    //ServerBuyTime

                sSql.Append(@" ) ");

                sSql.Append(@" insert into UserList (UserID, UserName, UserPW) ");

                sSql.Append(@" " + ReturnRandomID(rand) + ", ");       //UserID

                sSql.Append(@" '" + ReturnRandomString(rand) + "', ");    //UserName

                sSql.Append(@" '" + ReturnRandomString(rand) + "' ");     //UserPW

            ExecuteOperation(sSql.ToString());

            btAdd.Enabled = true;

        /// 删除

        private void btDelete_Click(object sender, EventArgs e)

            btDelete.Enabled = false;

                sSql.Append(@"delete from ServerList where ServerID="+ReturnRandomUpdateOrDeleteID(rand));

                sSql.Append(@"delete from UserList where UserID="+ReturnRandomUpdateOrDeleteID(rand));

            btDelete.Enabled = true;

    }

}

Demo與源碼下載下傳: 

<a href="http://blog.crfly.com/file.axd?file=code.zip">code.zip (68.69 kb)</a>

<a href="http://blog.crfly.com/file.axd?file=release.zip">release.zip (12.88 kb)</a>