13

我一直在追这个问题一天,很困惑,所以我想我会把它发给你们,以获得一些灵感。当谈到死锁和 SQL Server 锁定模式时,我有点新手,我很少需要深入研究。

短篇小说:

当用户登录到我们的应用程序时,我们希望根据他们现在拥有“会话”这一事实来更新 SQL Server 视图,以便当他们随后基于报表模型运行 SQL Server Reporting Services 报表时,它包括安全性他们的会话设置。

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

如果我正确读取 SQL Profiler 死锁事件,则 AuthRuleCache 具有 Sch-M 锁,并且报告具有 IS 锁。

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

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

下面是一个来自 SQL Profiler 的死锁事件示例:

<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>

长篇大论:

我决定将其作为问答。

问:为什么您必须频繁更改架构以强制报告安全性?

A:嗯,我之所以采用这种方法,是因为我们的 SSRS 报告机制完全基于报告模型,并且我们的应用程序通过应用规则来支持行级安全性。规则本身在数据库中定义为小的 SQL 片段。这些片段在运行时重新组合,并根据 a) 用户是谁、b) 他们想要做什么以及 c) 他们想要做什么来应用。因此,每个用户都可能拥有基于适用于他们的规则的独特数据视图。我们有用户创作和保存他们自己的报告,所以我希望在模型中强制执行这种安全性,以防止他们偶然发现他们不应该访问的数据。

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

规则很少更改,因此在用户会话期间这些规则的行为方式相同是可以的。因为我们有成千上万的用户,但在 24 小时内只有几百个左右的用户可以登录,所以我决定在用户​​登录时刷新 AuthRuleCache 并在 24 小时后将其过期,因此它只包含以下安全信息具有当前会话的用户。

问:AuthRuleCache 采用什么形式?

A:这是一种结合了许多其他观点的观点。每个用户都有自己的视图,例如widgets_authorized_123,其中widgets 是包含受保护数据的表,123 是用户ID。然后,有一个主视图(例如widgets_authorized)将所有用户视图联合在一起

问:这听起来效率低得可怕,你是个白痴吗?

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

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

答:嗯,用户在登录后做的第一件事是点击包含基于模型的报告的仪表板 - 所以我们需要在登录后立即启动并运行安全规则。

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

答:有点——我尝试启用更改数据库 read_committed_snapshot ON 但这似乎没有任何区别。回想起来,我认为我正在尝试执行 DROP/CREATE VIEW 并需要 Sch-M 锁这一事实意味着读取提交的快照隔离 (RCSI) 无济于事,因为它是关于处理 DML 语句的并发性,而我正在做 DDL。

问:您是否出于报告目的探索过整个数据库的数据库快照或镜像?

答:我不排除这种可能性,但我希望更多的是以应用程序为中心的解决方案,而不是进行基础设施更改。这将是资源利用和维护开销的跳跃,我需要将其升级给其他人。

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

A: 是的,AuthRuleCache 刷新过程包含在事务中,因为我想确保没有人看到不完整/无效的缓存,例如当 widget_authorized_123 因为用户会话过期而被删除时,widget_authorized 视图引用了 widget_authorized_123。我在没有事务的情况下进行了测试,死锁停止了,但我开始从 SQL Profiler 获取阻塞的进程报告。我在登录时看到了大约 15 秒的延迟,有时还会出现超时 - 所以把交易放回原处。

问:它多久发生一次?

A:AuthRuleCache 目前在生产环境中是关闭的,所以不会影响用户。我对 100 次连续登录的本地测试表明,可能有 10% 的死锁或失败。我怀疑对于在仪表板上拥有基于长期运行报表模型的报表的用户来说,情况会更糟。

问:报告快照如何?

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

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

答:以下是从 SQL Profiler 捕获的一个事务中针对一个用户登录发出的语句:

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

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
    )

删除用户“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]

为用户 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
    )

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

SELECT TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA + '.' + TABLE_NAME LIKE 'sec.actions_authorized_%'

删除现有的主操作视图

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

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

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
-- ...
4

3 回答 3

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 的建议并尽可能精简事务,将对于完成登录(例如过期旧会话)不重要的内容移出事务。

于 2013-01-29T10:28:06.823 回答
0

让我们将您的示例与小部件一起使用,我假设有一个表格说明每个用户授权了哪些小部件(如果您有用户组,它只是有点复杂)

当您使用 User_ID 时,我假设您有另一个带有用户登录名的表。

用户 (User_ID, Login) 小部件 (Widget_ID, ...) widgets_authorized (User_ID, Widget_ID)

将表 Widgets 重命名为 AllWidgets

创建视图小部件:

CREATE VIEW widgets
AS
SELECT AW.*
FROM AllWidgets AW
INNER JOIN widgets_authorized WA ON WA.Widget_ID = AW.Widget_ID
INNER JOIN Users U ON WA.User_ID = U.User_ID
WHERE U.Login = SYSTEM_USER

您可以保持以前的模型链接到视图小部件而不是以前的表格小部件,它们返回相同的列,数据根据连接的用户进行过滤。

如果你有性能问题试试这个,我有一个类似的问题:

CREATE VIEW widgets
AS
SELECT AW.*
FROM AllWidgets AW
INNER JOIN widgets_authorized WA ON WA.Widget_ID = AW.Widget_ID
WHERE WA.User_ID IN (SELECT U.User_ID FROM Users U WHERE U.Login = SYSTEM_USER)
于 2013-01-13T22:21:09.047 回答
0

另一个更接近你奇怪解决方案的建议。

与其使用单个架构创建多个视图,不如创建具有唯一名称和多个架构的视图:sec_182.actions_authorized

使用“FROM actions_authorized”运行查询,不要显式模式,sql 引擎将使用属于已连接用户模式的视图。

可以使用后台进程或在用户登录时创建模式及其视图(CREATE TRIGGER ... ON ALL SERVER ... AFTER LOGON ...)

于 2013-01-13T22:30:34.350 回答