通过删除/创建SQL Server视图导致登录到ASP应用程序时出现死锁

Mic*_*345 12 c# sql-server reporting-services

我一直在追逐这个问题一天,我很难过,所以我想我会把它给你们一些灵感.对于死锁和SQL Server锁定模式,我有点新手,我很少需要深入研究.

短篇小说:

当用户登录我们的应用程序时,我们希望基于他们现在具有"会话"的事实来更新SQL Server视图,以便当他们随后基于报表模型运行SQL Server Reporting Services报表时,它包含安全性他们的会话设置.

我注意到的常规死锁发生在DROPs和reCREATE视图的过程(我称之为AuthRuleCache)和尝试从视图中选择的Microsoft SQL Server Reporting Services 2008(SSRS)报告之间.

如果我正确读取SQL事件探查器死锁事件,AuthRuleCache有一个Sch-M锁,报告有一个IS锁.

AuthRuleCache代码是DotNet程序集中的C#,它是在用户登录我们的经典ASP应用程序时执行的.

显然我想避免死锁,因为它阻止了登录 - 我不介意我如何实现这一点,只要我不需要妥协任何其他功能.我已经完全控制了AuthRuleCache和数据库,但我会说我们对企业DBA专业知识"轻松".

以下是SQL事件探查器的示例死锁事件:

<deadlock-list>
 <deadlock victim="process4785288">
  <process-list>
   <process id="process4785288" taskpriority="0" logused="0" waitresource="OBJECT: 7:617365564:0 " waittime="13040" ownerId="3133391" transactionname="SELECT" lasttranstarted="2013-01-07T15:16:24.680" XDES="0x8005bd10" lockMode="IS" schedulerid="8" kpid="20580" status="suspended" spid="83" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2013-01-07T15:15:55.780" lastbatchcompleted="2013-01-07T15:15:55.780" clientapp=".Net SqlClient Data Provider" hostname="MYMACHINE" hostpid="1176" loginname="MYMACHINE\MyUser" isolationlevel="read committed (2)" xactid="3133391" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="adhoc" line="2" stmtstart="34" sqlhandle="0x02000000bd919913e43fd778cd5913aabd70d423cb30904a">
SELECT
    CAST(1 AS BIT) [c0_is_agg],
    1 [agg_row_count],
    COALESCE([dbo_actions2].[ActionOverdue30days], 0) [ActionOverdue30days],
    COALESCE([dbo_actions3].[ActionOverdueTotal], 0) [ActionOverdueTotal],
    COALESCE([dbo_actions4].[ActionOverdue90daysPLUS], 0) [ActionOverdue90daysPLUS],
    COALESCE([dbo_actions5].[ActionOverdue60days], 0) [ActionOverdue60days],
    COALESCE([dbo_actions6].[ActionOverdue90days], 0) [ActionOverdue90days],
    COALESCE([dbo_actions7].[ActionPlanned30days], 0) [ActionPlanned30days],
    COALESCE([dbo_actions8].[ActionPlanned60days], 0) [ActionPlanned60days],
    COALESCE([dbo_actions9].[ActionPlanned90days], 0) [ActionPlanned90days],
    COALESCE([dbo_actions10].[ActionPlanned90daysPLUS], 0) [ActionPlanned90daysPLUS],
    COALESCE([dbo_actions11].[ActionPlannedTotal], 0) [ActionPlannedTotal],
    CASE WHEN [dbo_actions12].[CountOfFilter] > 0 THEN 'Overdue0-30days' WHEN [dbo_actions13].[CountOfFilter] > 0 THEN 'Overdue90daysPlus' WHEN [dbo_actions5].[Count     </frame>
    </executionStack>
    <inputbuf>
  SET DATEFIRST 7
  SELECT
    CAST(1 AS BIT) [c0_is_agg],
    1 [agg_row_count],
    COALESCE([dbo_actions2].[ActionOverdue30days], 0) [ActionOverdue30days],
    COALESCE([dbo_actions3].[ActionOverdueTotal], 0) [ActionOverdueTotal],
    COALESCE([dbo_actions4].[ActionOverdue90daysPLUS], 0) [ActionOverdue90daysPLUS],
    COALESCE([dbo_actions5].[ActionOverdue60days], 0) [ActionOverdue60days],
    COALESCE([dbo_actions6].[ActionOverdue90days], 0) [ActionOverdue90days],
    COALESCE([dbo_actions7].[ActionPlanned30days], 0) [ActionPlanned30days],
    COALESCE([dbo_actions8].[ActionPlanned60days], 0) [ActionPlanned60days],
    COALESCE([dbo_actions9].[ActionPlanned90days], 0) [ActionPlanned90days],
    COALESCE([dbo_actions10].[ActionPlanned90daysPLUS], 0) [ActionPlanned90daysPLUS],
    COALESCE([dbo_actions11].[ActionPlannedTotal], 0) [ActionPlannedTotal],
    CASE WHEN [dbo_actions12].[CountOfFilter] > 0 THEN 'Overdue0-30days' WHEN [dbo_actions13].[CountOfFilter] > 0 THEN 'Overdue90daysPlus' WHEN [db    </inputbuf>
   </process>
   <process id="process476ae08" taskpriority="0" logused="16056" waitresource="OBJECT: 7:1854941980:0 " waittime="4539" ownerId="3132267" transactionname="user_transaction" lasttranstarted="2013-01-07T15:16:18.373" XDES="0x9a7f3970" lockMode="Sch-M" schedulerid="7" kpid="1940" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2013-01-07T15:16:33.183" lastbatchcompleted="2013-01-07T15:16:33.183" clientapp=".Net SqlClient Data Provider" hostname="MYMACHINE" hostpid="14788" loginname="MYMACHINE\MyUser" isolationlevel="read committed (2)" xactid="3132267" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="adhoc" line="3" stmtstart="202" stmtend="278" sqlhandle="0x02000000cf24d22c6cc84dbf398267db80eb194e79f91543">
  DROP VIEW [sec].[actions_authorized]     </frame>
    </executionStack>
    <inputbuf>

  IF EXISTS ( SELECT * FROM sys.VIEWS WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized]'))
  DROP VIEW [sec].[actions_authorized]
      </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <objectlock lockPartition="0" objid="617365564" subresource="FULL" dbid="7" objectname="617365564" id="lock932d2f00" mode="Sch-M" associatedObjectId="617365564">
    <owner-list>
     <owner id="process476ae08" mode="Sch-M"/>
    </owner-list>
    <waiter-list>
     <waiter id="process4785288" mode="IS" requestType="wait"/>
    </waiter-list>
   </objectlock>
   <objectlock lockPartition="0" objid="1854941980" subresource="FULL" dbid="7" objectname="1854941980" id="locke6f0b580" mode="IS" associatedObjectId="1854941980">
    <owner-list>
     <owner id="process4785288" mode="IS"/>
    </owner-list>
    <waiter-list>
     <waiter id="process476ae08" mode="Sch-M" requestType="convert"/>
    </waiter-list>
   </objectlock>
  </resource-list>
 </deadlock>
</deadlock-list>
Run Code Online (Sandbox Code Playgroud)

长篇故事:

我决定将此作为问答.

问:为什么必须经常更改架构以强制执行报告安全性?

答:嗯,我只是通过这种方法得出的,因为我们的SSRS报告机制完全基于报告模型,我们的应用程序通过应用规则支持行级安全性.规则本身在数据库中定义为少量SQL片段.这些片段在运行时重新组装并基于a)用户是谁,b)他们想要做什么,以及c)他们想要做什么来应用.因此,每个用户可以基于适用于他们的规则具有唯一的数据视图.我们有用户创作和保存他们自己的报告,因此我希望在模型中强制执行此安全性,以防止他们绊倒他们无法访问的数据.

我们在报告模型中面临的挑战是它们基于数据源视图(DSV),该视图只能由静态源组成,例如表,命名查询,视图.您无法将一些C#代码注入DSV,以使其动态响应运行报告的特定用户.您确实在模型(SMDL)上获得了UserID,因此您可以使用它来进行过滤.我们的解决方案是让DSV公开包含所有当前登录用户的唯一规则集(即AuthRuleCache)的所有数据的视图,然后SMDL将其过滤回请求用户的唯一规则集.嘿,你在SSRS报告模型中有动态的行级,基于规则的安全性!

规则不经常更改,因此在用户会话期间,这些规则的行为方式相同.因为我们有几十个用户,但是在24小时内只能登录几百个,我决定在用户​​登录时刷新AuthRuleCache并在24小时后过期,因此它只包含安全信息具有当前会话的用户.

问:AuthRuleCache采用什么形式?

答:这是一个观点UNIONing其他观点.每个用户都有自己的视图,例如widgets_authorized_123,其中小部件是包含受保护数据的表,123是用户ID.然后,有一个主视图(例如widgets_authorized),UNION将所有用户视图放在一起

问:这听起来非常低效,你是白痴吗?

答:可能 - 但是由于SQL查询处理器的强大功能,它似乎对于实时用户报告运行良好而快速.我尝试使用缓存表来实际保存记录ID以便与应用程序安全性一起使用,并发现这导致了膨胀表并延迟刷新和从缓存中读取.

问:好的,你可能仍然是个白痴,但让我们探索另一种选择.您是否可以异步重建AuthRuleCache而不是让用户在登录时等待?

答:嗯,用户登录后首先看到的是包含基于模型的报告的仪表板 - 因此我们需要在登录后立即启动并运行安全规则.

问:您是否探索过不同的锁定模式和隔离级别?

答:排序 - 我尝试启用更改数据库read_committed_snapshot ON但这似乎没有任何区别.回想起来,我认为我正在尝试执行DROP/CREATE VIEW并需要Sch-M锁定这一事实意味着Read Committed Snapshot Isolation(RCSI)无济于事,因为它是关于处理DML语句的并发性,而我做DDL.

问:您是否为了报告目的而探索了整个数据库数据库快照或镜像?

答:我不会排除这种情况,但我希望更多的是以应用为中心的解决方案,而不是进行基础设施的改变.这将是资源利用和维护开销的急剧增长,我需要升级到其他人.

问:还有什么我们应该知道的吗?

答:是的,AuthRuleCache刷新过程包含在一个事务中,因为我想让没有人看到不完整/无效缓存的父亲,例如当widget_authorized_123因为用户的会话已经过期而被删除时,widget_authorized_123指向widget_authorized_123.我在没有事务的情况下进行了测试,并且死锁已停止,但我开始从SQL事件探查器获取阻止的进程报告.我在登录时看到了大约15秒的延迟,有时超时 - 所以把交易放回去了.

问:这种情况多久发生一次?

答:AuthRuleCache目前在生产环境中关闭,因此不会影响用户.我对100个连续登录的本地测试显示可能有10%的死锁或失败.我怀疑对于在其仪表板上具有基于报表模型的长期报表的用户来说情况更糟.

问:报告快照怎么样?

答:也许是一种可能性 - 不确定这与参数化报告的效果如何.我担心的是,我们确实有一些用户如果插入记录就会惊慌,但直到半小时后才在仪表板上看到它.此外,我不能总是保证每个人都会正确使用报告快照,所以不要让门打开僵局,以便在以后偷偷溜回来.

问:我可以看到AuthRuleCache刷新事务的完整T-SQL吗?

答:以下是从SQL事件探查器捕获的一个事务中为一个登录用户发出的语句:

查找过期的会话 - 如果找到,我们将删除关联的视图

SELECT TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA + '.' + TABLE_NAME LIKE 'sec.actions_authorized_%'
  AND RIGHT(TABLE_NAME, NULLIF(CHARINDEX('_', REVERSE(TABLE_NAME)), 0) - 1) NOT IN (
    SELECT DISTINCT CAST(empid AS NVARCHAR(20))
    FROM session
    )
Run Code Online (Sandbox Code Playgroud)

删除用户'myuser'的任何预先存在的视图,ID 298

IF EXISTS (
    SELECT *
    FROM sys.VIEWS
    WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized_298]')
    )
  DROP VIEW [sec].[actions_authorized_298]
Run Code Online (Sandbox Code Playgroud)

为用户ID 298创建一个视图

CREATE VIEW [sec].[actions_authorized_298]
AS
SELECT actid
  ,'myuser' AS username
FROM actions
WHERE actid IN (
    SELECT actid
    FROM actions
    WHERE (
        --A bunch of custom where statements generated from security rules in the system prior to this transaction starting
    )
Run Code Online (Sandbox Code Playgroud)

获取操作实体的所有用户特定视图的列表

SELECT TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA + '.' + TABLE_NAME LIKE 'sec.actions_authorized_%'
Run Code Online (Sandbox Code Playgroud)

删除现有的主动作视图

IF EXISTS (
    SELECT *
    FROM sys.VIEWS
    WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized]')
    )
  DROP VIEW [sec].[actions_authorized]
Run Code Online (Sandbox Code Playgroud)

创建一个新的主动作视图,我们就完成了

CREATE VIEW [sec].[actions_authorized]
AS
SELECT actid
  ,username
FROM sec.actions_authorized_182    
UNION
SELECT actid
  ,username
FROM sec.actions_authorized_298
UNION
-- Repeat for a bunch of other per-user custom views, generated from the prior select
-- ...
Run Code Online (Sandbox Code Playgroud)

Mic*_*345 1

感谢所有提供建议的人。我已经确定了一个我认为对我们有用的解决方案。我可能需要一段时间才能得到最终的代码,但我已经做了一些测试,看起来很积极 - 我想用我计划的方法来结束这个问题。

首先,僵局是我从一开始就试图做的事情的完全适当的结果。据我了解,重新创建视图需要模式修改锁 - 从该视图读取数据的任何过程都需要模式稳定性锁。根据时间的不同,这些竞争锁会导致繁忙期间大约 10% 的登录尝试出现死锁。

当我在运行视图删除/重新创建之前更改代码以执行 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 操作时,死锁消失了,因为它对并发发生的情况有更多限制,牺牲了响应速度以换取稳定性。

不幸的是,我看到的不是死锁,而是阻塞的进程报告,其中进程等待了 10 秒以上才能获取必要的锁。还是没有真正解决我的问题。

我重新思考了我的“奇怪的解决方案”,即使用大的 UNIONed 视图来组合多个视图。让我明确一点,我并不是自愿选择这种方法,我只是试图解决 SSRS 报告模型中的一个限制,即您无法在模型底层的表/命名查询中实现参数。

我在 MS 文档中发现,分区视图在将多个表中的行合并到单个视图中时可以使用类似的结构,例如: http: //msdn.microsoft.com/en-us/library/ms190019(v=sql. 105).aspx

所以我并不是唯一一个以这种方式使用视图的人。我需要这个 UNIONed 视图,但是删除和重新创建视图将成为性能问题。因此,我使用 Service Broker 做了一些测试,发现我可以对视图删除/重新创建操作进行排队,从而允许用户快速登录,而无需等待 DDL 完成。我将遵循 @usr 的建议,使事务尽可能精简,将对于完成登录不重要的内容(例如过期的旧会话)移出事务。