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






3.52/5 (19投票s)
ASP.NET - 使用 SQL Server 中的 Timestamp 列进行并发用户更新。
引言
并发性是在分布式应用程序中应解决的关键问题之一。当多个用户尝试同时更新同一组数据时,更新将按先到先得的顺序进行,而不知道其他用户所做的更改,例如:
- “用户 A”读取一行数据进行编辑。
- 当用户 A 仍在编辑数据时,用户 B 读取相同的数据,修改某个字段,然后更新它。
- 用户 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!!!