式并对相应的生成文件进行了粗略的分析。今天开始把话题深入一下,聊一聊MSF的冲突检测和处理。
这里所说的冲突(Conflict) 主要是指当客户端与服务端数据在进行CUD时,所操作的数据同步
期间发生了错误,如通常所说的约束冲突(主键重复),以及在同步时多个节点(客户端)上更改了
同一行,或服务端删除该行而其它节点却更新了该行便存在冲突等。当然在设计应用程序时应避免产
生冲突(比如可通过筛选行和列等方式做到这一点),因为冲突的检测和解决会增加应用程序的复杂
性,增加处理负担和网络流量。
当前MSF本身对冲突这种问题也是提供了一些基础层面上的支持的。下面是其定义的几种冲突的
基本类型(Conflict.ConflictType),它们包括:
ClientInsertServerInsert: 客户端和服务器都插入了具有相同主键值的行。此操作导致了主键冲突。
ClientDeleteServerUpdate: 客户端删除了服务器更新的行。
ClientUpdateServerDelete: 服务器删除了客户端更新的行。
ClientUpdateServerUpdate: 客户端和服务器更新了相同的行。
ErrorsOccurred: 客户端或服务器存储区(通常为数据库)在应用更改时引发了一个异常。
Unknown: 客户端同步时提供程序能对所遇到的所有冲突进行分类,但服务器同步时不能提供程序。
ClientDeleteServerUpdate: 客户端删除了服务器更新的行。
ClientUpdateServerDelete: 服务器删除了客户端更新的行。
ClientUpdateServerUpdate: 客户端和服务器更新了相同的行。
ErrorsOccurred: 客户端或服务器存储区(通常为数据库)在应用更改时引发了一个异常。
Unknown: 客户端同步时提供程序能对所遇到的所有冲突进行分类,但服务器同步时不能提供程序。
如果要截获冲突,可以通过对相应的SyncProvider对象中ApplyChangeFailed事件绑定来完成为,
比如下面的示例代码:
syncClientSyncProvider.ApplyChangeFailed +=
new EventHandler<ApplyChangeFailedEventArgs>(syncClientSyncProvider_ApplyChangeFailed);
new EventHandler<ApplyChangeFailedEventArgs>(syncClientSyncProvider_ApplyChangeFailed);
上面代码中的“ApplyChangeFailed”事件,会在无法在客户端应用某行后发生。而我们可以在相
应的绑写事件syncSeverSyncProvider_ApplyChangeFailed中对各种冲突进行相应处理,示例如下:
void syncClientSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
{
switch (e.Conflict.ConflictType)
{
case ConflictType.ClientInsertServerInsert:
{
e.Action = ApplyAction.RetryWithForceWrite;
break;
}
case ConflictType.ClientUpdateServerUpdate:
{
e.Action = ApplyAction.RetryWithForceWrite;
break;
}
case ConflictType.ClientUpdateServerDelete:
{
e.Action = ApplyAction.Continue;
break;
}
case ConflictType.ClientDeleteServerUpdate:
{
e.Action = ApplyAction.RetryApplyingRow;
break;
}
}
}
{
switch (e.Conflict.ConflictType)
{
case ConflictType.ClientInsertServerInsert:
{
e.Action = ApplyAction.RetryWithForceWrite;
break;
}
case ConflictType.ClientUpdateServerUpdate:
{
e.Action = ApplyAction.RetryWithForceWrite;
break;
}
case ConflictType.ClientUpdateServerDelete:
{
e.Action = ApplyAction.Continue;
break;
}
case ConflictType.ClientDeleteServerUpdate:
{
e.Action = ApplyAction.RetryApplyingRow;
break;
}
}
}
上面代码分别针对不同的冲突类型绑定了如下的处理动作(Action):
ApplyAction: 指定在同步期间无法应用某行时用于处理该行的选项,包括如下类型值:
Continue 忽略冲突并继续执行同步。并将该行添加到 SyncConflict 中定义的冲突列表中(这是默
认行为)。
RetryApplyingRow 重新尝试应用该行。重试操作将失败,如果没有通过更改存在冲突的行之一
(或二者)来解决导致冲突的原因,将再次引发该事件。
RetryWithForceWrite 强制应用该行。
Continue 忽略冲突并继续执行同步。并将该行添加到 SyncConflict 中定义的冲突列表中(这是默
认行为)。
RetryApplyingRow 重新尝试应用该行。重试操作将失败,如果没有通过更改存在冲突的行之一
(或二者)来解决导致冲突的原因,将再次引发该事件。
RetryWithForceWrite 强制应用该行。
这样就简单实现了冲突类型的检测和后续处理(Action)。在之前的文章中,我们了解到在客户端和
服务端会提供各自的SyncProvider,在本DEMO中,客户端为BiDirectSyncDataClientSyncProvider,
服务端为BiDirectSyncDataServerSyncProvider。
我们可以通过对相应的SyncProvider定义上面的ApplyChangeFailed事件来检测在同步期间发生的
客户端或服务端的冲突。比如上面的事件绑定就是在ClientSyncProvider的ApplyChangeFailed事件上
绑写的。下面以ServerSyncProvider的ApplyChangeFailed绑定来实现在服务器上应用某行失败后对相
应的冲突进行处理:
void syncSeverSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
{
Msg.Text = "";
switch (e.Conflict.ConflictType)
{
case ConflictType.ClientDeleteServerUpdate:
{
//注:可通过设置客户端provider的ConflictResolver属性ConflictResolver.ServerWins
//来实现下面的ApplyAction.Continue语句的功能。
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientDeleteServerUpdate(客户端删除/服务端更新)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
case ConflictType.ClientUpdateServerUpdate:
{
//注:对于client-update/server-update冲突类型,会通过弹出框的方式来让用户选择解决方案.
//
//因为冲突可能有多行,而当前方式只提供单行解决方式。
if (e.Conflict.ServerChange.Rows.Count > 1)
{
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerUpdate(客户端更新/服务端更新)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端"r"n";
Msg.Text += "***********************************"r"n";
}
else
{
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerUpdate(客户端更新/服务端更新)."r"n";
Msg.Text += "冲突内容如下:"r"n";
Msg.Text += "***********************************"r"n";
//从冲突对象中获取冲突修改并进行显示. 当前冲突对象持有一个修改的拷贝。
//修改这个冲突对象不会被最终采用。如果要做最终修改,可以使用Context 对象,
//将会在下面代码中给出示例
DataTable conflictingServerChange = e.Conflict.ServerChange;
DataTable conflictingClientChange = e.Conflict.ClientChange;
int serverColumnCount = conflictingServerChange.Columns.Count;
int clientColumnCount = conflictingClientChange.Columns.Count;
Msg.Text += "服务端行:"r"n|";
//Display the server row.
for (int i = 0; i < serverColumnCount; i++)
{
Msg.Text += conflictingServerChange.Rows[0][i] + " | ";
}
Msg.Text += ""r"n客户端行: "r"n|";
//Display the client row.
for (int i = 0; i < clientColumnCount; i++)
{
Msg.Text += conflictingClientChange.Rows[0][i] + " | ";
}
Msg.Text += ""r"n";
//显示弹出提示框.
DialogResult dialogResult = MessageBox.Show(" 是==>服务端优先,
"r"n 否==>客户端优先,"r"n 取消==>自定义方案",
"冲突解决方案", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);
switch (dialogResult)
{
case DialogResult.Yes:
e.Action = ApplyAction.Continue;
Msg.Text += "服务端数据将被写入到客户端."r"n";
break;
case DialogResult.No:
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户端修改强制写入服务端."r"n";
break;
case DialogResult.Cancel:
//提供一个自定义方案来获取每个冲突列并将其列中的内容组合应用于客户端和服务端
//该方案只是讨论了如何使用不同方式来与同步期间的冲突数据进行交互
//
//首先从客户端表中获得冲突行的id,并添加到GUIDs列表中.稍后将使用这个列表来更
//新服务端的相应行.
int pid = (int)conflictingClientChange.Rows[0]["pid"];
_updateConflictGuids.Add(pid);
//创建一个dictionary来保存当前列的序号和相应的冲突值.
Dictionary<int, string> conflictingColumns = new Dictionary<int, string>();
string combinedColumnValue;
//找出那些列在 client 端和 server 端是不同的.
for (int i = 0; i < clientColumnCount; i++)
{
if (conflictingClientChange.Rows[0][i].ToString() !=
conflictingServerChange.Rows[0][i].ToString())
{
//如果找出不同列,则组合相应的client 和 server 值,并在中间写上 "| conflict |" .
combinedColumnValue = conflictingClientChange.Rows[0][i] + " | conflict | " +
conflictingServerChange.Rows[0][i];
conflictingColumns.Add(i, combinedColumnValue);
}
}
//遍历Context 对象的数据行, 该对象暴露出了从客户端上传的修改集合(the set of changes)。
//Note: 当 ApplyChangeFailed 事件是绑定在client provider上时,这个集合是从服务端下载的修改集合
DataTable allClientChanges = e.Context.DataSet.Tables["dnt_posts1"];
int allClientRowCount = allClientChanges.Rows.Count;
int allClientColumnCount = allClientChanges.Columns.Count;
for (int i = 0; i < allClientRowCount; i++)
{
//通过Conflict对象中的 GUID找出当前修改的行.
if (allClientChanges.Rows[i].RowState == DataRowState.Modified &&
(int)allClientChanges.Rows[i]["pid"] == pid)
{
//遍历列并检查是否当前列位于conflictingColumns dictionary中.
//如果在里面,则更新Context对象中当前allClientChanges的值.
for (int j = 0; j < allClientColumnCount; j++)
{
if (conflictingColumns.ContainsKey(j))
{
allClientChanges.Rows[i][j] = conflictingColumns[j].Split('|')[0].ToLower();
}
}
}
}
//我们可以使用ChangesApplied 事件去设置其余的字段,用于对应服务器值当前的值
//(参见 SampleServerSyncProvider_ChangesApplied).
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户修改强制写入到服务端."r"n";
break;
default:
Msg.Text += "无效的方案选项."r"n";
break;
}
}
break;
}
case ConflictType.ClientUpdateServerDelete:
{
//对于client-update/server-delete冲突类型,会将客户端修改强制写入服务端.
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerDelete(客户端更新/服务端删除)."r"n";
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户修改强制写入到服务端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
case ConflictType.ClientInsertServerInsert:
{
//与ClientDeleteServerUpdate类型相似,在当前情况下,我们可以通过在client provider中设置
//ConflictResolver.FireEvent 和 RetryWithForceWrite来定义冲突处理方式.这与
//ConflictResolver.ServerWins相同.
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientInsertServerInsert(客户端插入/服务端插入)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
}
}
{
Msg.Text = "";
switch (e.Conflict.ConflictType)
{
case ConflictType.ClientDeleteServerUpdate:
{
//注:可通过设置客户端provider的ConflictResolver属性ConflictResolver.ServerWins
//来实现下面的ApplyAction.Continue语句的功能。
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientDeleteServerUpdate(客户端删除/服务端更新)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
case ConflictType.ClientUpdateServerUpdate:
{
//注:对于client-update/server-update冲突类型,会通过弹出框的方式来让用户选择解决方案.
//
//因为冲突可能有多行,而当前方式只提供单行解决方式。
if (e.Conflict.ServerChange.Rows.Count > 1)
{
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerUpdate(客户端更新/服务端更新)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端"r"n";
Msg.Text += "***********************************"r"n";
}
else
{
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerUpdate(客户端更新/服务端更新)."r"n";
Msg.Text += "冲突内容如下:"r"n";
Msg.Text += "***********************************"r"n";
//从冲突对象中获取冲突修改并进行显示. 当前冲突对象持有一个修改的拷贝。
//修改这个冲突对象不会被最终采用。如果要做最终修改,可以使用Context 对象,
//将会在下面代码中给出示例
DataTable conflictingServerChange = e.Conflict.ServerChange;
DataTable conflictingClientChange = e.Conflict.ClientChange;
int serverColumnCount = conflictingServerChange.Columns.Count;
int clientColumnCount = conflictingClientChange.Columns.Count;
Msg.Text += "服务端行:"r"n|";
//Display the server row.
for (int i = 0; i < serverColumnCount; i++)
{
Msg.Text += conflictingServerChange.Rows[0][i] + " | ";
}
Msg.Text += ""r"n客户端行: "r"n|";
//Display the client row.
for (int i = 0; i < clientColumnCount; i++)
{
Msg.Text += conflictingClientChange.Rows[0][i] + " | ";
}
Msg.Text += ""r"n";
//显示弹出提示框.
DialogResult dialogResult = MessageBox.Show(" 是==>服务端优先,
"r"n 否==>客户端优先,"r"n 取消==>自定义方案",
"冲突解决方案", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);
switch (dialogResult)
{
case DialogResult.Yes:
e.Action = ApplyAction.Continue;
Msg.Text += "服务端数据将被写入到客户端."r"n";
break;
case DialogResult.No:
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户端修改强制写入服务端."r"n";
break;
case DialogResult.Cancel:
//提供一个自定义方案来获取每个冲突列并将其列中的内容组合应用于客户端和服务端
//该方案只是讨论了如何使用不同方式来与同步期间的冲突数据进行交互
//
//首先从客户端表中获得冲突行的id,并添加到GUIDs列表中.稍后将使用这个列表来更
//新服务端的相应行.
int pid = (int)conflictingClientChange.Rows[0]["pid"];
_updateConflictGuids.Add(pid);
//创建一个dictionary来保存当前列的序号和相应的冲突值.
Dictionary<int, string> conflictingColumns = new Dictionary<int, string>();
string combinedColumnValue;
//找出那些列在 client 端和 server 端是不同的.
for (int i = 0; i < clientColumnCount; i++)
{
if (conflictingClientChange.Rows[0][i].ToString() !=
conflictingServerChange.Rows[0][i].ToString())
{
//如果找出不同列,则组合相应的client 和 server 值,并在中间写上 "| conflict |" .
combinedColumnValue = conflictingClientChange.Rows[0][i] + " | conflict | " +
conflictingServerChange.Rows[0][i];
conflictingColumns.Add(i, combinedColumnValue);
}
}
//遍历Context 对象的数据行, 该对象暴露出了从客户端上传的修改集合(the set of changes)。
//Note: 当 ApplyChangeFailed 事件是绑定在client provider上时,这个集合是从服务端下载的修改集合
DataTable allClientChanges = e.Context.DataSet.Tables["dnt_posts1"];
int allClientRowCount = allClientChanges.Rows.Count;
int allClientColumnCount = allClientChanges.Columns.Count;
for (int i = 0; i < allClientRowCount; i++)
{
//通过Conflict对象中的 GUID找出当前修改的行.
if (allClientChanges.Rows[i].RowState == DataRowState.Modified &&
(int)allClientChanges.Rows[i]["pid"] == pid)
{
//遍历列并检查是否当前列位于conflictingColumns dictionary中.
//如果在里面,则更新Context对象中当前allClientChanges的值.
for (int j = 0; j < allClientColumnCount; j++)
{
if (conflictingColumns.ContainsKey(j))
{
allClientChanges.Rows[i][j] = conflictingColumns[j].Split('|')[0].ToLower();
}
}
}
}
//我们可以使用ChangesApplied 事件去设置其余的字段,用于对应服务器值当前的值
//(参见 SampleServerSyncProvider_ChangesApplied).
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户修改强制写入到服务端."r"n";
break;
default:
Msg.Text += "无效的方案选项."r"n";
break;
}
}
break;
}
case ConflictType.ClientUpdateServerDelete:
{
//对于client-update/server-delete冲突类型,会将客户端修改强制写入服务端.
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientUpdateServerDelete(客户端更新/服务端删除)."r"n";
e.Action = ApplyAction.RetryWithForceWrite;
Msg.Text += "再次尝试将客户修改强制写入到服务端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
case ConflictType.ClientInsertServerInsert:
{
//与ClientDeleteServerUpdate类型相似,在当前情况下,我们可以通过在client provider中设置
//ConflictResolver.FireEvent 和 RetryWithForceWrite来定义冲突处理方式.这与
//ConflictResolver.ServerWins相同.
Msg.Text += "***********************************"r"n";
Msg.Text += "发现冲突类型:ClientInsertServerInsert(客户端插入/服务端插入)."r"n";
e.Action = ApplyAction.Continue;
Msg.Text += "将服务器端更改应用到客户端."r"n";
Msg.Text += "***********************************"r"n";
break;
}
}
}
大家看到,除了ClientUpdateServerUpdate这种冲突之外,其余的冲突类型都与我们之前看到的
在syncClientSyncProvider_ApplyChangeFailed事件中处理相似。
这里之所以在ClientUpdateServerUpdate的"搞特殊"的原因是想演示给大家,如何在遇到冲突时
按自己的方案来解决冲突,而不是简单的使用那三种 ApplyAction 项来粗略的执行后续操作。因为自
定义方案会给我们提供更细的控制粒度。同时我们也可以了解到如何获取冲突表(包括服务端和客户
端),以及如何访问它们等等(详见注释)。
当然,MSF本身还提供了ConflictResolver对象来设置客户端同步期间,发生冲突时要执行的操
作。这个对象主要是为了简化上面通过事件绑写方式来检测处理冲突的方式,其属性值类型如下:
ResolveAction指定用于解决同步期间客户端上发生的任何冲突的选项。
ClientWins: 等效于将 ApplyAction 设置为 Continue。
ServerWins: 等效于将 ApplyAction 设置为 RetryWithForceWrite。
FireEvent: 激发 ApplyChangeFailed 事件(默认值),然后处理该事件。
ClientWins: 等效于将 ApplyAction 设置为 Continue。
ServerWins: 等效于将 ApplyAction 设置为 RetryWithForceWrite。
FireEvent: 激发 ApplyChangeFailed 事件(默认值),然后处理该事件。
下面即是相应的使用示例代码如下:
syncClientSyncProvider.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ClientWins;
syncClientSyncProvider.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.ClientWins;
syncClientSyncProvider.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ServerWins;
syncClientSyncProvider.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.ServerWins;
syncClientSyncProvider.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;
syncClientSyncProvider.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.ClientWins;
syncClientSyncProvider.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ServerWins;
syncClientSyncProvider.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.ServerWins;
syncClientSyncProvider.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;
当然,在代码中还包括了对事件ChangesApplied的绑定,该事件会在服务器处应用了同步组的所
有更改之后发生。 如下代码示例即显示了如何在ApplyChangeFailed事件之后(冲突处理完成之后),
对服务端数据进行进一步的处理,比如用于记录最近修改时间的字段更新等:
void syncServerSyncProvider_ChangesApplied(object sender, ChangesAppliedEventArgs e)
{
if (_updateConflictGuids.Count > 0)
{
SqlCommand updateTable = new SqlCommand();
updateTable.Connection = (SqlConnection)e.Connection;
updateTable.Transaction = (SqlTransaction)e.Transaction;
updateTable.CommandText = String.Empty;
for (int i = 0; i < _updateConflictGuids.Count; i++)
{
updateTable.CommandText += " UPDATE dnt_posts1 SET LastEditDate = GETDATE()
WHERE pid=" + _updateConflictGuids[i];
}
updateTable.ExecuteNonQuery();
}
}
{
if (_updateConflictGuids.Count > 0)
{
SqlCommand updateTable = new SqlCommand();
updateTable.Connection = (SqlConnection)e.Connection;
updateTable.Transaction = (SqlTransaction)e.Transaction;
updateTable.CommandText = String.Empty;
for (int i = 0; i < _updateConflictGuids.Count; i++)
{
updateTable.CommandText += " UPDATE dnt_posts1 SET LastEditDate = GETDATE()
WHERE pid=" + _updateConflictGuids[i];
}
updateTable.ExecuteNonQuery();
}
}
其实在我们的实际开发场景中,冲突的发生背景要比今天所说的还要复杂。所以如何避免冲突破
才是我们要首先考虑的。在MSF中就提供了一个方案, 即通过筛选行和列等方式做到这一点,下面即
是在SDK中所涉及到使用这种方式所能带来的好处,相关链接请点击这里:)
通过筛选数据,可以达到以下目的:
1.减少通过网络发送的数据量。
2.减少在客户端上需要的存储空间。
3.基于各个客户端需求提供自定义数据分区。
4.避免或减少冲突(如果客户端要更新数据),因为可以向不同的客户端发送不同的数据分区。
(不会出现两个客户端更新相同数据值的情况。)
1.减少通过网络发送的数据量。
2.减少在客户端上需要的存储空间。
3.基于各个客户端需求提供自定义数据分区。
4.避免或减少冲突(如果客户端要更新数据),因为可以向不同的客户端发送不同的数据分区。
(不会出现两个客户端更新相同数据值的情况。)
最后关于DEMO要说明的是,本DEMO文件BiDirectForm可以演示如下冲突类型:
ClientDeleteServerUpdate: 客户端删除了服务器更新的行。
ClientUpdateServerDelete: 服务器删除了客户端更新的行。
ClientUpdateServerUpdate: 客户端和服务器更新了相同的行。
只要在同步之前做好服务端和客户端数据的相应操作(更新或删除)即可。
好了,今天的内容就先到这里了:)
在下一篇中,将会介绍使用WCF进行远程数据同步,如果大家对这方面感兴趣,敬请关注:)
作者: daizhj, 代震军
Tags: 微软同步框架,ado.net,Conflict,冲突
网址: http://daizhj.cnblogs.com/
DEMO下载,请点击这里:)