一、(引言)
小菜通过一个简单的例子来说明如何进行并发处理,大家不用当心例子太简单反映不了问题。
小菜通过运行两个实例来模拟两个客户端。
一个阿猫、阿狗引发的故事:
数据库中记录:
客户端1:加载
客户端2:加载

客户端1在本地具有一份数据副本,名字暂时称为 副本1
客户端2在本地也具有一份数据副本,名字暂时称为 副本2
客户端1修改记录:AnimalID = 1 and AnimalType = "狗" 修改成 AnimalID = 1 and AnimalType = "阿狗"
修改的只是副本1中的数据,点击保存,将修改更新到服务器。
客户端1:
数据库中记录:
看来数据更改成功。
客户端2:
从图可以看出:客户端2并不知道客户端1已经把 AnimalType = "狗" 修改成了 AnimalType = "阿狗"
这时如果客户端2将 AnimalID = 1 and AnimalType = “狗”修改成了 AnimalID = 1 and AnimalType = “阿黄”
怎么办呢?(小菜也不知道怎么办,因为我们不知道用户想干什么)
用户目的是:将“狗”改成“阿黄”
如果用户知道了别人已经将“狗”改成了“阿狗”
用户可能会继续更改,也可能放弃更改。(这就出现了两种可能!!!)
所以我们应该让用户自己来选择:
以上解决并发处理的方案称为:开放式并发处理.
对应的保守式并发处理:
是当用户编辑数据库中的某行数据时,保守式并发会使此数据一直保持锁定.在锁定期间,其他用户不能更改数据,这样能够最高程度的保证数据的完整性,但是可用性低.
代码下载:https://files.cnblogs.com/a-peng/SmartClient_Chapter05.rar
(二)、分析
1、使用TableAdapter + 类型化DataSet 完成开放式并发处理
演化一:
新建Web服务:Server
添加数据集:DataSetAnimals(配置略,注意使用开放式并发 和 刷新数据表两个选项)
添加Web服务:DataService.asmx
代码如下:
public class DataService : System.Web.Services.WebService


{
private DataSetAnimalsTableAdapters.AnimalsTableAdapter m_daAnimals;

public DataService()

{
m_daAnimals = new DataSetAnimalsTableAdapters.AnimalsTableAdapter();
}

[WebMethod]
public DataSetAnimals GetAnimals()

{
DataSetAnimals dsAnimals = new DataSetAnimals();
m_daAnimals.Fill(dsAnimals.Animals);

return dsAnimals;
}

[WebMethod]
public DataSetAnimals UpdateAnimals(DataSetAnimals dsAnimals)

{
try

{
m_daAnimals.Update(dsAnimals.Animals);
}
catch (DBConcurrencyException)

{
// 更新失败,发生并发错误,dsAnimals中含有错误行信息.
return dsAnimals;
}

// 更新成功,返回最新数据 dsAnimals.Clear();
m_daAnimals.Fill(dsAnimals.Animals);

return dsAnimals;
}
}
添加Windows项目:Client
添加Web服务引用,名称:DataWS
添加窗体:MainForm
代码如下:
using System;
using System.Windows.Forms;

using Client.DataWS;

namespace Client


{
public partial class MainForm : Form

{
private DataService m_dataService; // Web服务代理
private DataSetAnimals m_dsAnimals;

public MainForm()

{
InitializeComponent();

m_dataService = new DataService();
}

private void btnLoad_Click(object sender, EventArgs e)

{
m_dsAnimals = m_dataService.GetAnimals();
this.dataGridView1.DataSource = m_dsAnimals.Animals.DefaultView; // DataGridView控件数据源绑定
}

private void btnSave_Click(object sender, EventArgs e)

{
UpdateAnimals();
}

private void UpdateAnimals()

{
DataSetAnimals dsAnimals = null;

try

{
dsAnimals = m_dataService.UpdateAnimals(m_dsAnimals);
}
catch

{ }

if (dsAnimals != null)

{
m_dsAnimals.Clear();
m_dsAnimals.Merge(dsAnimals);
}
}
}
}
这是我们的第一份代码,我们来试试并发时效果。
左边为客户端1,右边为客户端2。
客户端1将狗,猫,蛇 修改成 阿狗,阿猫,阿蛇,并保存,更新到数据库。
客户端2将狗,猫,蛇 修改成 小狗,小猫,小蛇,并保存,
效果图如下:
效果并不好,我们没有给用户做出选择的权利。
演化二:
修改主窗体中保存按钮响应代码:
private void btnSave_Click(object sender, EventArgs e)


{
do

{
UpdateAnimals();

if (m_dsAnimals.HasErrors) // 存在错误行

{
DataSetAnimals.AnimalsRow errorRow = null;
foreach (DataSetAnimals.AnimalsRow dr in m_dsAnimals.Animals.Rows)

{
if (dr.HasErrors)

{
errorRow = dr;
break;
}
}

// 使用乐观并行处理
CollisionForm cForm = new CollisionForm(errorRow);
cForm.ShowDialog();
}
else // 不存在错误行

{
return;
}

} while (true);
}
和演化一,一样的测试,效果如下:
到这时,我们就基本完成任务了。不过有哪些不足呢?
你要是注意看就会发现上面显示的,Previous Change的AnimalType为“狗”,而我们希望显示的是“阿狗”。
怎么办呢,我们通过m_errorRow[
"AnimalType", System.Data.DataRowVersion.Original].ToString();获取得原始只可能是“狗”。这就需要我们在Web服务上动手脚。
演化三:
修改DataService.asmx中的UpdateAnimals代码
[WebMethod]
public DataSetAnimals UpdateAnimals(DataSetAnimals dsAnimals)


{
try

{
m_daAnimals.Update(dsAnimals.Animals);
}
catch (DBConcurrencyException dbEx)

{
SqlConnection conn = new SqlConnection("Data Source=A-PENG;Initial Catalog=Test;User ID=sa;Password=password");
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "Select AnimalID,AnimalType From Animals Where AnimalID=@AnimalID";

SqlDataAdapter da = new SqlDataAdapter(cmd);
da.SelectCommand.Parameters.AddWithValue("@AnimalID", dbEx.Row["AnimalID"]);

DataSet ds = new DataSet();
da.Fill(ds);

if (ds.Tables[0].Rows.Count > 0)

{
DataRow proposedRow = dbEx.Row.Table.NewRow();
DataRow databaseRow = ds.Tables[0].Rows[0];

proposedRow.ItemArray = dbEx.Row.ItemArray;

dbEx.Row.Table.Columns["AnimalID"].ReadOnly = false;
dbEx.Row.ItemArray = databaseRow.ItemArray;
dbEx.Row.AcceptChanges();
dbEx.Row.ItemArray = proposedRow.ItemArray;
dbEx.Row.Table.Columns["AnimalID"].ReadOnly = true;

// 更新失败,发生并发错误,dsAnimals中含有错误行信息.
return dsAnimals;
}
else

{
dbEx.Row.Delete();
dbEx.Row.AcceptChanges();
}
}

// 更新成功,返回最新数据
dsAnimals.Clear();
m_daAnimals.Fill(dsAnimals.Animals);

return dsAnimals;
}
效果如下:
上面的代码有这么大魔力?
DataRow proposedRow = dbEx.Row.Table.NewRow();
DataRow databaseRow = ds.Tables[0].Rows[0];

proposedRow.ItemArray = dbEx.Row.ItemArray; // 将错误行全部信息拷贝到proposedRow中

dbEx.Row.Table.Columns["AnimalID"].ReadOnly = false;
dbEx.Row.ItemArray = databaseRow.ItemArray; // 将数据库中的行拷贝到错误行dbEx.Row中
dbEx.Row.AcceptChanges(); // 接受更改,将dbEx.Row的状态修改成UnChanged
dbEx.Row.ItemArray = proposedRow.ItemArray; // 恢复错误行信息
dbEx.Row.Table.Columns["AnimalID"].ReadOnly = true;
上面这几句好像在做无用功。改了又恢复,其实主要就是更改原始值为databaseRow.ItemArray。
2、不使用TableAdapter完成一样的效果。
DataService.asmx代码如下:
public class DataService : System.Web.Services.WebService


{
private const string m_connectionString = "Data Source=A-PENG;Initial Catalog=Test;User ID=sa;Password=password";
private SqlConnection m_conn;

private SqlDataAdapter m_daAnimals;
private SqlCommand m_selectAnimalsCommand;
private SqlCommand m_updateAnimalCommand;
private SqlCommand m_insertAnimalCommand;
private SqlCommand m_deleteAnimalCommand;

public DataService ()

{
m_conn = new SqlConnection(m_connectionString);

m_daAnimals = new SqlDataAdapter();

// Select
m_selectAnimalsCommand = new SqlCommand();
m_selectAnimalsCommand.Connection = m_conn;
m_selectAnimalsCommand.CommandText = "GetAnimals";
m_selectAnimalsCommand.CommandType = CommandType.StoredProcedure;

// Update
m_updateAnimalCommand = new SqlCommand();
m_updateAnimalCommand.Connection = m_conn;
m_updateAnimalCommand.CommandText = "UpdateAnimal";
m_updateAnimalCommand.CommandType = CommandType.StoredProcedure;
m_updateAnimalCommand.Parameters.Add(new SqlParameter("@AnimalID", SqlDbType.Int, 4, "AnimalID"));
m_updateAnimalCommand.Parameters.Add(new SqlParameter("@AnimalType", SqlDbType.VarChar, 50, "AnimalType"));
m_updateAnimalCommand.Parameters.Add(new SqlParameter("@Original_AnimalType", SqlDbType.VarChar, 50, ParameterDirection.Input, false, 0, 0, "AnimalType", DataRowVersion.Original, null));

// Insert
m_insertAnimalCommand = new SqlCommand();
m_insertAnimalCommand.Connection = m_conn;
m_insertAnimalCommand.CommandText = "InsertAnimal";
m_insertAnimalCommand.CommandType = CommandType.StoredProcedure;
m_insertAnimalCommand.Parameters.Add(new SqlParameter("@AnimalType", SqlDbType.VarChar, 50, "AnimalType"));

// Delete
m_deleteAnimalCommand = new SqlCommand();
m_deleteAnimalCommand.Connection = m_conn;
m_deleteAnimalCommand.CommandText = "DeleteAnimal";
m_deleteAnimalCommand.CommandType = CommandType.StoredProcedure;
m_deleteAnimalCommand.Parameters.Add(new SqlParameter("@AnimalID", SqlDbType.Int, 4, "AnimalID"));
m_deleteAnimalCommand.Parameters.Add(new SqlParameter("@Original_AnimalType", SqlDbType.VarChar, 50, ParameterDirection.Input, false, 0, 0, "AnimalType", DataRowVersion.Original, null));

// Initial Adapter
m_daAnimals.SelectCommand = m_selectAnimalsCommand;
m_daAnimals.UpdateCommand = m_updateAnimalCommand;
m_daAnimals.InsertCommand = m_insertAnimalCommand;
m_daAnimals.DeleteCommand = m_deleteAnimalCommand;

}

[WebMethod]
public DataSetAnimals GetAnimals()

{
DataSetAnimals dsAnimals = new DataSetAnimals();
m_daAnimals.Fill(dsAnimals.Animals);

return dsAnimals;
}

[WebMethod]
public DataSetAnimals UpdateAnimals(DataSetAnimals dsAnimals)

{
do

{
try

{
m_daAnimals.Update(dsAnimals.Animals);
break;
}
catch (DBConcurrencyException dbEx)

{
SqlCommand cmd = m_conn.CreateCommand();
cmd.CommandText = "GetAnimal";
cmd.CommandType = CommandType.StoredProcedure;

SqlDataAdapter da = new SqlDataAdapter(cmd);
da.SelectCommand.Parameters.AddWithValue("@AnimalID", dbEx.Row["AnimalID"]);

DataSet ds = new DataSet();
da.Fill(ds);

if (ds.Tables[0].Rows.Count > 0)

{
DataRow proposedRow = dbEx.Row.Table.NewRow();
DataRow databaseRow = ds.Tables[0].Rows[0];

proposedRow.ItemArray = dbEx.Row.ItemArray;

dbEx.Row.Table.Columns["AnimalID"].ReadOnly = false;
dbEx.Row.ItemArray = databaseRow.ItemArray;
dbEx.Row.AcceptChanges();
dbEx.Row.ItemArray = proposedRow.ItemArray;
dbEx.Row.Table.Columns["AnimalID"].ReadOnly = true;

// 更新失败,返回失败数据,含错误行
return dsAnimals;
}
else

{
dbEx.Row.Delete();
dbEx.Row.AcceptChanges();
}
}
} while (true);

// 更新成功,返回最新数据
dsAnimals.Clear();
m_daAnimals.Fill(dsAnimals.Animals);

return dsAnimals;
}
}
其它代码一样。
我们还可以使用IssueVision的并发处理策略,解决的方法很优雅。
我们可以在DataSetAnimals中新建一个表,和Animals表一样,引用相同的表结构,名称为Conficts,当成冲突记录用。
使用RowUpdating,出现错误行就将其添加Conficts中,更新完成后返回。客户端只需取出DataSetAnimals中的Conficts就可以获取冲突信息。
详细可参看IssueVision。
*************************************************************************
作者:a-peng
出处:http://a-peng.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出
原文连接,否则保留追究法律责任的权利。
*************************************************************************