一个典型的数据库操作事务死锁分析

【表A】与【表B】之间有外键约束(具体怎么约束的无所谓,因为外键和事务死锁没有绝对关系)。【表A】=主键表,【表B】=外键表。
公司有几位程序员写的代码总是出现死锁,现在将事务死锁情况重现.

using(事务)
{
    
try{
        
for()//一个循环
        {
            
if(查询【表A】有该【记录】==false)//这个查询没有用当前事务的数据库连接,而是新开一个数据库连接查询数据库
            {
                将【记录】插入【表A】;
                插入【表B】;
            }
        }
        事务.提交();
    }
catch{
        事务.回滚();
    }
}

 

4月25日早9:00补充:以上代码逻辑如果放在一个存储过程里面,是没有问题的。但是放在C#中,程序员稍不注意,就很可能出现问题。出现问题的关键地方在“查询【表A】”。C#中,如果查询【表A】和当前的事务不是同一个数据库连接,而是新开一个连接,就会死锁(可以看留言中我的回复)。所以:要么
1,查询【表A】改为使用当前事务的数据库连接(推荐用这种方法)
2,要么按下面的进行代码改造,将事务移到循环体内。
当循环只有1次的时候,上面代码运行没有问题。
一旦循环大于1次的时候,死锁立即出现,运行SQL Server 2005的sp_who_lock,发现死锁的地方正是“查询【表A】”这块。为什么呢?
因为:第1次循环,插入【表A】后,事务将【表A】设置了独占锁,但是第1次循环完后,事务并没有提交,也就没有解开【表A】上的独占锁。因为表A被独占锁了,所以第2次循环时,“查询【表A】”这个操作进行不下去(后面的插入【表A】更是如此),一直在等待事务提交以解开锁,但是事务运行到第2次循环的查询【表A】就死了,循环无法继续进行,也就不能运行到循环外边的事务.提交(),【表A】的独占锁永远没法解开死锁就这样产生了。

既然知道了死锁的原因是因为循环里面没有解开独占锁,所以我们应该把事务.提交()放置在循环内。另外根据事务体的逻辑尽量少的原则,我们把事务的声明移植循环体内,使事务体的代码行数尽量少。代码如下。

 

for()//一个循环
{
    
bool 有记录=查询【表A】有该【记录】;//将这个查询移出事务是良好的编码习惯,虽然这里不必要
    using(事务)
    {        
        
try{
            
if(有记录==false)
            {
                将【记录】插入【表A】;
                插入【表B】;
            }
            事务.提交();
        }
catch{
            事务.回滚();
        }
    }
}

经过以上改造之后,几位程序员写的业务运行正常。

由此总结:
1,在C#等程序代码中使用事务,并在事务内进行查询的时候,特别要小心,确保该查询和事务使用的是同一个数据库连接。防止表被独占死锁。
2,事务体内应尽可能少的逻辑,尽可能少的代码行数。
3,4月25日9:00补充:防止事务死锁最好的方法,还是大家推荐的用存储过程,在存储过程里面使用事务(只不过门槛稍高,手工活儿稍多)

本博客所有随笔,若未明确标示转载或带有原文链接,皆为原创。
本博客所有随笔版权归博客园和kai.ma所有,欢迎转载,转载请保留:
  • 出处:http://kaima.cnblogs.com
  • 作者:kai.ma
Tag标签: 事务,死锁
posted @ 2008-04-24 14:24 Kai.Ma 阅读(1637) 评论(22)  编辑 收藏 网摘 所属分类: 数据库

  回复  引用    
#1楼2008-04-24 14:37 | 3176[未注册用户]
既然存在于同一个事务上下文,这情况应该不会有总问题才对...
  回复  引用  查看    
#2楼[楼主]2008-04-24 14:40 | Kai.Ma      
@3176
可是现实很不幸,一碰到循环,事务死锁。我之前为这个问题也苦恼了很久。

  回复  引用  查看    
#3楼2008-04-24 14:43 | 李战      
http://www.cnblogs.com/Emoticons/qface/055243929.gif" alt="" />俺敢打赌,楼主所说的出现死锁的问题一定是其他原因造成的!http://www.cnblogs.com/Emoticons/msn/shades_smile.gif" alt="" />

试想一下,如果那个循环里的操作确实不能分割,请问楼主该怎么办呢?

俺现在是砖家了http://www.cnblogs.com/Emoticons/QQ/laf.gif" alt="" />

  回复  引用  查看    
#4楼2008-04-24 14:45 | Marklee      
using (OMTransactionScope Tran = new OMTransactionScope())
{
foreach (Entity entity in this)
{
entity.DB_InsertEntity();
}
Tran.Complete();
}
我系统里用到的框架里就有这样的代码,用的Orcale数据库,表之间没有外键约束,却没有此类问题
我想这个问题的产生应该跟外键约束有点关系

  回复  引用  查看    
#5楼[楼主]2008-04-24 14:51 | Kai.Ma      
@Marklee
我用的SQL server 2005数据库,不知道Orcale和SQL server 2005是否对事务以及锁的处理有差异?


  回复  引用  查看    
#6楼2008-04-24 15:04 | PerfectDesign      
@Marklee
你的并没有涉及2个以上的表,怎么出现死锁呢?

@lz
像这种代码,一般写在C#里面,实在是太浪费资源了。
建议将整个逻辑封装到数据库SP里。

另外,2000下沿用的sp_who_lock 到了2005,最好用dm_tran_locks 这个动态视图,05下的新东西

最后,说句老乡好,呵呵!

  回复  引用  查看    
#7楼[楼主]2008-04-24 15:04 | Kai.Ma      
@李战
欢迎欢迎。:D
如果那个循环里的操作确实不能分割
我的想法是用嵌套循环(本来打算下一节讨论的,看来不用了:D)。代码大概如下:

不知道是否可行?
using(事务)
{
    try{
        ...
        for(...)
        {
            using(子事务)
            {
                try{
                    ...
                    子事务.提交();
                }
                catch{
                    子事务.回滚();
                    throw;//抛给外层try catch捕捉,引发外层事务回滚
                }
            }
        }
        }
        事务.提交();
    }
    catch{
        事务.回滚();
    }
}

  回复  引用  查看    
#8楼[楼主]2008-04-24 15:10 | Kai.Ma      
@PerfectDesign
老乡好,呵呵。园子里老乡挺多的呵呵。
多谢你的建议,dm_tran_locks很好用。

  回复  引用  查看    
#9楼2008-04-24 15:11 | PerfectDesign      
@李战
我也对楼主的死锁分析也有异议。

即使在事务内循环查找和插入,但是同一个事务的内部还是不会对内部语句造成的锁进行阻塞。

  回复  引用  查看    
#10楼2008-04-24 15:13 | PerfectDesign      
死锁应该会是因为其他原因造成的,可能是并行的两个程序,一个程序锁住了A表,请求B,另一个程序锁住了B请求A了。

你可以再查看一下dm_tran_locks的锁类型,在竞争哪个资源的时候出现了死锁

  回复  引用  查看    
#11楼2008-04-24 15:16 | 侯垒      
我感觉也不会出现死锁的呀!
  回复  引用  查看    
#12楼[楼主]2008-04-24 15:21 | Kai.Ma      
我再深入查一查,可能真的是其他地方。先从首页撤下吧。
  回复  引用    
#13楼2008-04-24 15:32 | 在线代理[未注册用户]
本来不参合的,看着是老乡,就过来顶一下。
  回复  引用  查看    
#14楼2008-04-24 15:33 | PerfectDesign      
begin tran
declare @x int
set @x = 0;
while @x<100
begin
select top 1 * from test1
insert into test2 values (@x,'asd','adsqw')
insert into test3 values (@x,'asdqwe','asdqwe')
set @x = @x+1;
end
commit tran;

这个存储过程模拟了上面的CS代码,但是执行起来并没有死锁。

  回复  引用  查看    
#15楼2008-04-25 01:40 | Marklee      
我下午把那段代码运行在了SQL数据库下,没有死锁,估计还是还别的地方有关系

@PerfectDesign
LZ的意思对同个表插入的时候,因为事务没提交因而死锁了,我是针对这个来说的
"因为:第1次循环,插入【表A】后,事务将【表A】设置了独占锁,但是第1次循环完后,事务并没有提交,也就没有解开【表A】上的独占锁。因为表A被独占锁了"

@LZ
如果有分析结果,请发个文章说明下,3Q了

  回复  引用  查看    
#16楼2008-04-25 09:18 | PerfectDesign      
@Marklee
程序里是被catch了的,有错误会回滚
如果事务没有回滚,那么应该是整个程序崩溃,循环也无法继续的

  回复  引用  查看    
#17楼[楼主]2008-04-25 09:19 | Kai.Ma      
为什么“C#中死锁,而存储过程中不死锁”的原因我搞明白了。

如果是在存储过程中测试,不会死锁,因为同一段存储过程中,使用事务,使用的是同一个数据库连接。

而在C#代码中事务体内可以插入和事务本身毫无关系的查询,这样在独占锁没有解开之前,新开一个和当前事务毫无关系的查询,肯定会死。
最后我更新了日志。

  回复  引用  查看    
#18楼[楼主]2008-04-25 09:33 | Kai.Ma      
@李战
果然,被你说中了。呵呵

  回复  引用  查看    
#19楼2008-04-25 09:33 | PerfectDesign      
@Kai.Ma
恩,这个也就是为什么说肯定和其他查询有关系。
另外,不管是C#代码的事务还是存储过程的事务,在sqlserver内部都是一个本质。只是因为你的程序员把事务写在了C#里面,自然加长了事务的时间,并发情况下肯定跟其他事物发生了资源竞争。
不过这个问题可以使用保持事务资源的一致性访问上得到比较好的解决。

  回复  引用  查看    
#20楼[楼主]2008-04-25 09:41 | Kai.Ma      
@PerfectDesign
是的,经过俺老乡的指点,我和我的程序员们,更深刻地理解了事务和死锁了。希望以后园子里多多交流:D

  回复  引用  查看    
#21楼2008-04-25 09:44 | PerfectDesign      
指点谈不上,我只是喜欢凑热闹
多交流 ^_^

  回复  引用  查看    
#22楼2008-11-14 23:17 | Silent Void      
按我自己的理解,这应该算是SQL执行被阻塞了,而不是数据库死锁;我在下面这篇文章后面的示例中,分析了楼主文中所描述的情况,并提出了6中解决方法,楼主可以参考下。
http://www.cnblogs.com/happyhippy/archive/2008/11/14/1333922.html" target="_new">http://www.cnblogs.com/happyhippy/archive/2008/11/14/1333922.html




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1167550




相关文章:

相关链接: