Многопоточное кэширование в SQL CLR
Существуют ли многопоточные механизмы кэширования, которые будут работать в функции CLR SQL, не требуя регистрации сборки как "небезопасной"?
Как также описано в этом сообщении, просто использование оператора 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. В частности, следующие статьи могут быть полезны для понимания того, о чем я говорю:
Этот код в конечном итоге будет частью библиотеки, которая будет распространена среди других, поэтому я действительно не хочу, чтобы ее запускали как "небезопасные".
Один из вариантов, который я рассматриваю (приведенный ниже в комментариях Spender), заключается в том, чтобы обратиться к tempdb из кода SQLCLR и использовать его как кеш. Но я не совсем точно знаю, как это сделать. Я также не уверен, будет ли он где-нибудь рядом с исполняемым файлом, как кеш в памяти. См. Обновление ниже.
Меня интересуют любые другие альтернативы, которые могут быть доступны. Спасибо.
Пример
В приведенном ниже коде используется статический параллельный словарь в виде кеша и доступ к этому кешу через пользовательские функции 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')
UPDATE
Я понял, как получить доступ к базе данных из 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. Я действительно ничего не могу попробовать?
Ответы
Ответ 1
Я добавил комментарий, который говорит что-то подобное, но я собираюсь записать его здесь как ответ вместо этого, потому что я думаю, что ему может понадобиться некоторый фон.
ConcurrentDictionary
, как вы правильно указали, в конечном итоге требует UNSAFE
, потому что он использует примитивы синхронизации потоков даже после lock
- это явно требует доступа к ресурсам ОС более низкого уровня и поэтому требует, чтобы код, среды размещения SQL.
Таким образом, единственный способ получить решение, не требующее UNSAFE
, - использовать тот, который не использует никаких блокировок или других примитивов синхронизации потоков. Однако, если базовая структура является .Net Dictionary
, тогда единственный по-настоящему безопасный способ поделиться ею через несколько потоков - использовать lock
или Interlocked.CompareExchange
(см. здесь) со спиновым ожиданием. Кажется, я не могу найти никакой информации о том, разрешено ли это в соответствии с набором разрешений SAFE
, но я предполагаю, что это не так.
Я бы также поставил под сомнение обоснованность применения решения CLR на эту проблему внутри механизма базы данных, чья функция индексирования и поиска, вероятно, будет намного больше, чем любое размещенное решение CLR.
Ответ 2
Принятый ответ неверен. Interlocked.CompareExchange
не является опцией, так как для обновления требуется общий ресурс, и нет возможности создать указанную статическую переменную в сборке SAFE
, которая может быть обновлена.
Существует (по большей части) способ кэширования данных через вызовы в сборке SAFE
(и не должно быть). Причина в том, что существует один экземпляр класса (ну, внутри домена приложения, который является для каждой базы данных для каждого владельца), который является общим для всех сеансов. Это поведение чаще всего нежелательно.
Однако я сказал "по большей части", что это было невозможно. Есть способ, хотя я не уверен, что это ошибка или намеревался быть таким образом. Я бы ошибался, когда это снова было ошибкой, поскольку обмен переменной между сессиями - очень опасная деятельность. Тем не менее, вы можете (сделайте это на свой страх и риск, и это не является конкретным потоком, но может по-прежнему работать) изменить коллекции 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
. Поскольку желание использовать память для кеширования многократно используемых значений, почему бы не настроить сервер memcached или redis и создать функции SQLCLR для связи с ним? Это потребовало бы установки сборки на EXTERNAL_ACCESS
.
Таким образом, вам не нужно беспокоиться о нескольких проблемах:
-
потребляет кучу памяти, которая может/должна использоваться для запросов.
-
нет автоматического истечения данных, хранящихся в статических переменных. Он существует до тех пор, пока вы его не удалите или домен приложения не будет выгружен, что может не произойти в течение длительного времени. Но memcached и redis позволяют установить время истечения срока действия.
-
это явно не безопасно для потоков. Но кэш-серверы.
Ответ 3
Функции блокировки SQL Server sp_getapplock
и sp_releaseapplock
могут использоваться в контексте SAFE. Используйте их для защиты обычного Dictionary
, и у вас есть кэш!
Цена блокировки намного хуже, чем обычная lock
, но это не может быть проблемой, если вы обращаетесь к кешу относительно грубо-зернистым способом.
--- ОБНОВЛЕНИЕ ---
Interlocked.CompareExchange
может использоваться в поле, содержащемся в статическом экземпляре. Статическая ссылка может быть сделана readonly
, но поле в ссылочном объекте все равно может быть изменчивым и, следовательно, использоваться Interlocked.CompareExchange
.
Оба варианта Interlocked.CompareExchange
и static readonly
разрешены в контексте SAFE. Производительность намного лучше, чем sp_getapplock
.
Ответ 4
На основе ответа Andras, вот моя имплантация "SharedCache" для чтения и записи в словаре в БЕЗОПАСНО.
EvalManager (Static)
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();
}
}
}
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 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);
}
}
}
SharedCache
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;
}
}
}
}
Исходные файлы:
Ответ 5
Удовлетворены ли ваши потребности табличной переменной? Они хранятся в памяти, как можно дольше, так что производительность должна быть отличной. Не очень полезно, если вам нужно поддерживать кеш между вызовами приложений, конечно.
Созданный как тип, вы также можете передать такую таблицу в sproc или UDF.