0. 并发冲突的示例
单用户的系统现在应该比较罕见了,一般系统都会有很多用户在同时进行操作;在多用户系统中,涉及到的一个普遍问题:当多个用户“同时”更新(修改或者删除)同一条记录时,该如何更新呢?
下图展示了开放式并发冲突的一个示例:
假设数据库中有一条记录Record{Field1=5, Field2=6, Field3=7}(以下简写为{5, 6, 7}),A、B两个用户按照如下顺序操作这一条记录:
(1). A读取该记录,取得的值为{5, 6, 7},读取完毕后,不对该记录加排他锁;
(2). B读取该记录,取得的值也为{5, 6, 7},读取完毕后,不对该记录加排他锁;
(3). B将该记录修改为{3, 5, 7},并写回数据库;由于该记录没有被其他用户锁定,且B在修改时该记录的值与第(2)步中读取的值一致,因此可以正常写入数据库;
(4). A将该记录修改为{5, 8, 9},并写回数据库;由于A在修改时该记录的值已更新为{3, 5, 7},与第(1)步中读取的值{5, 6, 7}不一致,因此引发并发冲突;
1. 开放式并发(乐观并发) & 封闭式并发(悲观并发)
开始之前,先介绍两个概念(From 《SQL Server 2008联机丛书》)。
乐观并发控制 | 在乐观并发控制中,用户读取数据时不锁定数据。当一个用户更新数据时,系统将进行检查,查看该用户读取数据后其他用户是否又更改了该数据。如果其他用户更新了数据,将产生一个错误。一般情况下,收到错误信息的用户将回滚事务并重新开始。这种方法之所以称为乐观并发控制,是由于它主要在以下环境中使用:数据争用不大且偶尔回滚事务的成本低于读取数据时锁定数据的成本。 |
悲观并发控制 | 一个锁定系统,可以阻止用户以影响其他用户的方式修改数据。如果用户执行的操作导致应用了某个锁,只有这个锁的所有者释放该锁,其他用户才能执行与该锁冲突的操作。这种方法之所以称为悲观并发控制,是因为它主要用于数据争用激烈的环境中,以及发生并发冲突时用锁保护数据的成本低于回滚事务的成本的环境中。 |
举个例子来说:
乐观并发:本文起始位置的图片,展示的是乐观并发情况下引发的冲突。
悲观并发:继续沿用图片中示例的请求顺序,如果第(1)步中A读取这条记录后,给记录加上排他锁,并且一直持有,在更新完毕之前不释放该锁;则第(2)步中B尝试读取该记录时,请求会被阻塞,直到A释放该记录,或者请求超时。因此新的执行顺序(假设请求没有超时)为:A读取并锁定记录{5, 6, 7}-->B尝试读取该记录[B被阻塞等待]-->A写回{5, 8, 9}到数据库,并释放该记录-->B读取到{5, 8, 9},并锁定该记录-->B修改该记录并写回数据库……
2. Linq to SQL中的乐观并发控制
L2S支持开放式并发控制。使用L2S执行修改和删除操作时,同时打开SQL Server Profile来查看生成的SQL代码,我们可以看到类似这样的代码(假设表上加了TimeStamp字段):
UPDATE TableName SET Field1=@p0, Field2=@p1 WHERE PrimaryKey=@p2 AND TimeStampField=@p3
//(省略)....
DELETE TableName WHERE PrimaryKey=@p0 AND TimeStampField=@p1
//(省略)....
当记录被更新时,则其TimeStamp字段会被自动更新。因此,如果在用户读取该记录后、且更新该记录之前,有其他用户更新过这条记录,则更新会失败(根据受影响行数为0来判断),L2S会抛出ChangeConflictException异常。
以上描述的是表上有加timeStamp字段的情况,如果表上没有TimeStamp字段,L2S会对映射为UpdateCheck = UpdateCheck.Always 或 UpdateCheck.WhenChanged的字段成员进行开放式并发检查,可以根据Sql server Profile来查看,不再赘述。
3. 冲突解决
既然有了冲突,就需要把冲突给和谐掉。
还是以本文起始位置的例子来说,最后A更新时,该更新为啥呢?可以存在如下三种选择:
(1). 覆盖数据值库:{5,8,9}?
(2). 保留数据库值:{3,5,7}?
(3). 合并为:{3,8,9}?
这三种方式分别对了L2S的三种解决方案:
3.1 通过覆盖数据库值解决并发冲突
try
{
db.SubmitChanges(ConflictMode.ContinueOnConflict); //需要指定为ConflictMode.ContinueOnConflict
}
catch (ChangeConflictException e)
{
foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{
occ.Resolve(RefreshMode.KeepCurrentValues); //保留当前值,覆盖数据库中的值
}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //处理完冲突后,重试
3.2 通过保留数据库值解决并发冲突
try
{
db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e)
{
foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{
occ.Resolve(RefreshMode.OverwriteCurrentValues);//以数据库中的值,重写当前值
}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //处理完冲突后,重试
3.3 通过与数据库值合并解决并发冲突
try
{
db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e)
{
foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{
occ.Resolve(RefreshMode.KeepChanges);//保留数据库中的值和当前值,进行合并处理
}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //处理完冲突后,重试