65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 和 SQL Server 中的并发用户更新

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.52/5 (19投票s)

2006年10月30日

CPOL

4分钟阅读

viewsIcon

109690

ASP.NET - 使用 SQL Server 中的 Timestamp 列进行并发用户更新。

引言

并发性是在分布式应用程序中应解决的关键问题之一。当多个用户尝试同时更新同一组数据时,更新将按先到先得的顺序进行,而不知道其他用户所做的更改,例如:

  1. “用户 A”读取一行数据进行编辑。
  2. 当用户 A 仍在编辑数据时,用户 B 读取相同的数据,修改某个字段,然后更新它。
  3. 用户 A 最终在不知道用户 B 所做的更改的情况下更新了数据,而用户 B 所做的更改丢失了。

在解决并发问题的多种技术中,就性能、可靠性和易于实现而言,时间戳是最佳选择之一。时间戳是 SQL Server 对行进行活动的一系列操作,以二进制格式表示为递增的数字。包含时间戳列的行在插入或更新时,时间戳值会自动更新。

实现

这里的策略是,每当从数据库获取数据进行更新时,都将时间戳值与其他数据一起获取,并将其存储在前台的视图状态或隐藏变量中。尝试更新时,将数据库中的时间戳值与存储在前台的原始时间戳值进行比较。如果匹配,则表示记录未被其他用户修改,因此执行更新。如果不匹配,则表示记录已被其他用户修改,并且发生了并发冲突。通知用户数据已被其他用户修改。此时,我们可以为用户提供一个选项,可以选择覆盖其更改,也可以选择修订其他用户所做的更改。现在让我们深入研究代码。

步骤 1:向要处理并发更新的目标表添加时间戳列

在此步骤中,我们还可以添加用户名列以跟踪谁更新了数据。

USE pubs
IF EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'Contact')
DROP TABLE Contact
GO
CREATE TABLE [dbo].[Contact](
ContactID int IDENTITY(1,1),
ContactName nvarchar (100) NOT NULL , 
ChgUserID nvarchar (50) NOT NULL,
ChgTimeStamp timestamp) 

步骤 2:修改 SELECT 语句以获取时间戳和其他数据

由于时间戳是二进制数据字段,为了将其保存在 ASP.NET 视图状态中,我们需要将时间戳封送到字符串。我们可以通过几种方式处理这种封送。一种选择是在 .NET 端处理,将时间戳转换为字符串,反之亦然,以便存储和检索在视图状态或隐藏字段中(请参阅“关注点”)。另一种选择是将时间戳转换为 bigint 数据类型,然后再将其返回到前台,以便在 .NET 端轻松处理(在这种情况下无需进行二进制到字符串的转换)。在此说明中,我使用的是第二种选择。

SELECT ContactID, ContactName, 
CONVERT CONVERT(bigint, ChgTimeStamp) as 'TimeStamp'  
FROM Conact Where ContactID = @inContactID

步骤 3:相应地修改 Save 过程

在更新过程参数列表中添加一个额外的时间戳参数。将整数时间戳值转换回 Timestamp 类型。如果记录的当前时间戳与传递给过程的时间戳相同,则更新数据。在时间戳被修改的情况下,行将不会被更新,即行计数为 0 并引发错误。

CREATE PROC USP_UpdateContact(
     @inContactID nchar(10),
     @inContactName nvarchar(100),
     @inChgUserID nvarchar(50),
     @inChgTimeStamp bigint
)       
AS 
BEGIN   
    
    BEGIN TRANSACTION    
   
    --Declare Temporary variables
    DECLARE @ChgTimeStamp TIMESTAMP
    DECLARE @dbUserID NVARCHAR(50)          
    DECLARE @ErrorMsg VARCHAR(2000) --error strings 
    DECLARE @ERR VARCHAR    
            
    SET @ChgTimeStamp = _
        CONVERT(Timestamp,@inChgTimeStamp) --Convert Back
    SELECT @dbUserID = ChgUserID FROM Contact 
    WHERE ContactID = @inContactID 

    --INSERT/UPDATE 
    IF EXISTS (SELECT * FROM Contact  where ContactID = @inContactID)
    BEGIN
        UPDATE [dbo].[Contact] 
        SET              
            [ContactName] = @inContactName,
            [ChgUserID] = @inChgUserID
            WHERE ContactID = @inContactID 
            AND ChgTimeStamp = @ChgTimeStamp
    
        IF @@ROWCOUNT = 0 
        BEGIN 
            SET @ErrorMsg = _
            'The data you are about to save is modified by ' _
            + @dbUserID + _
            '. Please review the new data and save again.' 
            RAISERROR(@ErrorMsg,16,1, -999) 
            GOTO ERR_HANDLER 
        END 
        IF(@@ERROR <> 0) GOTO ERR_HANDLER 
    END 
    ELSE   
    BEGIN 
    INSERT INTO [dbo].[Contact] 
    (            
        [ContactName],   
        [ChgUserID] 
    )             
    VALUES 
    (
        @inContactName,    
        @inChgUserID            
    )             
    END    
    IF(@@ERROR <> 0) GOTO ERR_HANDLER  
    IF @@TRANCOUNT > 0 COMMIT TRANSACTION 
    RETURN 0  
    ERR_HANDLER:  
        IF @@TRANCOUNT > 0   
        ROLLBACK TRANSACTION     
        SELECT @ERR = @@error  
        RETURN @ERR  
END

步骤 4:.NET 代码以适应从数据库获取的时间戳

将时间戳获取到视图状态变量中。将此变量视为 ASP.NET 中的常规 Web 控件,即,每当从数据库获取数据以与其他 Web 控件一起显示时,都要填充它。将此值保存回数据库时(步骤 3),将其传回数据库。

//View state declaration 
private string TimeStamp
{
    get
    { 
        return (ViewState["TimeStamp"] != 
        null ? ViewState["TimeStamp"].ToString() : "");
    }
    set{ ViewState["TimeStamp"] = value; }
}

//Fill Time stamp
void DisplayContactUI()
{
    //Contact Display code here
    TimeStamp = ds.Tables[0].Rows[0]["TimeStamp"].ToString();
}

void SaveContactDB(..)
{
try
{
    //Open Connection, Add parameters
    ...

    pm = cm.Parameters.Add("@inChgTimeStamp", SqlDbType.BigInt);
    pm.Value = decimal.Parse(TimeStamp); 
    //TO DO: check for empty string 
    cn.Open();
    int i = cm.ExecuteNonQuery();
    cn.Close();
}
catch (SqlException sqlex)
{
    throw;
}
finally
{
}

关注点

替代方案:要在 .NET 端处理时间戳封送,请使用以下视图状态属性。在这里,我们可以在不将其转换为 bigint 的情况下从数据库获取时间戳列值。

public object TimeStamp
{
    get
    {
        byte[] bt = new byte[8];
        for(int i = 0; i < 8; i++)
        {
            bt[i] = 
                Convert.ToByte(
                ViewState["TimeStamp"].ToString().Substring(i * 3,2),16);
        }
        return bt;
    }
    set
    {
        ViewState["TimeStamp"] = BitConverter.ToString((byte[])value);
    }
}

上面的代码使用 BitConverter 类将从数据库接收的字节数组转换为字符串。Convert.ToByte 方法将字符串转换回字节数组,以便将数据发送回数据库。

我想感谢 Bruce J Mack 先生和 Luigi 先生提出的宝贵建议和反馈。请随时提出您的问题/想法/建议。感谢您的光临。Mahalo!!!

© . All rights reserved.