SQL CLR中的多线程缓存

Mat*_*int 14 c# sql-server multithreading caching sqlclr

是否有任何多线程缓存机制可以在SQL CLR函数中工作,而不需要将程序集注册为"不安全"?

由于还介绍在这个职位,只需使用一个lock语句将抛出一个安全组件的异常:

System.Security.HostProtectionException: 
Attempted to perform an operation that was forbidden by the CLR host.

The protected resources (only available with full trust) were: All
The demanded resources were: Synchronization, ExternalThreading
Run Code Online (Sandbox Code Playgroud)

我希望对我的函数的任何调用都以线程安全的方式使用相同的内部缓存,以便许多操作可以同时执行缓存读取和写入.基本上 - 我需要一个ConcurrentDictionary可以在SQLCLR"安全"程序集中工作的程序.不幸的是,使用ConcurrentDictionary它本身会产生与上面相同的例外.

是否有内置的SQLCLR或SQL Server来处理这个问题?或者我误解了SQLCLR的线程模型?

我已经阅读了关于SQLCLR的安全限制的内容.特别是,以下文章可能有助于理解我在说什么:

此代码最终将成为分发给其他人的库的一部分,因此我真的不希望将其作为"不安全"运行.

我正在考虑的一个选项(由Spender在下面的评论中提出)是从SQLCLR代码中扩展到tempdb并将其用作缓存. 但我不太清楚到底该怎么做.我也不确定它是否会像内存缓存一样高效. 请参阅下面的更新

我对可能提供的任何其他替代方案感兴趣.谢谢.

下面的代码使用静态并发字典作为缓存,并通过SQL CLR用户定义的函数访问该缓存.对函数的所有调用都将使用相同的缓存.但除非装配被注册为"不安全",否则这将无效.

public class UserDefinedFunctions
{
    private static readonly ConcurrentDictionary<string,string> Cache =
                            new ConcurrentDictionary<string, string>();

    [SqlFunction]
    public static SqlString GetFromCache(string key)
    {
        string value;
        if (Cache.TryGetValue(key, out value))
            return new SqlString(value);
        return SqlString.Null;
    }

    [SqlProcedure]
    public static void AddToCache(string key, string value)
    {
        Cache.TryAdd(key, value);
    }
}
Run Code Online (Sandbox Code Playgroud)

它们位于一个名为的程序集中SqlClrTest,并使用以下SQL包装器:

CREATE FUNCTION [dbo].[GetFromCache](@key nvarchar(4000))
RETURNS nvarchar(4000) WITH EXECUTE AS CALLER
AS EXTERNAL NAME [SqlClrTest].[SqlClrTest.UserDefinedFunctions].[GetFromCache]
GO

CREATE PROCEDURE [dbo].[AddToCache](@key nvarchar(4000), @value nvarchar(4000))
WITH EXECUTE AS CALLER
AS EXTERNAL NAME [SqlClrTest].[SqlClrTest.UserDefinedFunctions].[AddToCache]
GO
Run Code Online (Sandbox Code Playgroud)

然后他们在数据库中使用如下:

EXEC dbo.AddToCache 'foo', 'bar'

SELECT dbo.GetFromCache('foo')
Run Code Online (Sandbox Code Playgroud)

UPDATE

我想出了如何使用Context Connection从SQLCLR访问数据库.此Gist中的代码显示了ConcurrentDictionary方法和tempdb方法.然后我进行了一些测试,从客户统计数据中测量了以下结果(平均10次试验):

Concurrent Dictionary Cache
10,000 Writes: 363ms
10,000 Reads :  81ms

TempDB Cache
10,000 Writes: 3546ms
10,000 Reads : 1199ms
Run Code Online (Sandbox Code Playgroud)

因此,抛出了使用tempdb表的想法.我真的没有别的可以尝试吗?

And*_*tan 5

我添加了一个类似的评论,但我会把它放在这里作为答案,因为我认为它可能需要一些背景知识.

ConcurrentDictionary正如您正确指出的那样,UNSAFE最终需要使用线程同步原语,因为它lock明确要求访问较低级别的OS资源,因此需要在SQL主机环境之外捕获代码.

因此,获得不需要的解决方案的唯一方法UNSAFE是使用不使用任何锁或其他线程同步原语的方法.但是,如果底层结构是.Net,Dictionary那么在多个线程之间共享它的唯一真正安全的方法是使用LockInterlocked.CompareExchange(看这里)旋转等待.我似乎无法找到关于后者是否在SAFE许可集下被允许的任何信息,但我的猜测是它不是.

我还要质疑在数据库引擎中应用基于CLR的解决方案的有效性,数据库引擎的索引和查找功能可能远远超过任何托管的CLR解决方案.

  • 尚未提出的一点是为什么sql在安全程序集中不允许这些锁定原语.原因是sqlos使用的合作调度.预计sql server中的任务会定期生成.如果一个线程正在运行clr代码并在没有屈服的情况下等待资源,则可能发生各种问题.即使.net锁确实导致线程产生,也可能需要等待4ms才能再次调度.互锁交换将是您最好的选择,但可能会迫使不安全的组装. (3认同)

Sol*_*zky 5

接受的答案不正确。Interlocked.CompareExchange不是一个选项,因为它需要共享资源来更新,并且无法在程序集中创建SAFE可以更新的所述静态变量。

(在大多数情况下)没有办法在程序集中跨调用缓存数据SAFE(也不应该有)。原因是有一个类的单个实例(好吧,在 App Domain 中,每个数据库每个所有者)在所有会话之间共享。这种行为通常是非常不受欢迎的。

但是,我确实说过“在大多数情况下”这是不可能的。有一种方法,虽然我不确定它是一个错误还是打算这样。我会犯错误,因为它是一个错误,因为再次跨会话共享变量是一项非常不稳定的活动。尽管如此,您可以(这样做的风险由您自己承担,并且这不是专门的线程安全,但可能仍然有效)修改static readonly 集合。是的。如:

using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.Collections;

public class CachingStuff
{
    private static readonly Hashtable _KeyValuePairs = new Hashtable();

    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static SqlString GetKVP(SqlString KeyToGet)
    {
        if (_KeyValuePairs.ContainsKey(KeyToGet.Value))
        {
            return _KeyValuePairs[KeyToGet.Value].ToString();
        }

        return SqlString.Null;
    }

    [SqlProcedure]
    public static void SetKVP(SqlString KeyToSet, SqlString ValueToSet)
    {
        if (!_KeyValuePairs.ContainsKey(KeyToSet.Value))
        {
            _KeyValuePairs.Add(KeyToSet.Value, ValueToSet.Value);
        }

        return;
    }

    [SqlProcedure]
    public static void UnsetKVP(SqlString KeyToUnset)
    {
        _KeyValuePairs.Remove(KeyToUnset.Value);
        return;
    }
}
Run Code Online (Sandbox Code Playgroud)

运行上面的代码,数据库设置为TRUSTWORTHY OFF,程序集设置为SAFE,我们得到:

EXEC dbo.SetKVP 'f', 'sdfdg';

SELECT dbo.GetKVP('f'); -- sdfdg

SELECT dbo.GetKVP('g'); -- NULL

EXEC dbo.UnsetKVP 'f';

SELECT dbo.GetKVP('f'); -- NULL
Run Code Online (Sandbox Code Playgroud)

话虽如此,可能有更好的方法 notSAFE但也不是UNSAFE。既然希望使用内存来缓存重复使用的值,为什么不设置一个memcachedredis服务器并创建 SQLCLR 函数来与之通信呢?那只需要将程序集设置为EXTERNAL_ACCESS.

这样你就不必担心几个问题:

  • 消耗大量可以/应该用于查询的内存。

  • 静态变量中保存的数据不会自动过期。它一直存在,直到您将其删除或应用程序域被卸载,这可能在很长一段时间内都不会发生。但是memcachedredis确实允许设置过期时间。

  • 这不是明确的线程安全的。但是缓存服务器是。