SQL CLR 中的多线程缓存

Posted

技术标签:

【中文标题】SQL CLR 中的多线程缓存【英文标题】:Multithreaded caching in SQL CLR 【发布时间】:2013-04-24 14:09:09 【问题描述】:

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

正如in this post 所述,简单地使用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

我希望对我的函数的任何调用都以线程安全的方式使用相同的内部缓存,以便许多操作可以同时进行缓存读取和写入。本质上-我需要一个ConcurrentDictionary,它将在SQLCLR“安全”程序集中工作。不幸的是,使用ConcurrentDictionary 本身会产生与上述相同的异常。

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

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

SQL Server CLR Integration Part 1: Security Deploy/Use assemblies which require Unsafe/External Access with CLR and T-SQL

此代码最终将成为分发给其他人的库的一部分,因此我真的不想被要求以“不安全”的方式运行它。

我正在考虑的一个选项(由 Spender 在下面的 cmets 中提出)是从 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);
    

它们位于名为 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

然后它们在数据库中的使用是这样的:

EXEC dbo.AddToCache 'foo', 'bar'

SELECT dbo.GetFromCache('foo')

更新

我想出了如何使用Context Connection 从 SQLCLR 访问数据库。代码in this 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

这样就放弃了使用 tempdb 表的想法。真的没有别的我可以尝试的吗?

【问题讨论】:

不完全清楚你在问她什么 9 虽然这可能只是我 :])。什么代码抛出了这个异常,你想做什么? @Killercam - 添加了代码示例。 也许更好的缓存位置是在数据库本身作为表?届时您将免费获得同步。 @spender - 我想你可能正在做点什么。我可以在 SQLCLR 函数中管理我自己的 tempdb 数据吗?知道它是否会像内存缓存一样执行吗?如果您可以通过代码示例显示答案,那就太棒了。 @StrayCatDBA 然后我得到一个缓存未命中,下一次加载需要更长的时间,同时操作再次执行,但它会再次缓存,直到下一次卸载。当您要执行 1000 次某事时,请执行一次并缓存它。如果你回来的时候缓存已经不见了,所以你必须再做一次,那仍然是你可以使用缓存的 999 次。大多数缓存都遵循这一原则。 【参考方案1】:

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

ConcurrentDictionary,正如您正确指出的那样,最终需要UNSAFE,因为它使用的线程同步原语甚至超出了lock - 这明确需要访问较低级别的操作系统资源,因此需要在外面钓鱼SQL 托管环境。

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

我也会质疑在数据库引擎中应用基于 CLR 的解决方案来解决此问题的有效性,其索引和查找能力可能远远超过任何托管 CLR 解决方案。

【讨论】:

我尝试了一个更加以数据库为中心的解决方案,使用 tempdb 作为缓存。它在“安全”组件中工作,但性能要慢 10 到 15 倍。请参阅我的问题中的更新。谢谢。 还有一点没有提到,为什么 sql 不允许在安全程序集中使用这些锁定原语。原因是 sqlos 使用的协作调度。 sql server 中的任务预计会定期产生。如果一个线程正在运行 clr 代码并在不让步的情况下等待资源,则可能会发生各种问题。即使 .net 锁确实导致线程屈服,它也可能会导致 4 毫秒的等待,直到它再次被调度。联锁交换将是您最好的选择,但可能会导致不安全的组装。 @MattJohnson 对于您使用表格的解决方案,您为什么继续通过 CLR 函数来做呢?对这种方法的更好测试是编写简单的 SQL 函数/过程 - ADO.Net 上下文连接会慢得多。 我已经在 C++ 中实现了他的无锁队列/堆栈,然后尝试在 C# 中做同样的事情;不可能有效地做到这一点(即它仍然有效,但并不比简单地使用锁更好),因为您无法访问 .Net 中的低级 CAS 函数。是的,您确实有 Interlocked.CompareExchange,但不是双字,它只是操作系统功能的包装器。虽然版本化的指针几乎不可能在.Net 中以这样的方式正确实现,以至于它可以被视为 CAS 函数的数字。无论如何,即使是 - 它仍然需要是 UNSAFE @Triynko 和 Andras,Interlocked.CompareExchangeSAFE 程序集中是允许的,但它实际上是无用的,因为它需要对共享变量进行操作。 不会在SAFE 程序集中发生:变量必须是静态的(不会发生)或者将作用于实例级变量的线程需要从中启动该类的实例,但这也不会发生,因为所有 SQLCLR 方法都是static。请参阅我对 a) 如何使用 SAFE 完成此操作(即使可能有风险)和 b) 替代提案的答案。【参考方案2】:

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

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

但是,我确实说过“在大多数情况下”这是不可能的。有一种方法,虽然我不确定它是一个错误还是打算这样。我会错误地认为它是一个错误,因为在会话之间共享一个变量是一个非常不稳定的活动。尽管如此,您可以(这样做需要您自担风险,并且这不是特别线程安全的,但可能仍然有效)修改static readonlycollections。对。如:

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;
    

运行上面的代码,数据库设置为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

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

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

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

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

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

【讨论】:

我想这可能正是我想要的。我将尝试这种方法,并让您知道它在我打算使用的场景中效果如何。如果一切顺利,我会将其更改为已接受的答案。谢谢! @MattJohnson 我更新了关于线程安全的注释,因为您在问题中提到了它。能问一下剧情是什么吗?我很好奇为什么要避免将 Assembly 设置为 UNSAFE,而不仅仅是偏好(我当然理解)。 实际上,事实证明毕竟可以使用 Interlocked.CompareExchange。您可以只声明一个 reference 类型的静态只读字段,其中包含一个 int 字段,然后将该字段用于联锁操作。 正如您所说,这种方法不是线程安全的,因此不适合此目的。 Hashtable 确实有一个Synchronized 包装器,但它有自己的一组锁定,我很确定它在 SQLCLR 的“安全”模式下不起作用(尽管我还没有尝试过)。仍然in the MSDN 描述的限制似乎是个问题。 我同意在分布式解决方案中使用不同的缓存服务器是一种选择,但在这种特殊情况下,我正在寻找专门针对 SQLCLR 的库的答案,范围仅限于单个服务器。 【参考方案3】:

SQL Server 锁定函数 sp_getapplocksp_releaseapplock 可以在 SAFE 上下文中使用。使用它们来保护一个普通的Dictionary,你就有了自己的缓存!

以这种方式锁定的代价比普通的lock 差很多,但如果您以相对粗粒度的方式访问缓存,这可能不是问题。

--- 更新 ---

Interlocked.CompareExchange 可用于静态实例中包含的字段。 readonly 可以进行静态引用,但是被引用对象中的字段仍然可以是可变的,因此可以被 Interlocked.CompareExchange 使用。

Interlocked.CompareExchangestatic readonly 在 SAFE 上下文中都是允许的。性能比sp_getapplock好很多。

【讨论】:

【参考方案4】:

根据 Andras 的回答,这是我在 SAFE 权限下植入“SharedCache”以在字典中读写。

EvalManager(静态)

using System;
using System.Collections.Generic;
using Z.Expressions.SqlServer.Eval;

namespace Z.Expressions

    /// <summary>Manager class for eval.</summary>
    public static class EvalManager
    
        /// <summary>The cache for EvalDelegate.</summary>
        public static readonly SharedCache<string, EvalDelegate> CacheDelegate = new SharedCache<string, EvalDelegate>();

        /// <summary>The cache for SQLNETItem.</summary>
        public static readonly SharedCache<string, SQLNETItem> CacheItem = new SharedCache<string, SQLNETItem>();

        /// <summary>The shared lock.</summary>
        public static readonly SharedLock SharedLock;

        static EvalManager()
        
            // ENSURE to create lock first
            SharedLock = new SharedLock();
        
    

共享锁

using System.Threading;

namespace Z.Expressions.SqlServer.Eval

    /// <summary>A shared lock.</summary>
    public class SharedLock
    
        /// <summary>Acquires the lock on the specified lockValue.</summary>
        /// <param name="lockValue">[in,out] The lock value.</param>
        public static void AcquireLock(ref int lockValue)
        
            do
            
                // TODO: it's possible to wait 10 ticks? Thread.Sleep doesn't really support it.
             while (0 != Interlocked.CompareExchange(ref lockValue, 1, 0));
        

        /// <summary>Releases the lock on the specified lockValue.</summary>
        /// <param name="lockValue">[in,out] The lock value.</param>
        public static void ReleaseLock(ref int lockValue)
        
            Interlocked.CompareExchange(ref lockValue, 0, 1);
        

        /// <summary>Attempts to acquire lock on the specified lockvalue.</summary>
        /// <param name="lockValue">[in,out] The lock value.</param>
        /// <returns>true if it succeeds, false if it fails.</returns>
        public static bool TryAcquireLock(ref int lockValue)
        
            return 0 == Interlocked.CompareExchange(ref lockValue, 1, 0);
        
    

共享缓存

using System;
using System.Collections.Generic;

namespace Z.Expressions.SqlServer.Eval

    /// <summary>A shared cache.</summary>
    /// <typeparam name="TKey">Type of key.</typeparam>
    /// <typeparam name="TValue">Type of value.</typeparam>
    public class SharedCache<TKey, TValue>
    
        /// <summary>The lock value.</summary>
        public int LockValue;

        /// <summary>Default constructor.</summary>
        public SharedCache()
        
            InnerDictionary = new Dictionary<TKey, TValue>();
        

        /// <summary>Gets the number of items cached.</summary>
        /// <value>The number of items cached.</value>
        public int Count
        
            get  return InnerDictionary.Count; 
        

        /// <summary>Gets or sets the inner dictionary used to cache items.</summary>
        /// <value>The inner dictionary used to cache items.</value>
        public Dictionary<TKey, TValue> InnerDictionary  get; set; 

        /// <summary>Acquires the lock on the shared cache.</summary>
        public void AcquireLock()
        
            SharedLock.AcquireLock(ref LockValue);
        

        /// <summary>Adds or updates a cache value for the specified key.</summary>
        /// <param name="key">The cache key.</param>
        /// <param name="value">The cache value used to add.</param>
        /// <param name="updateValueFactory">The cache value factory used to update.</param>
        /// <returns>The value added or updated in the cache for the specified key.</returns>
        public TValue AddOrUpdate(TKey key, TValue value, Func<TKey, TValue, TValue> updateValueFactory)
        
            try
            
                AcquireLock();

                TValue oldValue;
                if (InnerDictionary.TryGetValue(key, out oldValue))
                
                    value = updateValueFactory(key, oldValue);
                    InnerDictionary[key] = value;
                
                else
                
                    InnerDictionary.Add(key, value);
                

                return value;
            
            finally
            
                ReleaseLock();
            
        

        /// <summary>Adds or update a cache value for the specified key.</summary>
        /// <param name="key">The cache key.</param>
        /// <param name="addValueFactory">The cache value factory used to add.</param>
        /// <param name="updateValueFactory">The cache value factory used to update.</param>
        /// <returns>The value added or updated in the cache for the specified key.</returns>
        public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
        
            try
            
                AcquireLock();

                TValue value;
                TValue oldValue;

                if (InnerDictionary.TryGetValue(key, out oldValue))
                
                    value = updateValueFactory(key, oldValue);
                    InnerDictionary[key] = value;
                
                else
                
                    value = addValueFactory(key);
                    InnerDictionary.Add(key, value);
                


                return value;
            
            finally
            
                ReleaseLock();
            
        

        /// <summary>Clears all cached items.</summary>
        public void Clear()
        
            try
            
                AcquireLock();
                InnerDictionary.Clear();
            
            finally
            
                ReleaseLock();
            
        


        /// <summary>Releases the lock on the shared cache.</summary>
        public void ReleaseLock()
        
            SharedLock.ReleaseLock(ref LockValue);
        

        /// <summary>Attempts to add a value in the shared cache for the specified key.</summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <returns>true if it succeeds, false if it fails.</returns>
        public bool TryAdd(TKey key, TValue value)
        
            try
            
                AcquireLock();

                if (!InnerDictionary.ContainsKey(key))
                
                    InnerDictionary.Add(key, value);
                

                return true;
            
            finally
            
                ReleaseLock();
            
        

        /// <summary>Attempts to remove a key from the shared cache.</summary>
        /// <param name="key">The key.</param>
        /// <param name="value">[out] The value.</param>
        /// <returns>true if it succeeds, false if it fails.</returns>
        public bool TryRemove(TKey key, out TValue value)
        
            try
            
                AcquireLock();

                var isRemoved = InnerDictionary.TryGetValue(key, out value);
                if (isRemoved)
                
                    InnerDictionary.Remove(key);
                

                return isRemoved;
            
            finally
            
                ReleaseLock();
            
        

        /// <summary>Attempts to get value from the shared cache for the specified key.</summary>
        /// <param name="key">The key.</param>
        /// <param name="value">[out] The value.</param>
        /// <returns>true if it succeeds, false if it fails.</returns>
        public bool TryGetValue(TKey key, out TValue value)
        
            try
            
                return InnerDictionary.TryGetValue(key, out value);
            
            catch (Exception)
            
                value = default(TValue);
                return false;
            
        
    

源文件:

https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/EvalManager/EvalManager.cs

https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/Shared/SharedLock.cs

https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/Shared/SharedCache.cs

【讨论】:

这看起来很有希望。我可能需要一段时间才能对其进行测试,但谢谢! 我是否遗漏了什么,或者这实际上不会共享值,因为InnerDictionary 未声明为静态的? 好的,我知道了。我会在接下来的一两周内玩这个。我现在的心思在其他事情上。但是非常感谢!【参考方案5】:

您的需求会满足表变量吗?无论如何,它们会尽可能长时间地保存在内存中,因此性能应该非常出色。当然,如果您需要在应用调用之间维护缓存,那它就不是那么有用了。

作为类型创建,您也可以将这样的表传递给 sproc 或 UDF。

【讨论】:

我需要缓存的数据必须能够跨线程和进程生存。如果你能告诉我如何保持一个全局范围的表变量,你将成为我的新英雄。 :) @MattJohnson 和 Jon,即使有某种全局表变量,您也不会在临时表上节省太多,因为表变量保存在 tempdb 中;它们比临时表更轻,但不是 100% 内存。请参阅我的答案以了解如何执行此操作(即使有风险)以及替代建议:)。

以上是关于SQL CLR 中的多线程缓存的主要内容,如果未能解决你的问题,请参考以下文章

反应式编程Reactor中的多线程

现代 CPU 中的多线程旧遗留应用程序 [关闭]

Linux 操作系统原理 — NUMA 架构中的多线程调度开销与性能优化

Linux 操作系统原理 — 进程管理 — NUMA 架构中的多线程调度开销与性能优化

Day765.Redis 6.0的新特性:多线程客户端缓存与安全 -Redis 核心技术与实战

Day765.Redis 6.0的新特性:多线程客户端缓存与安全 -Redis 核心技术与实战