实施并发优化

本文档是 Visual C# 教程 (切换到 Visual Basic 教程)

对于允许多个用户同时编辑数据的 Web 应用程序,存在着一定的风险,即两个用户可能同时编辑同一个数据。本教程中,我们将介绍如何采用并发优化控制来对付这一风险。

« 前一篇教程  |  下一篇教程 »

Part 1

简介

对于只允许用户浏览数据,或只允许单一用户修改数据的 Web 应用程序,就不会存在这样的风险:两个同时操作的用户碰巧在相互覆盖掉别人所做的修改。然而,对于允许多用户更新或删除数据的Web 应用程序,就存在着这样的可能性:一个用户所做的修改与另一个同时操作的用户的修改相冲突。如果没有适当的并发策略,当两个用户同时编辑同一条记录时,最后提交修改信息的用户就会用其内容覆盖掉第一个用户所做的修改。

例如,假设有两个用户,Jisun 和 Sam ,同时在访问我们应用程序的一个页面,这里允许访问者通过 GridView 控件更新和删除产品信息。两个人几乎是同时单击了 GridView 的 Edit 按钮。 Jisun 把产品名称更改成 Chai Tea ,并单击了 Update 按钮。其最终结果是把一条 UPDATE 语句传递到数据库,数据库则对该产品的所有可更新字段都重新赋值(虽然 Jisun 只更新了一个 ProductName 字段 ) 。这时,数据库中的这个产品的名称为 Chai Tea ,类别为 Beverages ,供应商为 Exotic Liquids ,等等。可是,在 Sam 的屏幕上,在可编辑的 GridView 行中,所显示的产品名称仍然是 Chai 。在 Jisun 的修改提交之后几秒种, Sam 把类别修改为 Condiments ,并单击 Update 。这导致一条 UPDATE 语句被传递到数据库,将产品名称赋值为 Chai ,并把 CategoryID 的值赋给了对应于 Beverages 的类别 ID ,等等。就这样, Jisun 所做的对产品名称的修改被覆盖了。图 1 用图形表示了这一系列事件。

图1 :当两个用户同时更新一条记录时,存在一个用户所做的修改被另一用户覆盖的潜在风险

类似的情况是,当两个用户同时访问一个页面时,一个用户可能正在更新一条记录,而另一个用户却把它删除了。或者,当一个用户调出了一个页面,正准备单击 Delete 按钮时,另一个用户可能已经修改了这条记录的内容。

有三种并发控制 策略可供采用:

  • Do Nothing  如果并发用户在修改同一条记录,则让后提交者所做的修改信息覆盖前者的修改信息(缺省设置)。
  • Optimistic Concurrency  假定虽然可能偶尔会出现并发冲突,但大多数情况下这类冲突不会发生。于是,当冲突确实发生时,就通知用户,他们的修改不能存储,因为另一用户已经修改了相同的数据。
  • Pessimistic Concurrency  假定并发冲突经常发生,用户不能容忍被告知他们的修改由于另一用户的并发行为而不能存储。于是,当一个用户开始修改一条记录时,则把这条记录锁住,从而防止任何其他用户再编辑或删除这条记录,直到此用户提交了他的修改内容。

到目前为止,我们所有的教程都用的是缺省的并发解决策略,也就是,让后提交者的修改信息覆盖前者的修改信息。本教程中,我们将探讨如何实施并发优化控制。

注意 :在本系列教程中,我们不讨论封锁式并发的例子。封锁式并发很少使用,因为这样的封锁如果不能适时解除,就会防碍其他用户更新数据。例如,如果一个用户为修改数据而锁住了一条记录,然后在解锁前他又走开了;这样,其他所有用户就都不能再更新这条记录,直到最初锁住它的用户回来并完成其更新。因此,在实施封锁式并发时,一般都要设置超时,一旦超时就解锁。票务销售网站是应用封锁式并发控制的一个例子,它在短期内锁定某些座位,让用户完成订票过程。

步骤1 :探讨如何实施并发优化

并发优化控制的机理,是保证正在修改或删除的记录值与修改或删除过程开始时的值相同。例如,在可编辑的 GridView 控件中单击 Edit 按钮,便从数据库中读出了记录的值并显示在 TextBox 和其他 Web 控件中。 GridView 保存下这些原始值。之后,在用户做了修改并单击了 Update 按钮后,原始值加上新赋的值都被送到业务逻辑层,然后下到数据访问层。数据访问层必须发出一条 SQL 语句,但只有当用户开始编辑时的原始值与数据库里的当前值完全相同时,才真正执行该语句更新记录。图 2 描述了事件的顺序。

图2 :只有在原始值与数据库当前值相同时更新或删除才能成功

实施并发优化的方法有多种(参阅 Peter A. BrombergOptmistic Concurrency Updating Logic ,其中有对多个方法的简要介绍)。ADO.NET 的 Typed DataSet 提供了一种只需勾选复选框便可配置的实施方法。对 Typed DataSet 的 TableAdapter 启用并发优化扩展了 TableAdapter 的 UPDATE 和 DELETE 语句,在 WHERE 子句中包含了一个对所有原始值的比较。以下面这句 UPDATE 语句为例,只有当前数据库的值等于最初修改 GridView 的记录时获得的值时,才更新产品名称和价格。其中,@ProductName 和 @UnitPrice 是用户输入的新值,而@original_ProductName 和@original_UnitPrice 是单击Edit 按钮时最初加载到GridView 的值:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

注意:为便于理解,这条UPDATE 语句是经过简化的。实际应用时,WHERE 子句对 UnitPrice 的检查还要考虑更多的因素,因为 UnitPrice 的值可能为 NULL 。而且,检查 NULL = NULL 是否总是返回 False (这样必须使用 IS NULL 代替)。

除了使用一条不同的基础 UPDATE 语句之外,配置TableAdapter 使用并发优化也要修改其数据库直接方法的签名。在我们的首篇教程(“创建数据访问层 ”)中曾提到,数据库直接方法是将一系列标量值作为输入参数的方法(而不是作为一个强类型的DataRow 或 DataTable 实例)。当使用并发优化时,数据库直接Update() 和 Delete() 方法也包括输入参数的原始值。此外,若使用批量更新模式(接受DataRow 和 DataTable ,而不是标量值的 Update() 方法过载),业务逻辑层中的代码也必须改变。

与其拓展我们现有数据访问层的TableAdapter 来使用并发优化(这也得修改业务逻辑层才能满足要求),还不如让我们创建一个新的Typed DataSet ,名为 NorthwindOptimisticConcurrency ,并向它添加一个使用并发优化的 Products TableAdapter 。然后,创建一个 ProductsOptimisticConcurrencyBLL 业务逻辑层类,并做适当的修改以支持并发优化的数据访问层。一旦奠定了这个基础,我们就可以着手创建ASP.NET 页面了。

步骤2 :创建支持并发优化的数据访问层

为创建一个新的 Typed DataSet ,右键单击App_Code 文件夹内的 DAL 文件夹,并添加一个新的 DataSet ,名为 NorthwindOptimisticConcurrency 。正像我们在首篇教程中学到的,这一操作将向Typed DataSet 添加一个新的 TableAdapter ,并自动启动 TableAdapter Configuration Wizard 。在第一个屏幕中,向导提示我们指定要连接的数据库—— 使用 Web.config 中的 NORTHWNDConnectionString 设置,连接到同一个 Northwind 数据库。

图3 :连接到同一个 Northwind 数据库

下一步,需要选择如何查询数据:通过一个ad-hoc SQL 语句、新的存储过程,或现有的存储过程。由于我们在原来的数据访问层中使用的是ad-hoc SQL 查询,所以这里仍选择这一选项。

图4 :指定使用 ad-hoc SQL 语句来检索数据

在下一个屏幕中,输入一条 SQL 查询来检索产品信息。我们使用原来在数据访问层中对Products TableAdapter 用过的完全相同的SQL 查询,返回产品信息的所有列,以及产品的供应商和类别名称:

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

图5 :使用与原来数据访问层的 Products TableAdapter 相同的 SQL 查询语句

在转到下一个屏幕之前,单击Advanced Options 按钮。为了让这个 TableAdapter 使用并发优化控制,勾选上Use optimistic concurrency 复选框即可。

图6 :勾选 Use optimistic concurrency 复选框以启用并发优化

最后,选中TableAdapter 应使用既填充 DataTable 又返回 DataTable 的数据访问模式;同时勾选上应创建数据库直接方法。将 Return a DataTable 模式的方法名称从 GetData 改为 GetProducts ,以反映我们在原来数据访问层中使用的命名规则。

图7 :让 TableAdapter 使用所有数据访问模式

向导运行完之后,DataSet Designer 将包括一个强类型的 Products DataTable 和 TableAdapter 。再花点时间来重新命名DataTable ,由 Products 改为 ProductsOptimisticConcurrency :右键单击DataTable 的标题栏,然后选择上下文菜单中的 Rename 。

图8 :一个 DataTable 和 TableAdapter 已被添加到Typed DataSet 中

要查看ProductsOptimisticConcurrency TableAdapter ( 使用并发优化 )和 Products TableAdapter (不使用并发优化)之间UPDATE 和 DELETE 查询的区别,单击 TableAdapter 并转至 Properties 窗口。在 DeleteCommand 和 UpdateCommand 属性的 CommandText 子属性中,可以看到调用数据访问层的与更新或删除相关的方法时传递到数据库的真实的 SQL 语句。ProductsOptimisticConcurrency TableAdapter 所使用的 DELETE 语句是:

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

而我们原来的数据访问层的 Product TableAdapter 所用的 DELETE 语句则要简单得多 :

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

可以看到,使用并发优化的 TableAdapter 的 DELETE 语句的 WHERE 子句中,将 Product 表中现有的各列的值与 GridView ( 或DetailsView 或 FormView )最后一次所填充的原始值进行比较。因为除了 ProductID 、 ProductName 和 Discontinued 之外的所有字段都可包含 NULL 值,所以在 WHERE 子句中还有附加的参数和检查,用适当方式来比较 NULL 值。

在本教程中,我们不在启用并发优化的 DataSet 中添加任何其他的 DataTable , 因为我们的ASP.NET 页面只提供更改和删除产品信息。不过,我们仍需要将 GetProductByProductID(productID) 方法添加到 ProductsOptimisticConcurrency TableAdapter 中。

为此,右键单击 TableAdapter 的标题栏( Fill 和 GetProducts 方法名称正上方的区域),并在快捷菜单中选择 Add Query 。这将启动TableAdapter 查询配置向导。正如我们的 TableAdapter 的初始配置一样,选择使用 ad-hoc SQL 语句创建 GetProductByProductID(productID) 方法(见图 4 )。由于 GetProductByProductID(productID) 方法返回某一特定产品的信息,所以要指明该查询属于返回行的 SELECT 查询类型。

图9 :选择 SELECT which returns rows 作为查询类型

下一个屏幕提示我们写上要用的 SQL 查询,预先调入了 TableAdapter 的缺省查询。 向现有的查询中增加子句 WHERE ProductID = @ProductID ,如图 10 所示。

图10 :增加 WHERE 子句到预先调入的查询中以返回某特定产品的记录

最后,将所生成的方法名称更改为 FillByProductID 和 GetProductByProductID 。

图11 :将方法重命名为 FillByProductID 和 GetProductByProductID

这个向导运行结束后,TableAdapter 现在包含两个检索数据的方法:返回所有产品的 GetProducts() 和返回特定产品的 GetProductByProductID(productID) 。

步骤3 :为启用并发优化的数据访问层创建业务逻辑层

我们现有的ProductsBLL 类既有使用批量更新模式也有使用数据库直接模式的例子。AddProduct 方法和 UpdateProduct 重 载都使用批量更新模式,把一个 ProductRow 实例传到 TableAdapter 的 Update 方法。另一方面, DeleteProduct 方法使用数据库直接模式,调用 TableAdapter 的 Delete(productID) 方法。

有了新的ProductsOptimisticConcurrency TableAdapter , 数据库直接方法现在要求也要传入原始值。例如, Delete 方法现在需要 10 个输入参数:原始的 ProductID 、 ProductName 、 SupplierID 、 CategoryID 、 QuantityPerUnit 、 UnitPrice 、 UnitsInStock 、 UnitsOnOrder 、 ReorderLevel 和 Discontinued 。在发往数据库的 DELETE 语句的 WHERE 子句中使用这些附加的输入参数值,只有当数据库的当前值与原始值相吻合时,才删除指定的记录。

尽管用在批量更新模式中的 TableAdapter 的 Update 方法的签名并没有改变,但记录原始值和新值的代码却有所改变。所以,与其尝试使用具有我们现有 ProductsBLL 类的启用并发优化的数据访问层,还不如创建一个新的业务逻辑层类,与新的数据访问层一起工作。

把一个名为 ProductsOptimisticConcurrencyBLL 的类添加到 App_Code 文件夹里的 BLL 文件夹中。

图12 :添加 ProductsOptimisticConcurrencyBLL 类到BLL 文件夹中

下一步,把以下代码添加到ProductsOptimisticConcurrencyBLL 类中:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

注意,在类声明开始部分上面NorthwindOptimisticConcurrencyTableAdapters 语句的使用。NorthwindOptimisticConcurrencyTableAdapters 命名空间包含ProductsOptimisticConcurrencyTableAdapter 类,它提供数据访问层的方法。在类声明之前,您还可以看到 System.ComponentModel.DataObject 特性,它指示 Visual Studio 将这个类包括在 ObjectDataSource 向导的下拉列表中。

ProductsOptimisticConcurrencyBLL 的 Adapter 属性提供了对 ProductsOptimisticConcurrencyTableAdapter 类的实例的快捷访问,并延续了在我们原来的业务逻辑层类(ProductsBLL 、CategoriesBLL 等 )中所使用的模式。最后,GetProducts() 方法只需向下调用数据访问层的 GetProducts() 方法,并返回一个 ProductsOptimisticConcurrencyDataTable 对象,其中填充了数据库中每个产品记录的 ProductsOptimisticConcurrencyRow 实例。

使用具有并发优化功能的数据库直接模式删除产品

在应用并发优化的数据访问层使用数据库直接模式,必须将新值和原始值都传给方法。对于删除过程,由于这里没有新值,所以只需传入原始值。然后,在我们的业务逻辑层中,必须接受所有的原始值作为输入参数。让我们用 ProductsOptimisticConcurrencyBLL 类中的 DeleteProduct 方法应用数据库直接方法。这意味着,这个方法需要将所有 10 个产品数据字段都作为输入参数,并把它们传递到数据访问层。代码如下:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

如果最后载入GridView ( 或 DetailsView 或 FormView )的那些原始值与用户单击 DELETE 按钮时数据库里的值不符,WHERE 子句将和数据库中的任何记录都匹配不上,不会有记录受到影响。因此,TableAdapter 的 Delete 方法将返回 0 ,而业务逻辑层的 DeleteProduct 方法则返回 false 。

在并发优化下使用批量更新模式修改产品记录

如前所述,无论是否使用并发优化,TableAdapter 的批量更新模式的 Update 方法具有相同的方法签名。也就是说,Update 方法可以接受 DataRow 、 DataRow 数组、 DataTable 或 Typed DataSet 作为参数。这里没有附加的输入参数来指定原始值。之所以能做到这点,是因为 DataTable 始终跟踪它的 DataRow 的原始值和修改值。当数据访问层调用其 UPDATE 语句时,把 DataRow 的原始值存入 @original_ColumnName 参数中,而把 DataRow 的修改值存入 @ColumnName 参数中。

在 ProductsBLL 类(它用的是我们原来的,没有并发优化的数据访问层),当使用批量更新模式更新产品信息时,我们的代码按顺序实现下列事件:

  1. 使用 TableAdapter 的 GetProductByProductID(productID) 方法读取当前的数据库产品信息到一个ProductRow 实例。
  2. 将新的值赋给步骤 1 所用的 ProductRow 实例。
  3. 调用 TableAdapter 的 Update 方法 , 传入该 ProductRow 实例。

但是,以上步骤并不会正确地支持并发优化,因为在步骤1 中放到 ProductRow 中的数据是直接从数据库里读出的,这意味着DataRow 所使用的原始值是当前存在数据库里的值,而不是在编辑过程开始时绑定到 GridView 中的值。取而代之的是,在启用了并发优化的数据访问层应用里,我们需要重载 UpdateProduct 方法,来使用下列步骤:

  1. 使用 TableAdapter 的 GetProductByProductID(productID) 方法 , 读取当前数据库产品信息到一个 ProductsOptimisticConcurrencyRow 实例。
  2. 原始值赋给步骤 1 中所用的 ProductsOptimisticConcurrencyRow 实例。
  3. 调用 ProductsOptimisticConcurrencyRow 实例的 AcceptChanges() 方法 , 它指示 DataRow : 当前值是 “ 原始 ” 值。
  4. 值赋给 ProductsOptimisticConcurrencyRow 实例。
  5. 调用 TableAdapter 的 Update 方法 , 传入该 ProductsOptimisticConcurrencyRow 实例。

步骤1 读出指定产品的记录当前在数据库中的所有值。这个步骤对于更新 所有产品列的 UpdateProduct 重载是多余的(因为这些值在步骤 2 中将被覆盖),但对于那些只传入部分列值作为输入参数的过载方法来说,它却是必要的。一旦把原始值赋给 ProductsOptimisticConcurrencyRow 实例, AcceptChanges() 方法即被调用,它标志着把当前 DataRow 中的值作为原始值用在 UPDATE 语句的 @original_ColumnName 参数中。下一步,将新值赋给 ProductsOptimisticConcurrencyRow 。最后,调用 Update 方法,传入该 DataRow 。

下面的代码列出了接受所有产品数据字段作为输入参数的 UpdateProduct 过载方法。虽然在这里没有列出,但在本教程的下载内容中有一个 ProductsOptimisticConcurrencyBLL 类,它也包含一个只接受产品名称和价格作为输入参数的 UpdateProduct 重载方法。

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct( // new parameter values 
    string productName, int? supplierID, int? categoryID, string quantityPerUnit, decimal? unitPrice, 
    short? unitsInStock, short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID, 
    // original parameter values 
    string original_productName, int? original_supplierID, int? original_categoryID, string original_quantityPerUnit, 
    decimal? original_unitPrice, short? original_unitsInStock, short? original_unitsOnOrder, 
    short? original_reorderLevel, bool original_discontinued, int original_productID) 
{ 
    // STEP 1: Read in the current database product information 
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products = 
        Adapter.GetProductByProductID(original_productID); 
    if (products.Count == 0) // no matching record found, return false 
        return false; 
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0]; 
    // STEP 2: Assign the original values to the product instance 
    AssignAllProductValues(product, original_productName, original_supplierID, original_categoryID, original_quantityPerUnit, 
        original_unitPrice, original_unitsInStock, original_unitsOnOrder, original_reorderLevel, original_discontinued); 
    // STEP 3: Accept the changes 
    product.AcceptChanges(); 
    // STEP 4: Assign the new values to the product instance 
    AssignAllProductValues(product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, 
        unitsOnOrder, reorderLevel, discontinued); 
    // STEP 5: Update the product record 
    int rowsAffected = Adapter.Update(product); 
    // Return true if precisely one row was updated, otherwise false 
    return rowsAffected == 1; 
}





上一页 | 1 | 2 | 下一页