LINQ学习之旅——第二站"LTQ"之并发访问冲突及处理
在编写用户应用软件时,只要是涉及到数据库的操作,尤其是对于多用户并发访问数据库的情况,开发人员都要考虑如何来解决并发冲突的问题。当一个用户从数据库中读取数据并修改数据,最后把修改结果保存到数据库中,在这个过程中,另外一个用户也同样在查询和修改和上一个用户相同的数据,并早于上一个用户向数据库提交了更改,这时第一个用户在向数据库提交修改就发生了所谓的并发冲突。所以所开发的程序一定要提供对并发冲突检测和处理的机制。一般情况下,处理并发冲突有两种方式:乐观并发和悲观并发。下面详细介绍一下这两种处理并发冲突的方式。
1.乐观并发:
从字面意思上来看,就是说它对于并发冲突的发生持有乐观的态度,且认为大部分的程序运行时间里不会发生并发冲突。所以程序在访问和修改数据库时不会锁定要修改的数据,而是采用一种在修改时动态监测并发冲突的可能性的方法。乐观并发允许大量的用户访问数据库,也是目前应用最广泛的一种并发冲突处理方式。而LINQ TO SQL就提供了对乐观并发的支持,来帮助完成冲突检测和冲突处理任务。LINQ TO SQL提供了两种方法来检测并发冲突。这两种方式关键都在之前对象——关心映射(ORM)一节中所提到的内联属性Column中参数IsVersion和UpdateCheck的设置上。那么第一种就是用参数IsVersion来修饰实体类的列属性,如果实体类的某一列使用了IsVersion修饰并其值设置为true,这该列用于检测并发冲突。在用示例进一步说明之前,先把前几节都用到过的实体类Student做如下更改:
1 [Table(Name ="dbo.student")]
2 class Student
3 {
4 [Column(Name ="sno",IsPrimaryKey=true, DbType ="char(8) NOT NULL")]
5 publicstring No { get; set; }
6
7 //Name列进行冲突检测
8 [Column(Name ="sname", DbType ="varchar(10) NOT NULL", IsVersion =true)]
9 publicstring Name { get; set; }
10
11 [Column(Name ="sdept", DbType ="varchar(10)", CanBeNull =true)]
12 publicstring Dept { get; set; }
13
14 [Column(Name ="ssex", DbType ="char(2)", CanBeNull =true)]
15 publicstring Sex { get; set; }
16
17 [Column(Name ="sage", DbType ="int", CanBeNull =true)]
18 publicint Age { get; set; }
19
20
21 //重新实现ToString()方法
22 publicoverridestring ToString()
23 {
24 return"{No="+ No +", Name="+ Name +", Dept="+ Dept+", Sex="+ Sex+", Age="+ Age +"}";
25 }
26 }
i.IsVersion参数列参与乐观并发冲突检测:
1 string str_conn =@"Data Source=localhost;Initial Catalog=DB_Student;User ID=sa;Password=king";
2 DataContext dc =new DataContext(str_conn);
3
4 var students = dc.GetTable<Student>();
5 //将SQL语句显示在控制台
6 dc.Log = Console.Out;
7
8 var student = (from stu in students
9 where stu.No =="20030005"
10 select stu).Single();
11
12 //修改实体类属性
13 student.Age +=1;
14
15 //提交修改到数据库
16 dc.SubmitChanges();
17
18 Console.Read();
ii.结果:
从运行结果中可以看到,在向数据库提交数据更新的时候,Update语句中不仅sno作为Where查找记录的条件,而且sname也作为查找条件参与更新。但要注意的是参数IsVersion在每个实体类中只能设置一个实体列。通常IsVersion参数和数据库中类型为Timestamp(时间戳)的字段数据结合使用。在LINQ TO SQL第一次查询数据时,会在DataContext对象中单独保存所有数据的原始值,当向数据库提交更新时,这些原始数据作为检测并发冲突的条件,将这些原始值添加到Update语句中的Where条件中,比如上述中的Name属性的原始值,如果数据库中该数据在这段时间里被别的用户修改了,那么显然该Update语句中的条件将不成立,这时LINQ TO SQL就感知到并发冲突的发生了,接着就可以相应做并发处理。IsVersion参数用于哪一列,哪一列就参与并发冲突检测。既然参数IsVersion只能在设置一个实体列,那么如果更新时要检测的条件很多,比如上述实体类中的列Sdept和列Age都要进行并发检测,那该如何解决呢?
LINQ TO SQL中还允许使用UpdateCheck参数来指定参与并发冲突检测的列,这就是第二种乐观并发冲突检测的方式。UpdateCheck是一个枚举类型,有Never、Always和WhenChanged这三个枚举值。其中Never指示该列不参加并发冲突检测;Always指示该列将一直参与并发冲突检测;WhenChanged指示该列只有值发生修改时才参与并发冲突检测。下面通过示例来进一步说明:
i.修改实体类Student:
1 [Table(Name ="dbo.student")]
2 class Student
3 {
4 [Column(Name ="sno",IsPrimaryKey=true, DbType ="char(8) NOT NULL")]
5 publicstring No { get; set; }
6
7 //Name列一直参与并发冲突检测
8 [Column(Name ="sname", DbType ="varchar(10) NOT NULL",UpdateCheck=UpdateCheck.Always)]
9 publicstring Name { get; set; }
10
11 //Dept列只有在值发生改变时才参与并发冲突检测
12 [Column(Name ="sdept", DbType ="varchar(10)", CanBeNull =true,UpdateCheck=UpdateCheck.WhenChanged)]
13 publicstring Dept { get; set; }
14
15 //Sex列从不参与并发冲突检测
16 [Column(Name ="ssex", DbType ="char(2)", CanBeNull =true,UpdateCheck=UpdateCheck.Never)]
17 publicstring Sex { get; set; }
18
19 //Age列只有在值发生改变时才参与并发冲突检测
20 [Column(Name ="sage", DbType ="int", CanBeNull =true,UpdateCheck=UpdateCheck.WhenChanged)]
21 publicint Age { get; set; }
22
23 //重新实现ToString()方法
24 publicoverridestring ToString()
25 {
26 return"{No="+ No +", Name="+ Name +", Dept="+ Dept+", Sex="+ Sex+", Age="+ Age +"}";
27 }
28 }
ii.UpdateCheck参数列参与乐观并发冲突检测:
1 string str_conn =@"Data Source=localhost;Initial Catalog=DB_Student;User ID=sa;Password=king";
2 DataContext dc =new DataContext(str_conn);
3
4 var students = dc.GetTable<Student>();
5
6 //将SQL语句显示在控制台
7 dc.Log = Console.Out;
8
9 var student1 = (from stu in students
10 where stu.No =="20030006"
11 select stu).Single();
12
13 //修改实体类属性
14 student1.Dept ="软件工程";
15 student1.Age +=1;
16
17 //提交修改到数据库
18 dc.SubmitChanges();
19
20 Console.Read();
iii.结果:
从运行结果中可以看到Name列参与了冲突检测,Age列和Dept列因为修改后属性值与原始值不同,所以也参与到并发冲突的检测中,而Sex列却没有参加任何检测。接下来说说,假如发生了并发冲突的情况,那么该如何进行处理。
冲突检测发生在DataContext对象调用SubmitChanges方法时,此方法可以接受一个ConflictMode类型的枚举值作为参数,用来指定在将更改的数据保存到数据库过程中发生并发冲突时的处理策略。它包含FailOnFirstConflict和ContinueOnConflict两个值,其中,FailOnFirstConflict指示在修改的数据保存到数据库过程中第一次发生并发冲突时,SubmitChanges方法就抛出异常,不再继续更新;而ContinueOnConflict指示发生并发冲突时继续更新其他的数据,最后抛出一个全部更新失败的异常。在并发冲突发生时,SumbitChanges抛出类型为ChangeConflictException的异常对象,通过在代码中捕获该异常对象,就可以对其进行并发冲突的逻辑处理。其实LINQ TO SQL中提供了Resolve和ResolveAll方法来帮助实现并发冲突处理功能,在使用这两个方法时,要指定一个RefreshMode枚举类型的参数来指示处理并发冲突的策略,RefreshMode枚举类型包含KeepChanges、KeepCurrentValues和OverWriteCurrentValues三个值。而这三个值具体的作用说明可以参考之前所讲过的DataContext对象成员方法Refresh,这里就不在详细说明了。在用Resolve和ResolveAll方法查询数据库,并获取数据库最新数据时,保存在DataContext对象中的实体类对象原始值将会自动更新,一旦更新完成,那么冲突条件就不再存在,除非发生新的冲突。下面通过示例来进一步说明(注:实体类Student代码不变):
i.并发访问冲突处理:
1 staticvoid Main(string[] args)
2 {
3 string str_conn =@"Data Source=localhost;Initial Catalog=DB_Student;User ID=sa;Password=king";
4 DataContext dc =new DataContext(str_conn);
5
6 var students = dc.GetTable<Student>();
7 //把SQL语句输出在控制台
8 dc.Log = Console.Out;
9
10 var student = (from stu in students
11 where stu.Name =="刘麻子"
12 select stu).Single();
13
14 //修改实体类对象属性
15 student.Name ="张麻子";
16
17 //模拟并发访问
18 SqlConnection conn =new SqlConnection(str_conn);
19 SqlCommand cmd = conn.CreateCommand();
20 cmd.CommandText ="update student set sname='刘晓光' where sname='刘麻子'";
21 cmd.CommandType = System.Data.CommandType.Text;
22 conn.Open();
23 cmd.ExecuteNonQuery();
24 conn.Close();
25
26 //提交更改数据到数据库
27 try
28 {
29 dc.SubmitChanges(ConflictMode.ContinueOnConflict);
30 }
31 catch (ChangeConflictException)
32 {
33
34 foreach (var c in dc.ChangeConflicts)
35 {
36 foreach (var m in c.MemberConflicts)
37 {
38 //记录并发冲突的数据信息
39 var exception =new
40 {
41 MemberName = m.Member.Name,
42 CurrentValue = m.CurrentValue,
43 DataBaseValue = m.DatabaseValue,
44 OriginValue = m.OriginalValue
45 };
46 Console.WriteLine("并发冲突数据信息:");
47 Console.WriteLine(exception.ToString()+"\n");
48
49 //并发冲突处理
50 m.Resolve(RefreshMode.KeepCurrentValues);
51 }
52
53 }
54 //或
55 //dc.ChangeConflicts.ResolveAll(RefreshMode.KeepCurrentValues);
56 }
57 finally
58 {
59 //再次提交
60 try
61 {
62 dc.SubmitChanges(ConflictMode.ContinueOnConflict);
63 }
64 catch (ChangeConflictException e)
65 {
66 Console.WriteLine(e);
67 }
68 }
69
70 Console.Read();
71 }
ii.结果:
从结果中可以看到程序分两次执行更新语句向数据库提交。
2.悲观并发:
同样地,从字面意思来看悲观并发就是对并发冲突的发生持有悲观的态度,认为程序运行的大部分时间里都可能发生并发冲突。所以在修改数据库中的数据时,要将其锁定,只允许一个用户访问并修改,且直到该用户修改完成了,才对该数据解除锁定。悲观并发不会发生并发冲突,而一般悲观并发要在数据库用户访问数量比较少情况下才适合。那该如何用悲观并发来避免并发冲突的发生呢,其实很简单,就是把要操作的数据代码包含在一个事务里。下面通过示例进一步说明:
i.事务处理来避免并发冲突:
1 staticvoid Main(string[] args)
2 {
3 string str_conn =@"Data Source=localhost;Initial Catalog=DB_Student;User ID=sa;Password=king";
4 DataContext dc =new DataContext(str_conn);
5
6 //使用事务将数据库中的记录锁定
7 using (TransactionScope t1 =new TransactionScope())
8 {
9 var students = dc.GetTable<Student>();
10
11 var student = (from stu in students
12 where stu.Name =="刘麻子"
13 select stu).Single();
14 //修改实体类对象
15 student.Name ="张麻子";
16
17 //模拟并发访问
18 using (TransactionScope t2 =new TransactionScope(TransactionScopeOption.RequiresNew))
19 {
20 SqlConnection conn =new SqlConnection(str_conn);
21 SqlCommand cmd = conn.CreateCommand();
22 cmd.CommandText ="update student set sname='刘晓光' where sname='刘麻子'";
23 cmd.CommandType = System.Data.CommandType.Text;
24 conn.Open();
25 cmd.ExecuteNonQuery();
26 conn.Close();
27 //事务2提交
28 t2.Complete();
29 }
30
31 //提交数据修改到数据库
32 dc.SubmitChanges();
33 //事务1提交
34 t1.Complete();
35 }
36
37 Console.Read();
38 }
ii.结果:
原因在于事务1将数据库中数据锁住,进行数据修改,但并未提交事务,所以导致事务2一直等待事务1结束,而事务1的结束必须要等事务2结束,所以事务2不可能会有机会来执行,所以事务2最终等待超时抛出异常。