远山雪菜1000:TaskVision并发处理 - a-peng - 博客园

来源:百度文库 编辑:九乡新闻网 时间:2024/05/01 00:57:02
一、(引言)
小菜通过一个简单的例子来说明如何进行并发处理,大家不用当心例子太简单反映不了问题。
小菜通过运行两个实例来模拟两个客户端。
一个阿猫、阿狗引发的故事:
数据库中记录:

客户端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 = “阿黄”
怎么办呢?(小菜也不知道怎么办,因为我们不知道用户想干什么)
用户目的是:将“狗”改成“阿黄”
如果用户知道了别人已经将“狗”改成了“阿狗”
用户可能会继续更改,也可能放弃更改。(这就出现了两种可能!!!)
所以我们应该让用户自己来选择:

以上解决并发处理的方案称为:开放式并发处理.
对应的保守式并发处理:
是当用户编辑数据库中的某行数据时,保守式并发会使此数据一直保持锁定.在锁定期间,其他用户不能更改数据,这样能够最高程度的保证数据的完整性,但是可用性低.
代码下载:http://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);
}
和演化一,一样的测试,效果如下:

我们来看看CollisionForm的代码:
using System;
using System.Windows.Forms;

using Client.DataWS;

namespace Client
{
    public partial class CollisionForm : Form
    {
        private DataSetAnimals.AnimalsRow m_errorRow;

        public CollisionForm(DataSetAnimals.AnimalsRow errorRow)
        {
            InitializeComponent();

            m_errorRow = errorRow;
        }

        private void CollisionForm_Load(object sender, EventArgs e)
        {
            // 绑定当前值
            txtAnimalID.Text = m_errorRow.AnimalID.ToString();
            txtAnimalType.Text = m_errorRow.AnimalType;

            // 绑定原始值
            txtOriginalAnimalID.Text = m_errorRow["AnimalID", System.Data.DataRowVersion.Original].ToString();
            txtOriginalAnimalType.Text = m_errorRow["AnimalType", System.Data.DataRowVersion.Original].ToString();
        }

        // 保存更改
        private void btnSave_Click(object sender, EventArgs e)
        {
            if (txtAnimalType.Text.Trim().Length > 0)
            {
                m_errorRow.ClearErrors();

                m_errorRow.AnimalType = txtAnimalType.Text;

                this.Close();
            }
        }

        // 忽略更改
        private void btnIgnore_Click(object sender, EventArgs e)
        {
            m_errorRow.ClearErrors();
            m_errorRow.RejectChanges();

            this.Close();
        }
    }
}
到这时,我们就基本完成任务了。不过有哪些不足呢?
你要是注意看就会发现上面显示的,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;
    }
}
存储过程:GetAnimals
CREATE PROCEDURE [GetAnimals]
 AS
SET NOCOUNT ON;
SELECT AnimalID, AnimalType FROM Animals
GO
存储过程:UpdateAnimal
CREATE PROCEDURE UpdateAnimal
(
    @AnimalID int,
    @AnimalType varchar(50),
    @Original_AnimalType varchar(50)
)
AS
SET NOCOUNT OFF;
UPDATE [Animals] SET [AnimalType] = @AnimalType WHERE (([AnimalID] = @AnimalID)
AND ([AnimalType] = @Original_AnimalType));

SELECT AnimalID, AnimalType FROM Animals WHERE (AnimalID = @AnimalID)
GO
存储过程:InsertAnimal
CREATE PROCEDURE [InsertAnimal]
(
    @AnimalType varchar(50)
)
AS
SET NOCOUNT OFF;
INSERT INTO [Animals] ([AnimalType]) VALUES (@AnimalType);

SELECT AnimalID, AnimalType FROM Animals WHERE (AnimalID = SCOPE_IDENTITY())
GO
存储过程:DeleteAnimal
CREATE PROCEDURE [DeleteAnimal]
(
    @AnimalID int,
    @Original_AnimalType varchar(50)
)
AS
SET NOCOUNT OFF;
DELETE FROM [Animals] WHERE (([AnimalID] = @AnimalID) AND ([AnimalType] = @Original_AnimalType))
GO
存储过程:GetAnimal
CREATE PROCEDURE [GetAnimal]
(
    @AnimalID int
)
 AS
SET NOCOUNT ON;
SELECT AnimalID, AnimalType FROM Animals WHERE AnimalID = @AnimalID
GO
其它代码一样。
我们还可以使用IssueVision的并发处理策略,解决的方法很优雅。
我们可以在DataSetAnimals中新建一个表,和Animals表一样,引用相同的表结构,名称为Conficts,当成冲突记录用。
使用RowUpdating,出现错误行就将其添加Conficts中,更新完成后返回。客户端只需取出DataSetAnimals中的Conficts就可以获取冲突信息。
详细可参看IssueVision。
*************************************************************************
作者:a-peng
出处:http://a-peng.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出
原文连接,否则保留追究法律责任的权利。
*************************************************************************
分类:小菜之智能客户端
绿色通道:好文要顶关注我收藏该文与我联系
a-peng
关注 - 0
粉丝 - 3
+加关注
0
0
(请您对文章做出评价)
« 上一篇:TaskVision自定义数据验证控件
» 下一篇:TaskVision基础版
Feedback
#1楼回复引用查看
2008-10-14 21:02 by斯克迪亚      
教程很生动,非常感谢。
不过我记得乐观并发处理是乐观地认为并发出现几率极小,并不足以产生问题呀,悲观并发处理才是重视冲突的吧?
#2楼[楼主]回复引用查看
2008-10-14 21:46 by阿鹏      
@斯克迪亚
谢谢.
我刚才查了下:
http://technet.microsoft.com/zh-cn/library/ms189132.aspx
并发控制的类型:
并发控制理论根据建立并发控制的方法而分为两类:
悲观并发控制
一个锁定系统,可以阻止用户以影响其他用户的方式修改数据。如果用户执行的操作导致应用了某个锁,只有这个锁的所有者释放该锁,其他用户才能执行与该锁冲突的操作。这种方法之所以称为悲观并发控制,是因为它主要用于数据争用激烈的环境中,以及发生并发冲突时用锁保护数据的成本低于回滚事务的成本的环境中。
乐观并发控制
在乐观并发控制中,用户读取数据时不锁定数据。当一个用户更新数据时,系统将进行检查,查看该用户读取数据后其他用户是否又更改了该数据。如果其他用户更新了数据,将产生一个错误。一般情况下,收到错误信息的用户将回滚事务并重新开始。这种方法之所以称为乐观并发控制,是由于它主要在以下环境中使用:数据争用不大且偶尔回滚事务的成本低于读取数据时锁定数据的成本。
--------------------------------------
我修改了文章,改成开放式并发和保守式并发更清晰.
#3楼回复引用
2008-10-15 09:08 by
在IssueVision中,由于是使用.DataAdapter,因此,可以通过RowUpdating事件完成处理。
如果使用TableAdapter,则只能通过捕获DBConcurrencyException去处理冲突。
诚然,IssueVision使用ConflictTable的设计,在更新表较少时,确实是一个实用的设计。
#4楼[楼主]回复引用查看
2008-10-15 13:43 by阿鹏      
@Jeff Chen
:) 同意