先看测试代码:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var random = new Random().Next(1,10000); 6 for (var i = 0; i < 2; i++) 7 { 8 var thread = new Thread((o) => 9 { 10 var id = (int) o; 11 Process(id); 12 }); 13 thread.Start(i + random); 14 } 15 16 Console.ReadLine(); 17 } 18 19 private static void Process(int accountId) 20 { 21 try 22 { 23 using (var scope = new TransactionScope()) 24 { 25 using (var db = new TestDbContext()) 26 { 27 var nothing = db.MpUsers.FirstOrDefault(x => x.AccountId == -1); 28 Thread.Sleep(1000); 29 var user = new MpUser() 30 { 31 AccountId = accountId 32 }; 33 db.MpUsers.Add(user); 34 db.SaveChanges(); 35 } 36 scope.Complete(); 37 Console.WriteLine(accountId + ": done"); 38 } 39 } 40 catch (DbUpdateException ex) 41 { 42 Console.WriteLine(accountId + ": " + ex.InnerException.InnerException.Message); 43 } 44 } 45 46 47 }
上面的代码先执行查询(返回空),然后在执行插入,先执行的那个线程一定会抛异常。请问是为什么呢?
异常信息:事务(进程 ID 56)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。
但是,当把
var nothing = db.MpUsers.FirstOrDefault(x => x.AccountId == -1);
替换为:
var nothing = db.MpUsers.FirstOrDefault(x => x.Id == -1);
之后,虽然都返回空,但是却不再报死锁的错误,两个线程都可以正确执行了。这又是为什么呢?
当把var nothing = db.MpUsers.FirstOrDefault(x => x.AccountId == -1);
替换为var nothing = db.MpUsers.FirstOrDefault(x => x.AccountId == 1);
因为找到了AccountId为1的记录,不返回空的时候两个线程又可以正确执行了。
不用TransactionScope的情况呢
不用transactionscope不会报错
@Leo C.W: 通过id查询是不会锁表的.因为有主键索引.
通过没有索引的字段查询.会锁表.
另外这个和ef没关系.是数据库锁的问题.
@吴瑞祥: 同样的数据结构,同样的代码,我用java hibernate连mysql就没有死锁的问题,难道是sql server这么弱吗?
@Leo C.W: 说明mysql和hibernate的事务管理有问题.
死锁不死锁和数据库不应该有关系.是写sql的人操作不当导致.
应该死锁的地方不死锁.反而不好.
@吴瑞祥: 但上面的C#代码这种情况不应该死锁啊,因为这种需求很常见,我先从数据库查看是否已存在该username,不存在就插入新纪录。
@Leo C.W: 所以你的问题处在不理解这个操作为什么死锁上.而我上面已经解释了.这个操作为什么会死锁.
想不让他死锁.别用TransactionScope就好.
PS:ef要实现这个逻辑很麻烦.得先读.再写.然后再读.然后有多条再保留最后一条删除前面的
mysql差不多也是这样的.sqlserver有一个专门的mergin指令实现这个逻辑
@吴瑞祥: 我现在反正是明白了,只要transactionscope里面有非主键查询,并且查询结果返回null,同时又有增/删/改,那就一定会死锁(多线程同时操作时)。但是就想搞明白其中的内部逻辑。大神有没有推荐的文章可以拜读一下?
@吴瑞祥: 或者大神能不能写篇博文,专门研究transactionscope是如何导致死锁的,以及最佳解决之道?
@Leo C.W: 我上面不是说了.
你用的事务级别的问题.如果没有索引.他会全表扫描.所以会把整个表锁起.
没什么复杂的原因.关键词就是:事务/事务级别/索引/全表扫描/索引对锁的影响.
@吴瑞祥: 好像跟索引没有关系哦,根据我的测试,只要不是在主键上的查询,就死锁,包括在其他索引列上的查询。
@Leo C.W: 你详细的看下事务级别的文档吧.里面有讲.什么时候锁什么.上面关于索引的说法确实是不对.