Hello World
Spiga

在Linq to Sql中管理并发更新时的冲突(3):使用记录的时间戳进行检测

2007-11-23 09:21 by 老赵, 4782 visits

在《在Linq to Sql中管理并发更新时的冲突(2):引发更新冲突》一文中,我们描述了Linq to Sql检测在更新时是否产生了冲突的基本方法:将该记录每个字段原来的值和更新时的值进行对比,如果稍有不同则意味着记录被修改过,因此产生了更新冲突。不过您是否有这样的感觉,这种方法实在累赘了一些?如果一个表中有数十个字段,那么更新就必须完整地检测一遍(不过我会在今后的文章中提到这方面的控制)。再者,如果其中某一个字段储存了洋洋洒洒上万字的文章,那么在验证时仅仅是将它从Web服务器发送到数据库服务器就需要耗费可观的带宽与时间,这是不是显得有些“得不偿失”呢?

因此Linq to Sql提供了另外一种检测并发更新冲突的方式:使用记录的时间戳。这并不是Linq to Sql特有的功能,如果您了解其他的ORM框架的话,就会发现诸如Hibernate也提供了类似的机制——自然,在使用上不会像Linq to Sql那样方便。

在Sql Server中设计数据表时,我们可以使用一个特殊的数据类型:timestamp。请不要将它与SQL-2003标准中的timestamp类型混淆起来,那里的timestamp和Sql Server中的datetime比较相似(Oracle中timestamp的概念符合SQL-2003标准,而MySql中timestamp的概念与Sql Server相同),而Sql Server中的timestamp与SQL-2003标准中的rowversion类型对应。Sql Server中的timestamp类型和binary(8)在存储上非常类似(不过nullable的timestamp和nvarchar(8)类似),从类型名称上我们就可以看出,这是一个“时间戳”字段:当数据表中的某一条记录被添加或者修改之后,Sql Server会自动向类型为timestamp的字段写入当前时间。换句话说,只要在更新时发现该字段的值没有被修改过,就表明没有产生并发冲突。

我们还是通过一个例子来体验一下吧。

如上图。我们定义了一个新的数据表,其中有个record_version字段为timestamp类型,这就是记录的时间戳(record_version这个字段名似乎有点不太“雅观”,我觉得我们不会去主动使用它,所以问题不大——当然一些静态检查工具可不这么认为:))。有了记录的时间戳,我们就可以在检测更新冲突时获得更好的性能了。

try
{
LinqToSqlDemoDataContext dataContext = new LinqToSqlDemoDataContext();

Order order = dataContext.Orders.Single(o => o.OrderID == 1);
order.Name = "New Order Name";

dataContext.Log = Console.Out;
//
在下面的语句上设置一个断点
dataContext.SubmitChanges();
}
catch (ChangeConflictException e)
{
Console.WriteLine(e.Message);
}

Console.ReadLine();

在最后的语句上设置断点,并且在程序运行至断点后去数据库里对OrderID为1的纪录作任意更新。然后按F5继续运行:

UPDATE [dbo].[Order]
SET [Name] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)

SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 14; Prec = 0; Scale = 0) [New Order Name]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

Row not found or changed.

上面代码中的UPDATE语句相信大家都很清楚其含义。不过这里可能还需要解释其他两个问题:

首先是那句SELECT语句。如果您去阅读自动生成的Object Model的代码时就会发现,record_version属性上有一个ColumnAttribute标记(假设您使用了Attribute Based Mapping Source),其AutoSync属性为Always,因此在任何操作之后,Linq to Sql都会补充一句SELECT语句,以此获得新的数据并修改DataContext中的特定对象。其次,由于timestamp类型的数据在记录被修改时就会设置,因此在更新时其他纪录的值与之前相同,也会引发更新冲突,这一点和基于字段值比较的前一种方法是不同的。

那么,我们一直说出现了“并发更新冲突”,那么发生冲突后又会出现什么问题呢?我们来看一个略有些复杂的示例:

try
{
LinqToSqlDemoDataContext dataContext = new LinqToSqlDemoDataContext();

Order order1 = dataContext.Orders.Single(o => o.OrderID == 1);
Order order2 = dataContext.Orders.Single(o => o.OrderID == 2);
Order order3 = dataContext.Orders.Single(o => o.OrderID == 3);

Console.WriteLine('Order 1: ' + order1.Introduction);
Console.WriteLine('Order 2: ' + order2.Introduction);
Console.WriteLine('Order 3: ' + order3.Introduction);
Console.WriteLine();

order1.Introduction = 'Order 1 modified.';
order2.Introduction = 'Order 2 modified.';
order3.Introduction = 'Order 3 modified.';

dataContext.Log = Console.Out;
// 在下面的语句上设置一个断点
dataContext.SubmitChanges();
}
catch (ChangeConflictException e)
{
Console.WriteLine('---------- ' + e.Message + ' ----------');
}

LinqToSqlDemoDataContext db = new LinqToSqlDemoDataContext();
Order o1 = db.Orders.Single(o => o.OrderID == 1);
Order o2 = db.Orders.Single(o => o.OrderID == 2);
Order o3 = db.Orders.Single(o => o.OrderID == 3);

Console.WriteLine('Order 1: ' + o1.Introduction);
Console.WriteLine('Order 2: ' + o2.Introduction);
Console.WriteLine('Order 3: ' + o3.Introduction);

Console.ReadLine();

假设我们的数据表里有以下三条记录:

OrderID Name Introduction record_version
1 Order 1 This is order 1
2 Order 2 This is order 2
3 Order 3 This is order 3

当程序进入到SubmitChanges语句的断点时,我们去数据库中运行以下代码,以修改OrderID为2的记录。

UPDATE Order SET OrderID = "New Order 2" WHERE OrderID = 2

继续运行程序,最终控制台中会打印出以下信息:

Order 1: This is order 1
Order 2: This is order 2
Order 3: This is order 3

UPDATE [dbo].[Order]
SET [Introduction] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)

SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 26; Prec = 0; Scale = 0) [Order 1 modified.]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

UPDATE [dbo].[Order]
SET [Introduction] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)

SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 26; Prec = 0; Scale = 0) [Order 2 modified.]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

---------- 1 of 2 updates failed. ----------
Order 1: This is order 1
Order 2: This is order 2
Order 3: This is order 3

首先我们分别打印出三个Video对象的Introduction并将它们修改为新的值。在SubmitChanges方法调用之前,数据库中ID为2的记录已经被修改过了,因此在第一组UPDATE+SELECT调用成功之后——请注意,这是一次调用,Linq to Sql每次更新一条记录——在更新第二条记录之后发现了并发冲突。于是抛出异常(请注意异常的Message表示“两次更新其中有一次失败了”),第三条记录也不会再更新了。在冲突发生之后,ID为2和纪录自然没有被修改(WHERE条件不成立),但是第一条记录呢?从try...catch块之后的操作中看,ID为1的记录也没有被更新。

也就是说,第一次更新被回滚了。这自然是事务的作用。在调用(默认的)SubmitChanges方法时,Linq to Sql会把所有的更新放在同一个事务中,因此它们“共同进退”。但是由于业务需求不同,有时候我们不希望某条记录的冲突导致了所有更新失败。自然,Linq to Sql也提供了这个方面的控制。在下一篇文章中,我们就来看一下Linq to Sql中与乐观并发控制有关的事务问题,以及出现并发冲突之后的解决方式。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

19 条回复

  1. Luna[未注册用户]
    *.*.*.*
    链接

    Luna[未注册用户] 2007-11-23 00:47:00

    好文!正需要:):

  2. henry
    *.*.*.*
    链接

    henry 2007-11-23 08:54:00

    如何知道这个并发是不是存在冲突?
    在现实中往往要人手去协调因为A和B需要更新一条记录,但数据的逻辑合法性到底是A正确还是B正确程序几乎是不可能知道.
    对于所谓的更新冲突在多个appserver中的client是如何协调的?

  3. 老赵
    admin
    链接

    老赵 2007-11-23 09:33:00

    @henry
    更新冲突并不代表更新的逻辑是不合法的,A和B都可以更新,但是如果A更新了,B就必须在A的基础上再更新,否则就会出现并发冲突——是这个意思。

  4. 木野狐(Neil Chen)
    *.*.*.*
    链接

    木野狐(Neil Chen) 2007-11-23 09:51:00

    mark

  5. henry
    *.*.*.*
    链接

    henry 2007-11-23 09:53:00

    @Jeffrey Zhao
    在一台appservice里保证后者在前者的基础上作修改并不难,难点是多台appservice直接对数据操作上如何保证这么干.如果不能保证操作切入在一个点上基本很难入手(如果多个client server的事务有排它那基本就省事很多了).

  6. 韩现龙
    *.*.*.*
    链接

    韩现龙 2007-11-23 09:55:00

    也?老赵,你的文章地址怎么是英文显示的嘞?

  7. 木野狐(Neil Chen)
    *.*.*.*
    链接

    木野狐(Neil Chen) 2007-11-23 09:56:00

    @韩现龙
    发布文章的时候有个选项的。允许自定义友好的地址。

  8. 1-2-3
    *.*.*.*
    链接

    1-2-3 2007-11-23 10:25:00

    好文,关注。

  9. 小庄
    *.*.*.*
    链接

    小庄 2007-11-23 16:34:00

    好文,支持楼主。

  10. Enzo
    *.*.*.*
    链接

    Enzo 2007-11-23 16:37:00

    @木野狐(Neil Chen)
    原来如此

  11. 上海搬家公司[未注册用户]
    *.*.*.*
    链接

    上海搬家公司[未注册用户] 2007-11-23 18:42:00

    如果多个client server的事务有排它那基本就省事很多了

  12. Ariel Y.
    *.*.*.*
    链接

    Ariel Y. 2008-03-02 21:58:00

    "Sql Server中的timestamp类型和binary(8)在存储上非常类似(不过nullable的timestamp和nvarchar(8)类似)"

    这个好像有点问题,nvarchar(8)应为varbinary(8)。

    MSDN原文:A nonnullable timestamp column is semantically equivalent to a binary(8) column. A nullable timestamp column is semantically equivalent to a varbinary(8) column.

  13. 老赵
    admin
    链接

    老赵 2008-03-02 22:23:00

    @Ariel Y.
    对的,是我写错了。

  14. 优雅旋律
    *.*.*.*
    链接

    优雅旋律 2008-05-23 10:55:00

    请教下
    到底是通过record_version的特定列名来标识其时间戳列还是通过类型timestamp
    如果是通过类型的话 那假如有n个timestamp列
    到底是使用的哪个?

  15. 老赵
    admin
    链接

    老赵 2008-05-23 11:12:00

    通过类型是timestamp,一个表为什么要有多个字段是timestamp?

  16. 优雅旋律
    *.*.*.*
    链接

    优雅旋律 2008-05-25 00:07:00

    忽然想到的
    万一有n个字段为timestamp
    会不会出bug.....

  17. 天 天
    *.*.*.*
    链接

    天 天 2008-08-14 10:50:00

    @优雅旋律
    不能吧

  18. 唐文
    *.*.*.*
    链接

    唐文 2008-08-22 10:48:00

    一个表只能有一个时间戳列。

  19. yezie
    *.*.*.*
    链接

    yezie 2009-02-05 23:19:00

    下一篇呢????????

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我