Многопоточное кэширование в SQL CLR - программирование
Подтвердить что ты не робот

Многопоточное кэширование в 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. Я действительно ничего не могу попробовать?

4b9b3361

Ответ 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.