Multithreaded caching in SQL CLR

匆匆过客 提交于 2019-12-17 22:26:06

问题


Are there any multithreaded caching mechanisms that will work in a SQL CLR function without requiring the assembly to be registered as "unsafe"?

As also described in this post, simply using a lock statement will throw an exception on a safe assembly:

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

I want any calls to my functions to all use the same internal cache, in a thread-safe manner so that many operations can do cache reads and writes simultaneously. Essentially - I need a ConcurrentDictionary that will work in a SQLCLR "safe" assembly. Unfortunately, using ConcurrentDictionary itself gives the same exception as above.

Is there something built-in to SQLCLR or SQL Server to handle this? Or am I misunderstanding the threading model of SQLCLR?

I have read as much as I can find about the security restrictions of SQLCLR. In particular, the following articles may be useful to understand what I am talking about:

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

This code will ultimately be part of a library that is distributed to others, so I really don't want to be required to run it as "unsafe".

One option that I am considering (brought up in comments below by Spender) is to reach out to tempdb from within the SQLCLR code and use that as a cache instead. But I'm not quite sure exactly how to do that. I'm also not sure if it will be anywhere near as performant as an in-memory cache. See update below.

I am interested in any other alternatives that might be available. Thanks.

Example

The code below uses a static concurrent dictionary as a cache and accesses that cache via SQL CLR user-defined functions. All calls to the functions will work with the same cache. But this will not work unless the assembly is registered as "unsafe".

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);
    }
}

These are in an assembly called SqlClrTest, and and use the following SQL wrappers:

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

Then they are used in the database like this:

EXEC dbo.AddToCache 'foo', 'bar'

SELECT dbo.GetFromCache('foo')

UPDATE

I figured out how to access the database from SQLCLR using the Context Connection. The code in this Gist shows both the ConcurrentDictionary approach, and the tempdb approach. I then ran some tests, with the following results measured from client statistics (average of 10 trials):

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

TempDB Cache
10,000 Writes: 3546ms
10,000 Reads : 1199ms

So that throws out the idea of using a tempdb table. Is there really nothing else I can try?


回答1:


I've added a comment that says something similar, but I'm going to put it here as an answer instead, because I think it might need some background.

ConcurrentDictionary, as you've correctly pointed out, requires UNSAFE ultimately because it uses thread synchronisation primitives beyond even lock - this explicitly requires access to lower-level OS resources, and therefore requires the code fishing outside of the SQL hosting environment.

So the only way you can get a solution that doesn't require UNSAFE, is to use one which doesn't use any locks or other thread synchronisation primitives. However, if the underlying structure is a .Net Dictionary then the only truly safe way to share it across multiple threads is to use Lock or an Interlocked.CompareExchange (see here) with a spin wait. I can't seem to find any information on whether the latter is allowed under the SAFE permission set, but my guess is that it's not.

I'd also be questioning the validity of applying a CLR-based solution to this problem inside a database engine, whose indexing-and-lookup capability is likely to be far in excess of any hosted CLR solution.




回答2:


The accepted answer is not correct. Interlocked.CompareExchange is not an option since it requires a shared resource to update, and there is no way to create said static variable, in a SAFE Assembly, that can be updated.

There is (for the most part) no way to cache data across calls in a SAFE Assembly (nor should there be). The reason is that there is a single instance of the class (well, within the App Domain which is per-database per-owner) that is shared across all sessions. That behavior is, more often than not, highly undesirable.

However, I did say "for the most part" it was not possible. There is a way, though I am not sure if it is a bug or intended to be this way. I would err on the side of it being a bug since again, sharing a variable across sessions is a very precarious activity. Nonetheless, you can (do so at your own risk, AND this is not specifically thread safe, but might still work) modify static readonly collections. Yup. As in:

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;
    }
}

And running the above, with the database set as TRUSTWORTHY OFF and the assembly set to SAFE, we get:

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

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

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

EXEC dbo.UnsetKVP 'f';

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

That all being said, there is probably a better way that is not SAFE but also not UNSAFE. Since the desire is to use memory for caching of repeatedly used values, why not set up a memcached or redis server and create SQLCLR functions to communicate with it? That would only require setting the assembly to EXTERNAL_ACCESS.

This way you don't have to worry about several issues:

  • consuming a bunch of memory which could/should be used for queries.

  • there is no automatic expiration of the data held in static variables. It exists until you remove it or the App Domain gets unloaded, which might not happen for a long time. But memcached and redis do allow for setting an expiration time.

  • this is not explicitly thread safe. But cache servers are.




回答3:


SQL Server locking functions sp_getapplock and sp_releaseapplock can be used in SAFE context. Employ them to protect an ordinary Dictionary and you have yourself a cache!

The price of locking this way is much worse than ordinary lock, but that may not be an issue if you are accessing your cache in a relatively coarsely-grained way.

--- UPDATE ---

The Interlocked.CompareExchange can be used on a field contained in a static instance. The static reference can be made readonly, but a field in the referenced object can still be mutable, and therefore usable by Interlocked.CompareExchange.

Both Interlocked.CompareExchange and static readonly are allowed in SAFE context. Performance is much better than sp_getapplock.




回答4:


Based on Andras answer, here is my implantation of a "SharedCache" to read and write in a dictionary in SAFE permission.

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'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);
        }
    }
}

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;
            }
        }
    }
}

Source Files:

  • 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




回答5:


Would your needs be satisfied with a table variable? They're kept in memory, as long as possible anyway, so performance should be excellent. Not so useful if you need to maintain your cache between app calls, of course.

Created as a type, you can also pass such a table into a sproc or UDF.



来源:https://stackoverflow.com/questions/16303557/multithreaded-caching-in-sql-clr

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!