SQL CLR中的multithreading缓存

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

正如本文所述,只需使用lock语句就会在安全程序集上引发exception:

 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 

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

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

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

  • SQL Server CLR集成第1部分:安全性
  • 使用CLR和T-SQL部署/使用需要不安全/外部访问的程序集

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

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

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

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

 public class UserDefinedFunctions { private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); [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的程序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') 

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 

因此,抛出使用tempdb表的想法。 我真的没有别的办法吗?

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

正如您正确指出的那样, ConcurrentDictionary最终需要UNSAFE因为它使用甚至超出lock线程同步原语 – 这显然需要访问较低级别的OS资源,因此需要在SQL托管环境之外捕获代码。

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

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

接受的答案是不正确的。 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; } } 

运行上面的操作,将数据库设置为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确实允许设置到期时间。

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

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

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

—更新—

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

在SAFE上下文中允许Interlocked.CompareExchangestatic readonly 。 性能比sp_getapplock好得多。

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

EvalManager(静态)

 using System; using System.Collections.Generic; using Z.Expressions.SqlServer.Eval; namespace Z.Expressions { /// Manager class for eval. public static class EvalManager { /// The cache for EvalDelegate. public static readonly SharedCache CacheDelegate = new SharedCache(); /// The cache for SQLNETItem. public static readonly SharedCache CacheItem = new SharedCache(); /// The shared lock. public static readonly SharedLock SharedLock; static EvalManager() { // ENSURE to create lock first SharedLock = new SharedLock(); } } } 

SharedLock

 using System.Threading; namespace Z.Expressions.SqlServer.Eval { /// A shared lock. public class SharedLock { /// Acquires the lock on the specified lockValue. /// [in,out] The lock value. 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)); } /// Releases the lock on the specified lockValue. /// [in,out] The lock value. public static void ReleaseLock(ref int lockValue) { Interlocked.CompareExchange(ref lockValue, 0, 1); } /// Attempts to acquire lock on the specified lockvalue. /// [in,out] The lock value. /// true if it succeeds, false if it fails. public static bool TryAcquireLock(ref int lockValue) { return 0 == Interlocked.CompareExchange(ref lockValue, 1, 0); } } } 

SharedCache

 using System; using System.Collections.Generic; namespace Z.Expressions.SqlServer.Eval { /// A shared cache. /// Type of key. /// Type of value. public class SharedCache { /// The lock value. public int LockValue; /// Default constructor. public SharedCache() { InnerDictionary = new Dictionary(); } /// Gets the number of items cached. /// The number of items cached. public int Count { get { return InnerDictionary.Count; } } /// Gets or sets the inner dictionary used to cache items. /// The inner dictionary used to cache items. public Dictionary InnerDictionary { get; set; } /// Acquires the lock on the shared cache. public void AcquireLock() { SharedLock.AcquireLock(ref LockValue); } /// Adds or updates a cache value for the specified key. /// The cache key. /// The cache value used to add. /// The cache value factory used to update. /// The value added or updated in the cache for the specified key. public TValue AddOrUpdate(TKey key, TValue value, Func 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(); } } /// Adds or update a cache value for the specified key. /// The cache key. /// The cache value factory used to add. /// The cache value factory used to update. /// The value added or updated in the cache for the specified key. public TValue AddOrUpdate(TKey key, Func addValueFactory, Func 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(); } } /// Clears all cached items. public void Clear() { try { AcquireLock(); InnerDictionary.Clear(); } finally { ReleaseLock(); } } /// Releases the lock on the shared cache. public void ReleaseLock() { SharedLock.ReleaseLock(ref LockValue); } /// Attempts to add a value in the shared cache for the specified key. /// The key. /// The value. /// true if it succeeds, false if it fails. public bool TryAdd(TKey key, TValue value) { try { AcquireLock(); if (!InnerDictionary.ContainsKey(key)) { InnerDictionary.Add(key, value); } return true; } finally { ReleaseLock(); } } /// Attempts to remove a key from the shared cache. /// The key. /// [out] The value. /// true if it succeeds, false if it fails. 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(); } } /// Attempts to get value from the shared cache for the specified key. /// The key. /// [out] The value. /// true if it succeeds, false if it fails. public bool TryGetValue(TKey key, out TValue value) { try { return InnerDictionary.TryGetValue(key, out value); } catch (Exception) { value = default(TValue); return false; } } } } 

源文件:

您的需求是否满足于表变量? 尽管如此,它们仍然留在记忆中,因此性能应该非常出色。 当然,如果你需要在应用程序调用之间维护缓存,那就不那么有用了。

创建为类型,您还可以将此类表传递给sproc或UDF。