在一個CS結構的項目里使用SQLServer時碰到一個有意思的現象,以下是從日志中摘出來的用戶操作:
用戶A的操作會引發程序在事務中使用Local臨時表,例如:
1 BEGIN TRAN 2 3 SELECT * INTO #temp FROM DB1.dbo.Table1 4 5 --do something 6 7 DROP TABLE #temp 8 9 COMMIT TRAN
用戶B的操作也會引發程序使用臨時表且在刪除臨時表前會先判斷臨時表的存在性,例如:
1 SELECT * INTO #MyTempTable FROM TestJJV9.dbo.test 2 3 IF EXISTS(SELECT 1 FROM tempdb.dbo.sysobjects WHERE name like '%#MyTempTable%') 4 BEGIN 5 DROP TABLE tempdb.dbo.#MyTempTable 6 END
用戶A和用戶B使用的是同一個SQLServer實例,但是用的是不同的數據庫。
那么,現象來了,用戶A的操作越頻繁,用戶B越容易碰到長時間的等待甚至超時。
分析:
從用戶B的操作來看,判斷臨時表存在性的SQL語句(line3)導致了對tempdb.dbo.sysobjects的掃描(sysobjects是一個view,實際上掃描的是sys.sysschobjs的聚集索引),該掃描需要對sys.sysschobjs的聚集索引申請S鎖。
而用戶A的操作會對sys.sysschobjs表的聚集索引的KEY持有一個X鎖,在用戶A的事務提交之前,用戶B無法獲得S鎖,所以處於等待狀態。
如果有C,D,E等用戶持續在執行類似用戶A的操作,那么用戶B基本上就只能等死了。。。
解決方案:
使用OBJECT_ID判斷臨時表的存在性,可以避免對sys.sysschobjs的掃描,防止被鎖(猜想其內部可能是從類似緩存的結構中取得的結果),例如:
SELECT * INTO #MyTempTable FROM DB2.dbo.AnyTable IF OBJECT_ID('tempdb.dbo.#MyTempTable') IS NOT NULL BEGIN DROP TABLE tempdb.dbo.#MyTempTable END
另外,上述用戶A的操作無論生成的是Local臨時表還是Global臨時表,都有這個現象,原因是一樣的。
最后轉載一個即時調查當前服務器狀況的SP,要調查類似上述問題時,可以如下使用:
exec sp_WhoIsActive @get_locks = 1, @find_block_leaders = 1
從blocking_session_id, blocked_session_count, locks列可以看出誰正在被誰阻塞